# Android purchase acceptance: staging vs production

## Purpose and scope

`ANDROID_PURCHASE_ENVIRONMENT` controls which **Google-verified receipts** this
server accepts. It does not select test cards or change Google's store environment.
No mobile build flavour, Gradle change, purchase-token deletion or database migration.
No iOS handling changed. `utils/Config.tsx` remains the mobile endpoint switch.

| Value | Receipt acceptance | Deployment use |
| --- | --- | --- |
| `legacy` (default) | Previous acceptance of real and test receipts | Backward-compatible opt-out; does not provide isolation |
| `sandbox` | Only Google's `testPurchase` receipts | Staging/local/testing only |
| `production` | Only receipts without Google's `testPurchase` marker | Production |

Google's empty JSON `testPurchase: {}` decodes to PHP `[]` and is still a test
purchase. Malformed markers are rejected, not guessed to be real. Client request
flags, local DB tiers, APK debug/release type and API URL cannot classify a receipt.

## Configuration and mandatory pre-release check

Staging: `APP_ENV=staging`, `ANDROID_PURCHASE_ENVIRONMENT=sandbox`,
`GOOGLE_PLAY_WEBHOOK_PURCHASE_ENVIRONMENT=sandbox`.

Production: `APP_ENV=production`, `ANDROID_PURCHASE_ENVIRONMENT=production`,
`GOOGLE_PLAY_WEBHOOK_PURCHASE_ENVIRONMENT=production`.

Refresh configuration using the existing deployment process, then run:

```sh
php artisan purchases:inspect-google-subscription --require-isolation
```

This is config-only without a row ID: no database lookup or Google request.
It exits nonzero if the purchase and RTDN modes do not match the deployment.
`sandbox` on a production server is rejected by both receipt and webhook runtime policies as well.
This fail-closed guard can block checkout if misconfigured: do not release until
the read-only check succeeds. No deployment workflow or live `.env` was modified.

For the existing GitHub Action: operation `artisan`, correct environment, arguments
`["purchases:inspect-google-subscription", "--require-isolation"]`; retrieve output
with the existing show_output/status mechanism. Do not use a production row ID
while diagnosing staging.

## Protected paths

- Upgrade preflight: current real receipt on sandbox returns
  `real_purchase_in_sandbox` before checking local ownership; test receipt on
  production returns `test_purchase_in_production`. No store request is launched
  by the current frontend for either response.
- Explicitly EXPIRED + past-expiry cached receipts may return `action=new` even
  if they originated in the other environment. That merely omits the old token
  from a fresh checkout; it never restores, acknowledges or transfers the receipt.
- Both compatibility and v2 validation check environment before grants, pending
  rows, expiry updates, linked-token handling, acknowledgement or notifications.
- Strict-mode upgrade validation also verifies the linked old token's environment
  with Google before retiring its local row. A client skipping preflight cannot
  bypass that check. This adds one Google lookup for linked purchases in strict
  mode; Google/network failure remains retryable and must not trigger a new charge.
- Scheduled acknowledgement recovery skips other-environment rows unchanged.
- RTDN keeps its **existing, separate** routing policy, with an added guard against
  sandbox mode on a production server. Configure both modes
  consistently; the new purchase variable alone does not isolate webhook handling.
- Existing historical mixed-environment rows are not cleaned up, transferred or
  revoked. They can still affect existing entitlement aggregation until reviewed;
  use a clean staging user for QA rather than resetting production ownership.

## Compatibility and rollback

The default `legacy` value preserves old acceptance. Real receipts remain accepted
in `production` mode for 1.4.4, missing-version/non-opted-in requests and 1.4.6.
Version selection and upgrade opt-in still use the existing minimum/main switch.

Environment isolation is an explicit **server-wide operational policy**, independent
of version selection: turning v2 off cannot make staging acknowledge real purchases.
Old apps may show a generic error for a wrong-environment receipt; their ordinary
real-money production flow is unchanged. To disable only this new filter, use
`ANDROID_PURCHASE_ENVIRONMENT=legacy` and refresh configuration. That removes
receipt isolation; it does not alter the separately configured RTDN filter.

Do not enable sandbox acceptance on production, remove account ownership checks,
reuse a real purchase token for test upgrades, or automatically fall back to a
second subscription after a replacement error.

## Read-only diagnosis of the reported purchase

```sh
php artisan purchases:inspect-google-subscription 123 --user-id=456
```

Replace these example numbers with a purchase row ID and optional current user ID
from that environment. A token is loaded only inside the backend, then queried
using the existing Google API service. Output contains bounded store state,
UTC expiry, environment classification, local tier, owner-exists and owner-match
booleans. It contains no receipt/token, linked token, account identifier, email,
Google credentials or raw HTTP error. No acknowledgement or DB writes occur.

The actual live Google expiry/test flag is not known merely because these tests
pass; run this command on the relevant staging row after deployment. If it reports
EXPIRED with past expiry but current preflight still returns account_mismatch,
check deployed release identity, selected row, config cache and server clock.

## QA / release acceptance

1. New sandbox `prov1` and `prov2`, restore, and active sandbox prov1→prov2 work.
2. Cancelled-but-unexpired real prov1 is not replaced from staging.
3. An expired real cache token allows ordinary checkout, not a replacement.
4. Real receipts on staging and test receipts on production perform no grant,
   acknowledgement, ownership transfer, pending row insert or linked-row retirement.
5. Real production checkout and restore work for old 1.4.4 and new 1.4.6, including
   the main-switch compatibility fallback.
6. The production isolation check succeeds; release APK contains the production URL.

Always verify **Test card** before confirming QA checkout. Receipt filtering happens
after a new purchase and cannot undo or prevent a real card payment at Google.
Use a dedicated Google license tester with no live real subscription plus one
consistent staging Conju Gate login. Same package/products means changing only
the backend URL does not create independent Google Play ownership.

The app's `docs/ANDROID_SANDBOX_PRODUCTION_TEST.md` contains the step-by-step QA guide.

Sources: [testing](https://developer.android.com/google/play/billing/test),
[subscriptionsv2](https://developers.google.com/android-publisher/api-ref/rest/v3/purchases.subscriptionsv2).
