-- +goose Up -- Audit C5/C6/C10/C13: bind each in-app-purchase transaction to exactly one -- account so a valid receipt cannot be replayed against a second account to -- grant Pro for free. -- -- apple_original_transaction_id is a dedicated, indexed column — it replaces -- the LIKE '%...%' scan over apple_receipt_data that the Apple webhook used -- to find users (C13). google_purchase_token already exists; we just add the -- uniqueness guarantee. ALTER TABLE subscription_usersubscription ADD COLUMN IF NOT EXISTS apple_original_transaction_id text; -- Partial unique indexes: one account per transaction. NULL/empty rows are -- excluded so accounts without an IAP are unaffected. CREATE UNIQUE INDEX IF NOT EXISTS uq_subscription_apple_original_txn ON subscription_usersubscription (apple_original_transaction_id) WHERE apple_original_transaction_id IS NOT NULL AND apple_original_transaction_id <> ''; -- Pre-flight dedup for the Google index below. apple_original_transaction_id -- is brand-new (added above), so it is all-NULL and cannot collide. But -- google_purchase_token is a pre-existing column, and the C6 replay bug being -- fixed here is exactly "the same token bound to multiple accounts" — so -- duplicate rows may exist and would make the UNIQUE index below fail to -- build, aborting the migrate Job. Keep the earliest subscription row for -- each token and clear the token on the rest; those rows lose a binding that -- was disputed anyway, while the original (earliest) owner keeps it. UPDATE subscription_usersubscription s SET google_purchase_token = NULL WHERE google_purchase_token IS NOT NULL AND google_purchase_token <> '' AND id <> ( SELECT MIN(s2.id) FROM subscription_usersubscription s2 WHERE s2.google_purchase_token = s.google_purchase_token ); CREATE UNIQUE INDEX IF NOT EXISTS uq_subscription_google_purchase_token ON subscription_usersubscription (google_purchase_token) WHERE google_purchase_token IS NOT NULL AND google_purchase_token <> ''; -- +goose Down DROP INDEX IF EXISTS uq_subscription_google_purchase_token; DROP INDEX IF EXISTS uq_subscription_apple_original_txn; ALTER TABLE subscription_usersubscription DROP COLUMN IF EXISTS apple_original_transaction_id;