import { applyGooglePlayPurchase, GOOGLE_PLAY_ACKNOWLEDGED_STATE, type GooglePlayPurchaseApi, type GooglePlayPurchaseStore, type StoredGooglePlayPurchase, type VerifiedGooglePlayPurchaseRecord, } from './google-play-apply.ts' import { GooglePlayVerificationError, sha256Hex, type NormalizedGooglePlayPurchase } from './google-play.ts' import { classifyGooglePlayPersistenceError, createSupabaseGooglePlayPurchaseStore, GooglePlayPurchasePersistenceError, } from './google-play-purchase-store.ts' function assert(condition: boolean, message: string): asserts condition { if (!condition) throw new Error(message) } const USER_ID = '11111111-2222-3333-4444-555555555555' const PRODUCT_ID = 'd3ro_voice_pro_monthly' const TOKEN = 'purchase-token-0001' const PENDING_ACK = 'ACKNOWLEDGEMENT_STATE_PENDING' function purchase(overrides: Partial = {}): NormalizedGooglePlayPurchase { return { platform: 'google_play', productId: PRODUCT_ID, tier: 'pro', storeTransactionId: 'GPA.1234-5678-9012-34567', purchaseState: 'purchased', purchaseAt: '2029-12-01T00:00:00Z', expiresAt: '2030-01-01T00:00:00Z', autoRenewing: true, acknowledged: false, entitled: true, verification: { subscriptionState: 'SUBSCRIPTION_STATE_ACTIVE', acknowledgementState: PENDING_ACK, lineItems: [{ productId: PRODUCT_ID, expiryTime: '2030-01-01T00:00:00Z' }], }, linkedPurchaseToken: null, ...overrides, } } class FakePlayApi implements GooglePlayPurchaseApi { verifyCalls = 0 acknowledged: Array<{ productId: string; purchaseToken: string }> = [] failAcknowledge = false ownsLookup: ((token: string) => Promise) | null = null constructor(private readonly log: string[], private readonly result: NormalizedGooglePlayPurchase) {} verify( _userId: string, _productId: string, _purchaseToken: string, ownsExpiredPurchaseToken: (token: string) => Promise, ): Promise { this.verifyCalls += 1 this.ownsLookup = ownsExpiredPurchaseToken this.log.push('verify') return Promise.resolve(this.result) } acknowledge(productId: string, purchaseToken: string): Promise { this.log.push('acknowledge') if (this.failAcknowledge) { return Promise.reject(new GooglePlayVerificationError('google_play_acknowledgement_failed', 502)) } this.acknowledged.push({ productId, purchaseToken }) return Promise.resolve() } } class FakeStore implements GooglePlayPurchaseStore { applied: VerifiedGooglePlayPurchaseRecord[] = [] marked: Array<{ userId: string; purchaseToken: string }> = [] ownsQueries: Array<{ userId: string; purchaseToken: string }> = [] reject: GooglePlayPurchasePersistenceError | null = null result: StoredGooglePlayPurchase = { applied: true, purchase_id: 'p-1', acknowledged: false } constructor(private readonly log: string[]) {} ownsPurchaseToken(userId: string, purchaseToken: string): Promise { this.ownsQueries.push({ userId, purchaseToken }) return Promise.resolve(true) } applyVerified(record: VerifiedGooglePlayPurchaseRecord): Promise { this.log.push('apply') if (this.reject) return Promise.reject(this.reject) this.applied.push(record) return Promise.resolve(this.result) } markAcknowledged(userId: string, purchaseToken: string): Promise { this.log.push('mark') this.marked.push({ userId, purchaseToken }) return Promise.resolve() } } function setup(verified: NormalizedGooglePlayPurchase = purchase()) { const log: string[] = [] const playApi = new FakePlayApi(log, verified) const store = new FakeStore(log) return { log, playApi, store } } const input = { userId: USER_ID, productId: PRODUCT_ID, purchaseToken: TOKEN } Deno.test('purchase rejected by the database is never acknowledged with Google', async () => { for (const code of ['active_subscription_other_provider', 'purchase_owned_by_other_user'] as const) { const { log, playApi, store } = setup() store.reject = new GooglePlayPurchasePersistenceError(code) let caught: unknown = null try { await applyGooglePlayPurchase({ playApi, store }, input) } catch (error) { caught = error } assert(caught instanceof GooglePlayPurchasePersistenceError && caught.code === code, `${code} must propagate`) assert(playApi.acknowledged.length === 0, `${code}: rejected purchase must stay unacknowledged`) assert(store.marked.length === 0, `${code}: rejected purchase must not be marked acknowledged`) assert(log.join(',') === 'verify,apply', `${code}: unexpected call order ${log.join(',')}`) } }) Deno.test('accepted purchase is persisted unacknowledged, then acknowledged, then marked', async () => { const { log, playApi, store } = setup() const result = await applyGooglePlayPurchase({ playApi, store }, input) assert(log.join(',') === 'verify,apply,acknowledge,mark', `unexpected call order ${log.join(',')}`) assert(store.applied.length === 1, 'purchase must be persisted once') const persisted = store.applied[0] assert(persisted.userId === USER_ID && persisted.purchaseToken === TOKEN, 'persisted identity must match') assert(persisted.purchase.acknowledged === false, 'persistence must record acknowledged=false before Google ack') assert( persisted.purchase.verification.acknowledgementState === GOOGLE_PLAY_ACKNOWLEDGED_STATE, 'entitled receipt must be stored in its canonical post-acknowledgement form', ) assert( playApi.acknowledged.length === 1 && playApi.acknowledged[0].productId === PRODUCT_ID && playApi.acknowledged[0].purchaseToken === TOKEN, 'Google must be acknowledged for the verified product and token', ) assert(store.marked.length === 1 && store.marked[0].userId === USER_ID, 'acknowledgement must be recorded') assert(result.acknowledgedNow, 'result must report the acknowledgement') assert(result.purchase.acknowledged, 'returned purchase must be acknowledged') assert( result.stored !== null && result.stored.acknowledged === true && result.stored.purchase_id === 'p-1', 'returned stored row must reflect the acknowledgement', ) }) Deno.test('duplicate and stale persistence results are still acknowledged', async () => { const results: StoredGooglePlayPurchase[] = [ { applied: false, duplicate: true, purchase_id: 'p-1', acknowledged: false }, { applied: false, duplicate: false, reason: 'stale_provider_event', purchase_id: 'p-1', acknowledged: false }, ] for (const stored of results) { const { playApi, store } = setup() store.result = stored await applyGooglePlayPurchase({ playApi, store }, input) assert(playApi.acknowledged.length === 1, 'stored purchase must be acknowledged to avoid auto-refund') assert(store.marked.length === 1, 'acknowledgement must be recorded') } }) Deno.test('failed Google acknowledgement leaves the stored purchase unacknowledged and propagates', async () => { const { log, playApi, store } = setup() playApi.failAcknowledge = true let caught: unknown = null try { await applyGooglePlayPurchase({ playApi, store }, input) } catch (error) { caught = error } assert( caught instanceof GooglePlayVerificationError && caught.code === 'google_play_acknowledgement_failed' && caught.status === 502, 'acknowledgement failure must propagate as 502', ) assert(store.applied.length === 1 && store.applied[0].purchase.acknowledged === false, 'row stays acknowledged=false') assert(store.marked.length === 0, 'failed acknowledgement must not be recorded') assert(log.join(',') === 'verify,apply,acknowledge', `unexpected call order ${log.join(',')}`) }) Deno.test('already acknowledged purchase is persisted as acknowledged without another Google call', async () => { const { playApi, store } = setup(purchase({ acknowledged: true, verification: { subscriptionState: 'SUBSCRIPTION_STATE_ACTIVE', acknowledgementState: GOOGLE_PLAY_ACKNOWLEDGED_STATE, }, })) const result = await applyGooglePlayPurchase({ playApi, store }, input) assert(store.applied[0].purchase.acknowledged === true, 'acknowledged purchase must persist acknowledged=true') assert(playApi.acknowledged.length === 0 && store.marked.length === 0, 'no second acknowledgement') assert(!result.acknowledgedNow && result.purchase.acknowledged, 'result must stay acknowledged') }) Deno.test('non-entitled purchase is persisted verbatim and never acknowledged', async () => { const { playApi, store } = setup(purchase({ entitled: false, purchaseState: 'pending' })) const result = await applyGooglePlayPurchase({ playApi, store }, input) assert( store.applied[0].purchase.verification.acknowledgementState === PENDING_ACK, 'non-entitled receipt must keep Google acknowledgement state', ) assert(playApi.acknowledged.length === 0 && store.marked.length === 0, 'non-entitled purchase must not be acknowledged') assert(!result.purchase.acknowledged, 'result must stay unacknowledged') }) Deno.test('preverified purchase skips Google verification but keeps the persist-then-acknowledge order', async () => { const { log, playApi, store } = setup() await applyGooglePlayPurchase({ playApi, store }, { ...input, preverified: purchase() }) assert(playApi.verifyCalls === 0, 'preverified purchase must not be verified again') assert(log.join(',') === 'apply,acknowledge,mark', `unexpected call order ${log.join(',')}`) }) Deno.test('expired-token ownership lookup is scoped to the purchasing user', async () => { const { playApi, store } = setup() await applyGooglePlayPurchase({ playApi, store }, input) assert(playApi.ownsLookup !== null, 'verify must receive an ownership lookup') assert(await playApi.ownsLookup('expired-token-0001'), 'lookup must delegate to the store') assert( store.ownsQueries.length === 1 && store.ownsQueries[0].userId === USER_ID && store.ownsQueries[0].purchaseToken === 'expired-token-0001', 'lookup must be scoped to the purchasing user', ) }) Deno.test('database exception messages map to stable persistence codes', () => { assert( classifyGooglePlayPersistenceError('purchase_owned_by_other_user') === 'purchase_owned_by_other_user', 'owner conflict must map', ) assert( classifyGooglePlayPersistenceError('linked_purchase_owned_by_other_user') === 'purchase_owned_by_other_user', 'linked owner conflict keeps its historical mapping', ) assert( classifyGooglePlayPersistenceError('active_subscription_other_provider') === 'active_subscription_other_provider', 'provider conflict must map', ) assert( classifyGooglePlayPersistenceError('provider_event_payload_mismatch') === 'purchase_persistence_failed', 'other failures must map to the generic code', ) }) interface RecordedCall { kind: 'rpc' | 'update' name: string args: Record filters: Array<[string, string, unknown]> } function fakeSupabaseClient(calls: RecordedCall[], rpcError: { message: string } | null = null) { const client = { rpc(name: string, args: Record) { calls.push({ kind: 'rpc', name, args, filters: [] }) return Promise.resolve(rpcError ? { data: null, error: rpcError } : { data: { applied: true, purchase_id: 'p-1', acknowledged: false }, error: null }) }, from(table: string) { return { update(values: Record) { const call: RecordedCall = { kind: 'update', name: table, args: values, filters: [] } calls.push(call) const chain = { eq(column: string, value: unknown) { call.filters.push(['eq', column, value]) return chain }, is(column: string, value: unknown) { call.filters.push(['is', column, value]) return Promise.resolve({ error: null }) }, } return chain }, } }, } return client as unknown as Parameters[0] } Deno.test('Supabase store maps the RPC with hashed tokens and records acknowledgement by direct update', async () => { const calls: RecordedCall[] = [] const store = createSupabaseGooglePlayPurchaseStore(fakeSupabaseClient(calls)) const record = { userId: USER_ID, purchaseToken: TOKEN, purchase: purchase({ linkedPurchaseToken: 'linked-token-0001' }), } const stored = await store.applyVerified(record) await store.markAcknowledged(USER_ID, TOKEN) const tokenHash = await sha256Hex(TOKEN) const [rpc, update] = calls assert(rpc.kind === 'rpc' && rpc.name === 'apply_verified_google_play_purchase', 'must call the purchase RPC') assert(rpc.args.p_token_hash === tokenHash, 'token must be hashed') assert(rpc.args.p_linked_token_hash === await sha256Hex('linked-token-0001'), 'linked token must be hashed') assert(rpc.args.p_purchase_token === TOKEN && rpc.args.p_user_id === USER_ID, 'identity must be mapped') assert(rpc.args.p_acknowledged === false, 'acknowledged flag must be forwarded') assert(stored !== null && stored.purchase_id === 'p-1', 'RPC row must be returned') assert(calls.filter((call) => call.kind === 'rpc').length === 1, 'acknowledgement must not re-enter the RPC') assert(update.kind === 'update' && update.name === 'iap_purchases', 'acknowledgement must update iap_purchases') assert(typeof update.args.acknowledged_at === 'string', 'acknowledged_at must be set') assert( JSON.stringify(update.filters) === JSON.stringify([ ['eq', 'platform', 'google_play'], ['eq', 'user_id', USER_ID], ['eq', 'token_hash', tokenHash], ['is', 'acknowledged_at', null], ]), `unexpected update filters ${JSON.stringify(update.filters)}`, ) }) Deno.test('Supabase store turns RPC rejections into persistence errors', async () => { const store = createSupabaseGooglePlayPurchaseStore( fakeSupabaseClient([], { message: 'active_subscription_other_provider' }), ) let caught: unknown = null try { await store.applyVerified({ userId: USER_ID, purchaseToken: TOKEN, purchase: purchase() }) } catch (error) { caught = error } assert( caught instanceof GooglePlayPurchasePersistenceError && caught.code === 'active_subscription_other_provider', 'RPC rejection must surface as a typed persistence error', ) })