# iOS emergency receipt compatibility — 2026-10-09

> Superseded on 2026-10-15: the active checkout routes now use the July-style legacy controllers. `IOS_PURCHASE_VALIDATION_MODE` no longer selects the `/api/validate-receipt` handler. See `PAYMENT_LOGIC_ROLLBACK_2026_10_15.md` for the current deployment and testing instructions. The historical implementation remains in Git for reference.

## Scope

Temporary, explicitly selected alternative for **v2 iOS checkout, restore and the read-only historical transaction recovery check**. The app can remain on version 1.4.6 with `https://api.v2.appconjugate.com/api`. No mobile rebuild, migration, Android change or production webhook URL change is required for this patch.

This is not deployed automatically by changing source files. Deploy the patched backend and change the active v2 server configuration as below. Do not enable migration execution for this release.

## Enable on v2 only

```dotenv
IOS_PURCHASE_VALIDATION_MODE=receipt_compatibility
```

Keep the already configured:

```dotenv
APP_ENV=production
APP_DEBUG=false
APPLE_STORE_BUNDLE_ID=app.conjugate.ios
APPLE_STORE_APP_ID=6560107571
APPLE_STORE_ALLOWED_ENVIRONMENTS=Production,Sandbox
```

Keep the **existing, valid** `APPLE_SHARED_SECRET`. Do not put a placeholder in its place. This mode does not use `APPLE_STORE_ISSUER_ID`, `APPLE_STORE_KEY_ID`, `APPLE_STORE_PRIVATE_KEY_PATH` or `APPLE_STORE_ROOT_CA_PATHS` for checkout/restore/recovery. Do not delete those settings: the v2 webhook and server-api mode still require them.

Refresh configuration using the existing deployment process. If clearing a stale config cache through the workflow, select its `artisan` operation and arguments `["config:clear"]`. Do not run `config:cache`, `optimize`, `key:generate` or migrations. Do not change APP_KEY.

**Leave the Apple production notification URL on the old backend.** Both backends must use the same intended production database for existing purchase/account linking. Ensure the actual deployed old handler remains functional; the locally inspected backup is not proof of the live server revision.

## What the switch does

1. Authenticate the Conju Gate user as before.
2. Send the receipt to Apple's production `verifyReceipt` HTTPS endpoint with the shared secret; retry sandbox only on Apple's `21007` response.
3. Require successful Apple verification, matching bundle/app identity, permitted environment and a receipt containing the requested supported product.
4. Resolve the latest transaction in its original-transaction chain, including effective upgrades. Check dates, refund/revocation, superseded transactions, grace period and billing retry. A receipt with status 0 alone is **not** sufficient to grant access.
5. Keep the repository's cross-account ownership protection and idempotent row adoption. Do not transfer purchases, accept arbitrary client product claims or use the old `in_app[0]` shortcut.
6. Save the verified tier and return the current frontend-compatible `entitlement_granted` result. Recovery stays read-only and may finish only a matched transaction whose chain is proven expired or revoked; active/grace/retry/uncertain cases remain protected.

Missing/invalid data or an Apple outage never triggers an automatic unsafe fallback. `dev=true` remains blocked outside local/testing. This mode is deployment-wide for iOS requests reaching this v2 instance, not gated by the client version. Old request contracts remain accepted; Android switches are independent.

## Shared database and old webhook limitation

The old webhook writes `purchases.status`, `purchases.is_pro` and `users.pro_mode`, but does not maintain the new expiry column. Receipt mode therefore sets `purchases.expires_at=NULL` **only on the specific Apple-verified row it saves**. Actual expiry is still checked before granting access and returned in the validation response. Leaving a stale database expiry would incorrectly block a later renewal written by the old webhook.

There is no bulk cleanup or change to unrelated purchase rows, account bindings or Android entitlements. Existing v2 rows with stale expiry are not all repaired on deployment; each requires verified recovery/restore or a separately reviewed reconciliation. Do not run an indiscriminate SQL NULL/update across purchases.

This temporary design relies on the existing webhook for later expiry/refund updates when the app is not validating receipts. Missed or incorrect legacy webhook events can therefore leave stale access, as with the old system. The patch does not fix the legacy webhook's security/ordering/cancellation behavior. Do not treat it as a permanent replacement for the stricter store-state workflow. Confirm renewals and expiration through the actual old webhook before broad release.

The new v2 webhook has **not** been weakened: it still requires signed-payload verification and Server API configuration. If the sandbox URL still points to v2, its API-configuration errors will not be fixed by this checkout switch. For an end-to-end compatibility test, deliberately route sandbox notifications to the legacy sandbox handler using the matching test database, or complete v2's Apple API configuration first. Never redirect production notifications for a test.

## Test before release / incident recovery

1. Back up the database and record the current deployed revision/configuration. Do not delete purchases or rotate APP_KEY as troubleshooting.
2. Deploy this patch to the intended backend, enable the switch, refresh config. Existing required purchase columns/indexes from the prior release must already exist; this patch adds no schema.
3. With a dedicated Conju Gate account and Apple sandbox account, confirm prov1 then prov2 in separate clean test cases. Check `POST /api/validate-receipt` returns HTTP 200 with `success=true`, `state=entitlement_granted`, and `/api/user` reports the correct tier.
4. Check that the saved receipt reference is Apple's original transaction ID (the legacy webhook's lookup key), platform is ios, and this row's expiry column is NULL in compatibility mode.
5. Retry Restore on the same account: same purchase row, no duplicate start notification. Another account must not claim it. Do not buy again merely because server validation previously failed; try recovery/Restore with the original account first.
6. Exercise the queued-expired-prov1 recovery path: it must work without Server API credentials while preserving active/pending transactions. Test declined verification, network failure, and expired/refunded receipts: no unverified Pro grant.
7. Verify a renewal and expiration through the **existing** webhook, then reopen the app/check the profile. Confirm the webhook and app are using the same database. A first notification arriving before receipt validation may legitimately be unlinked; successful validation plus a later renewal verifies continuing linkage.
8. Recheck an existing older-app subscriber and Android purchase behavior. No mobile change is part of this patch.

Sandbox accepted on a production-database backend creates real test rows and entitlements in that database. Use dedicated accounts; never clear production tables.

## Reversal

```dotenv
IOS_PURCHASE_VALIDATION_MODE=server_api
```

`server_api` remains the default when unset. Invalid mode names fail closed. Switch back only after the API signing credentials and trusted certificates work and webhook/expiry ownership has been coordinated. Re-enabling server-api mode resumes expiry-aware writes on subsequent validations; it does not backfill every receipt-mode row. Plan a verified reconciliation before relying on expiry for all legacy rows. Do not simply switch modes while leaving competing webhook writers unmanaged.

## Verification boundaries

Automated tests use mocked Apple transport and in-memory SQLite, not production. They cover both products, old/new request versions, restore idempotency, ownership, expiry/refund/upgrade/grace/retry behavior, read-only recovery, unavailable API credentials, failure behavior, switch reversal and unchanged webhook signature requirements. They cannot certify the deployed .env, Apple credentials, the live legacy webhook or live renewal delivery. Perform the sandbox smoke test after deployment.

The older verifyReceipt API is deprecated by Apple. This is an emergency compatibility route, not a recommendation to extend its lifetime indefinitely. Relevant Apple references: [receipt expiry](https://developer.apple.com/documentation/appstorereceipts/expires_date_ms), [latest receipt information](https://developer.apple.com/documentation/appstorereceipts/responsebody/latest_receipt_info-data.dictionary).

Credential exposure from the troubleshooting conversation also requires coordinated replacement across every affected deployment. Do not include secrets or raw receipts in logs, Git or test reports.
