HoopSpark IAP — final review checklist
Final pre-launch review requested by Leong on 2026-09-29. 7 independent reviewers, every serious finding re-checked in the code by Tora Master. Code fixes merged and deployed 2026-09-30 (v2.827); the app part reaches phones with NOVA's next TestFlight build. Updated 2026-09-30 10:48 (UTC+8). Click a task for details.
57 tasksDone: 54Partly done: 1To do: 0Needs decision: 0Waiting: 2Not fixing: 0
Critical bugs 3 of 3 done
F4 HighSecond refund reversal gives back double Done
- Problem
- Refund → reversal → refund → reversal returned both clawbacks (buyer ends with 1,000 for a 500 pack).
- Why it matters
- Free Sparks for anyone Apple cycles twice.
- Files / components
backend/internal/domain/iap/repository.go RefundReversed- Fix
- Give back refund rows minus reversal rows already given back.
- Dependencies
- —
- Acceptance
- Balance is 500 after the four steps.
- Tests
- refund_cycle_db_test.go TestAppleSecondReversalGivesBackOnlyTheLastClawback (knifed)
- Status
- Done — b55add8fc (branch)
F5 HighSecond refund after a reversal fails forever Done
- Problem
- The second refund re-matched a partly used spend; the iap_refund_matches primary key rejected it, the whole refund rolled back, Apple kept getting 500.
- Why it matters
- The clawback never lands.
- Files / components
repository.go reverseCreatorShares- Fix
- ON CONFLICT (transaction_id, spend_id) DO UPDATE SET matched = matched + excluded.
- Dependencies
- —
- Acceptance
- Second refund succeeds; creator reversal capped at the original share.
- Tests
- refund_cycle_db_test.go TestSecondRefundAfterReversalMatchesSameSpendAgain (knifed)
- Status
- Done — b55add8fc (branch)
F9 MediumRefund-first then reversed: buyer paid, got nothing Done
- Problem
- A placeholder from a refund that arrived before redeem was marked credited with no owner; the later redeem got "already credited, 0".
- Why it matters
- A paying customer gets no Sparks.
- Files / components
repository.go RefundReversed- Fix
- Ownerless rows: delete the placeholder; put an unclaimed purchase back to unclaimed.
- Dependencies
- —
- Acceptance
- Redeem after refund+reversal credits 500; unclaimed stays on the admin list.
- Tests
- TestAppleRefundFirstThenReversedThenRedeemCredits, TestUnclaimedRefundReversedGoesBackToUnclaimed (knifed)
- Status
- Done — b55add8fc (branch)
Security issues 10 of 10 done
F6 LowPurchase-to-account binding set by the app Done
- Problem
- appAccountToken / obfuscatedAccountId are chosen by the app. A modified app can credit another user, then refund; the refund takes the same amount back from that user.
- Why it matters
- Corrected 2026-09-30: the victim ends where they started. Harm is confusion, tips reversed if they spent the surprise Sparks, and appearing on the refund-risk list. Nuisance, not theft.
- Files / components
iap/binding.go; app/lib/services/iap_service.dart- Fix
- Server issues a signed per-purchase token; the server trusts only that; fall back to the caller otherwise.
- Dependencies
- Leong decision A; NOVA app build
- Acceptance
- A forged account token no longer moves Sparks to another account.
- Tests
- New redeem tests with forged / valid tokens.
- Status
- Done — Leong 00:57 go. One-time purchase code: server a009b98ae + migration 000483, app 153aa020b, plan docs (branch). Knives: 3 server + 2 app red. Needs NOVA build after merge
F13 MediumAnyone can flood the refused-purchases list Done
- Problem
- Refused rows are keyed by a transaction ID read from an unverified body; 30 requests a minute per account.
- Why it matters
- Real "paid but not credited" rows get pushed off the 200-row admin list.
- Files / components
iap/rejections.go- Fix
- At most 20 rows per user; existing rows still count attempts.
- Dependencies
- —
- Acceptance
- 25 fake IDs leave 20 rows; an existing row's attempts still increase.
- Tests
- final_review_db_test.go TestRejectionsCappedPerUser (knifed)
- Status
- Done — 662b626cf (branch)
F32 LowGoogle purchase token in network-error logs Done
- Problem
- Timeout/DNS errors carry the full URL, including the purchase token.
- Why it matters
- Tokens in logs.
- Files / components
iap/google.go- Fix
- Redact the token from the error text.
- Dependencies
- —
- Acceptance
- No full token in error logs.
- Tests
- No dedicated test (one-line change).
- Status
- Done — 662b626cf (branch)
F33 LowApple notification endpoint parses junk Done
- Problem
- Unauthenticated endpoint accepted 1 MB bodies and parsed any number of certificates before failing.
- Why it matters
- Cheap CPU exhaustion.
- Files / components
iap/handler.go, verify.go- Fix
- 64 KB body cap; exactly 3 certificates or reject before parsing.
- Dependencies
- —
- Acceptance
- Wrong certificate counts rejected as bad JWS.
- Tests
- final_review_db_test.go TestJWSWithWrongCertificateCountRejectedBeforeParsing (knifed)
- Status
- Done — 662b626cf (branch)
F34 LowClient-reported Google price/currency unbounded Done
- Problem
- Display-only values came straight from the phone.
- Why it matters
- Garbage in the Spark history line.
- Files / components
iap/google_checker.go- Fix
- Currency clipped to 3 upper-case letters; out-of-range price recorded as 0.
- Dependencies
- —
- Acceptance
- Bounded values stored.
- Tests
- No dedicated test.
- Status
- Done — 662b626cf (branch)
F43 LowWelcome-gift farming with email aliases Done
- Problem
- Fingerprints lower-case the email only; a+1@gmail.com counts as new.
- Why it matters
- Extra welcome gifts (limited by the invite gate).
- Files / components
wallet/grant_claims.go- Fix
- Normalise plus-aliases (and Gmail dots) before fingerprinting.
- Dependencies
- Leong: welcome-gift task
- Acceptance
- Aliases share one fingerprint.
- Tests
- Unit test on the normaliser.
- Status
- Done — Live 2026-09-30 v2.830. Startup backfill checked on the live DB: 23 of 23 past gifts now have the alias fingerprint
F43.1 LowEmail normaliser Done
- Problem
- Aliases of one inbox look like different people.
- Why it matters
- Farming extra welcome gifts.
- Files / components
wallet/grant_claims.go- Fix
- One function: lower-case; drop +tag; Gmail/Googlemail also drops dots and treats googlemail.com as gmail.com.
- Dependencies
- —
- Acceptance
- a+1@gmail.com, a.b@gmail.com and A@googlemail.com all normalise to a@gmail.com / ab@gmail.com as expected.
- Tests
- Table-driven unit test.
- Status
- Done — 7a9c8c6ac (branch tora/iap-last4). Knives: without the +tag step or the Gmail dot step, the unit test goes red
F43.2 LowFingerprint the normalised email too Done
- Problem
- Only the raw lower-cased email is fingerprinted.
- Why it matters
- The normaliser alone changes nothing.
- Files / components
wallet/grant_claims.go- Fix
- Add a second email fingerprint from the normalised address, next to the old one, so existing claims keep matching.
- Dependencies
- F43.1
- Acceptance
- A new account with an alias of a claimed inbox gets 0 welcome Sparks.
- Tests
- DB test: claim with a@gmail.com, then a+1@gmail.com and a.@gmail.com get nothing; unrelated email still gets the gift.
- Status
- Done — 7a9c8c6ac (branch). Knife: without the new fingerprint, both alias tests go red
F43.3 LowBackfill the new fingerprint for past gifts Done
- Problem
- Accounts that already got the gift have only the old fingerprint.
- Why it matters
- Aliases of existing users would still slip through.
- Files / components
wallet/grant_claims.go (BackfillGrantClaims)- Fix
- The existing startup backfill also writes the normalised fingerprint (idempotent).
- Dependencies
- F43.2
- Acceptance
- After a restart every granted account has both fingerprints.
- Tests
- DB test on the backfill; live count checked after deploy.
- Status
- Done — 7a9c8c6ac (branch). No new code: the existing startup backfill now writes the new fingerprint too; test proves it
F42.2 LowGuard the signed date Done
- Problem
- Trusting the signed date lets a receipt claim any date.
- Why it matters
- A receipt dated in the future or outside the certificate's life must fail.
- Files / components
iap/verify.go- Fix
- Reject a signed date more than 5 minutes in the future, and any date outside the chain's validity.
- Dependencies
- F42.1
- Acceptance
- Future-dated or out-of-range receipts are refused.
- Tests
- Unit tests for both cases.
- Status
- Done — 75b3d33b4 + test fix (branch). Knife on the future-date guard first stayed green (another check caught it); test changed so only this guard can catch it → red
Apple configuration 1 of 2 done
F1 CriticalKey files unreadable by the server Done
- Problem
- The setup guide had Jeff copy the Apple and Google key files as root and chmod 600. The backend runs as user 10001, so it cannot read them.
- Why it matters
- Android purchases and Apple refund answers would silently stay off on launch day.
- Files / components
docs/iap-setup-en.md A7.2 / B7.2 (and Chinese); backend/Dockerfile; deploy/docker-compose.prod.yml- Fix
- Add chown 10001:10001 and a READABLE check command to both steps.
- Dependencies
- —
- Acceptance
- The guide's check prints READABLE; the admin page shows no red banner for the key.
- Tests
- Doc change. Verified on the live server that the backend runs as uid 10001 and apns.p8 is world-readable.
- Status
- Done — Guides fixed (f9b4433df, a81b7e20f), live
F2 HighApp Review purchases refused (sandbox) Partly done
- Problem
- Apple's reviewers buy in the sandbox. The server only accepts sandbox purchases from HOOP accounts in IAP_TEST_UIDS.
- Why it matters
- The reviewer's purchase fails and the version is rejected under guideline 2.1.
- Files / components
backend/internal/domain/iap/verify.go, handler.go; docs/iap-setup-en.md A8.1- Fix
- Guide step A8.1: review demo account's HOOP ID goes into IAP_TEST_UIDS before submitting; note in App Review Information.
- Dependencies
- Jeff: confirm or create the review account
- Acceptance
- Review account listed in IAP_TEST_UIDS; its sandbox purchase credits Sparks.
- Tests
- Part D test with that account before submission.
- Status
- Partly done — Guide step A8.1 live; waiting for Jeff's review account
Google configuration 1 of 1 done
F3 High"Backwards compatible" purchase option is required Done
- Problem
- The Android plugin (in_app_purchase_android 0.5.1) only reads the backwards-compatible purchase option. The guide said "tick it if offered".
- Why it matters
- Without it the app finds no product and shows "store unavailable".
- Files / components
docs/iap-setup-en.md B3 / B8; ~/.pub-cache/.../google_play_product_details.dart:31-35- Fix
- Make it mandatory in B3, B8 and the final checklist; stop and send a screenshot if the box is missing.
- Dependencies
- —
- Acceptance
- Every product's buy option is Active and Backwards compatible.
- Tests
- D2 step 2 shows the packs.
- Status
- Done — Guides fixed (f9b4433df), live
Backend issues 14 of 14 done
F10 MediumMissed Apple refund notifications are lost Done
- Problem
- Google has a daily refund sweep; Apple had none. A refunded JWS shown again was only answered "revoked".
- Why it matters
- A missed refund leaves Sparks in the wallet for good.
- Files / components
iap/receipt.go; (new) Apple notification-history sweep- Fix
- Done now: a verified revoked JWS claws back immediately. Remaining: daily App Store Server API sweep (needs the Apple key).
- Dependencies
- Leong decision C; Jeff's Apple In-App Purchase key
- Acceptance
- Refund applied even when the notification was missed.
- Tests
- final_review_db_test.go TestAppleRevokedJWSClawsBackWhenRefundNotificationWasMissed (knifed)
- Status
- Done — Revoked-JWS clawback 662b626cf + daily Apple sweep 2d57519cd (branch; runs once the Apple key is set)
F11 MediumHalf-configured Google notifications are silent Done
- Problem
- Android on but IAP_GOOGLE_NOTIFY_SA unset → every notification 401, no admin warning.
- Why it matters
- Refunds only by the daily sweep; slow payments auto-refunded after 3 days.
- Files / components
server/router.go- Fix
- Red admin banner google_notify + startup error.
- Dependencies
- —
- Acceptance
- Banner shows when the email is unset.
- Tests
- Startup wiring only; no unit test.
- Status
- Done — 662b626cf (branch)
F12 MediumMulti-quantity purchase via notification → 500 forever Done
- Problem
- The notification path returned 500 for a refused multi-quantity purchase; Pub/Sub retried for 7 days; no record.
- Why it matters
- Log noise, retries, no trace for support.
- Files / components
iap/google_notify.go- Fix
- Treat as permanent: record the refusal, answer 200.
- Dependencies
- —
- Acceptance
- 200 and a refusal row.
- Tests
- final_review_db_test.go TestGoogleNotifyMultiQuantityIsPermanent (knifed)
- Status
- Done — 662b626cf (branch)
F24 LowMissing Play Console permissions only in logs Done
- Problem
- A key without Play permissions makes every call 403; no admin banner.
- Why it matters
- "Most missed step" is invisible.
- Files / components
iap/google.go, router.go- Fix
- Startup probe (voided list, 1 result); 401/403 → setup banner.
- Dependencies
- —
- Acceptance
- Banner when permissions are missing.
- Tests
- Fake Google 403 test.
- Status
- Done — 3d992b4af (branch): 401/403 from Google → red setup line on the admin risk page; clears on next success
F29 LowGoogle refund sweep could fail on the 30-day boundary Done
- Problem
- First run asked for exactly 30 days back; any 400 was logged as "token not found".
- Why it matters
- Daily refund sweep silently failing.
- Files / components
iap/google.go, google_voided.go- Fix
- 29-day first window; 400 means "not found" only for token lookups.
- Dependencies
- —
- Acceptance
- Sweep succeeds on first run.
- Tests
- Existing sweep tests pass.
- Status
- Done — 662b626cf (branch)
F30 LowNotification-credited Android purchases lack a price line Done
- Problem
- When the notification credits first, the later redeem doesn't fill price/currency.
- Why it matters
- History line without price.
- Files / components
iap/repository.go- Fix
- Fill price/currency on an already-credited Google row with price 0.
- Dependencies
- —
- Acceptance
- Price shown.
- Tests
- Redeem-after-notify test.
- Status
- Done — 7f205ecc3 (branch): price filled once when the app redeem arrives after the notification
F31 LowNo clock leeway on Google push tokens Done
- Problem
- A slightly slow server clock rejects valid pushes.
- Why it matters
- Redelivery storms.
- Files / components
iap/google_notify.go- Fix
- 60 s leeway.
- Dependencies
- —
- Acceptance
- Valid pushes accepted.
- Tests
- No dedicated test.
- Status
- Done — 662b626cf (branch)
F36 LowCONSUMPTION_REQUEST before redeem never answered Done
- Problem
- Marked answered with nothing sent if it arrives before the purchase is credited.
- Why it matters
- Apple decides the refund on the buyer's word.
- Files / components
iap/consumption.go- Fix
- Keep it pending within the 12-hour window.
- Dependencies
- Apple key
- Acceptance
- Answered once credited.
- Tests
- Consumption test.
- Status
- Done — a2d5ffa42 (branch): waits until credited, then answered
F37 LowUnclaimed Google expiry counted from order time Done
- Problem
- Slow payments were marked refunded before Google's 3 days from payment.
- Why it matters
- Admin report shows a refund that hasn't happened.
- Files / components
iap/google_voided.go- Fix
- Count from created_at.
- Dependencies
- —
- Acceptance
- Slow payment stays unclaimed.
- Tests
- TestExpireUnclaimedGoogleAfterThreeDays slow-payment row (knifed)
- Status
- Done — 662b626cf (branch)
F41 LowGoogle "not found" treated as permanent Done
- Problem
- A transient 404 right after purchase ends the purchase client-side and 200s the notification.
- Why it matters
- Purchase left unacknowledged → Google auto-refund (customer not charged, but lost).
- Files / components
iap/google.go, google_notify.go; app iap_service.dart- Fix
- Retry notifications a few times; let the app retry refused Android purchases on resume.
- Dependencies
- —
- Acceptance
- Transient 404 recovers.
- Tests
- Fake Google 404-then-200 test.
- Status
- Done — Live 2026-09-30 v2.830 (backend)
F41.1 LowRedeem: grace window for Google 'not found' Done
- Problem
- Google 404 on redeem is refused for good at once.
- Why it matters
- A purchase Google hasn't finished recording is refused (Google then refunds it).
- Files / components
iap/receipt.go, rejections.go- Fix
- First 15 minutes after the first try: answer 'not confirmed yet' so the app retries; after that refuse as today. Grace-window rows don't show on the admin refused list.
- Dependencies
- —
- Acceptance
- 404-then-200 within the window gets credited; 404 for 15+ minutes is refused.
- Tests
- DB test with a fake Google that answers 404, then 200.
- Status
- Done — 20db78e56 (branch tora/iap-last4). Knife: grace removed → first 404 refused, test red
F41.2 LowNotifications: retry on Google 'not found' Done
- Problem
- A purchased notification whose token Google can't find yet is dropped.
- Why it matters
- That purchase then depends on the app coming back.
- Files / components
iap/google_notify.go- Fix
- Answer the notification with an error so Pub/Sub resends it later, instead of dropping it.
- Dependencies
- —
- Acceptance
- Notification for a 404 token is retried, credited once Google knows it.
- Tests
- DB test: notify 404 → 5xx; then 200 → credited.
- Status
- Done — 20db78e56 (branch). Knife: grace removed → notification dropped (200), test red
F42 LowApple leaf certificate checked at redeem time Done
- Problem
- A transaction left unfinished for a very long time could fail once the leaf expires.
- Why it matters
- Very rare lost credit.
- Files / components
iap/verify.go- Fix
- Check validity at the signed date, or accept expired leaf with a valid chain at signing time.
- Dependencies
- —
- Acceptance
- Old JWS still accepted.
- Tests
- Verify unit test.
- Status
- Done — Live 2026-09-30 v2.830 (backend)
F42.1 LowMatch Apple's own library on certificate dates Done
- Problem
- We check Apple's certificate at today's date.
- Why it matters
- Very old unfinished purchases could be refused after Apple's certificate expires.
- Files / components
iap/verify.go- Fix
- Apple's official library (no online revocation checks, same as us) checks the chain at the receipt's signed date. Do the same.
- Dependencies
- —
- Acceptance
- A receipt signed while the certificate was valid is accepted after it expires.
- Tests
- Unit test with a fake chain whose leaf has expired.
- Status
- Done — 75b3d33b4 (branch tora/iap-last4). Matches Apple app-store-server-library (checked in its source). Knife: chain back to today → red
Customer experience 8 of 8 done
F41.3 LowApp: 'not confirmed yet' keeps the purchase open Done
- Problem
- The app must not finish a purchase on the new answer.
- Why it matters
- Finishing would lose the retry.
- Files / components
app iap_service.dart- Fix
- Check the new answer maps to 'unconfirmed' (retry, don't finish); change only if it doesn't.
- Dependencies
- F41.1
- Acceptance
- App retries and never finishes on the grace answer.
- Tests
- Unit test on the answer mapping.
- Status
- Done — 20db78e56 (branch). No app code change needed; test pins not_found_yet = keep open and retry. Knife: mapping it to refused → red
F16 Medium"You're offline" shown when online Done
- Problem
- Server 5xx and store errors count as offline.
- Why it matters
- Confusing; support tickets.
- Files / components
app/lib/services/iap_service.dart, util/http_retry.dart- Fix
- Offline only for real connectivity errors; otherwise "store unavailable".
- Dependencies
- —
- Acceptance
- Server down shows unavailable, not offline.
- Tests
- Widget test.
- Status
- Done — 83d7a7efe (branch)
F17 MediumTop-up reachable on the web version Done
- Problem
- Web has no store; the page says offline forever; bindAccount throws on every web launch.
- Why it matters
- Broken page on web.
- Files / components
app/lib/screens/account_screen.dart and other TopUpScreen.open callers; main.dart- Fix
- Hide on web / say "top up in the mobile app"; skip IAP init on web.
- Dependencies
- —
- Acceptance
- No top-up entry on web; no startup exception.
- Tests
- Widget test with kIsWeb override.
- Status
- Done — 83d7a7efe (branch)
F18 MediumRestore gives no feedback Done
- Problem
- No spinner, no result, errors only logged; our own copy tells users to tap it.
- Why it matters
- Users think it's broken.
- Files / components
topup_screen.dart, iap_service.dart- Fix
- Busy state + snackbar with the result.
- Dependencies
- —
- Acceptance
- Restore shows checking / nothing to restore / couldn't reach store.
- Tests
- Widget test.
- Status
- Done — 83d7a7efe (branch)
F25 LowiOS finish can hang and freeze the screen Done
- Problem
- The plugin's finish never completes for some transactions; the grid stays locked.
- Why it matters
- Stuck top-up screen until restart.
- Files / components
iap_service.dart- Fix
- Timeout on complete(); clear in-flight before awaiting finish.
- Dependencies
- —
- Acceptance
- Screen unlocks.
- Tests
- Service test with a hanging fake.
- Status
- Done — 83d7a7efe (branch)
F26 Low"Awaiting payment" stuck after a declined request Done
- Problem
- No store event for a declined Ask-to-Buy / lapsed pending payment.
- Why it matters
- Wrong advice ("don't buy again").
- Files / components
iap_service.dart, en/zh copy- Fix
- Clear on resume/restore when the store no longer reports it; soften copy.
- Dependencies
- —
- Acceptance
- Tier unlocks after decline.
- Tests
- Service test.
- Status
- Done — 83d7a7efe (branch)
F27 LowAndroid refused purchase blocks its tier silently Done
- Problem
- itemAlreadyOwned → restore → silently skipped.
- Why it matters
- Tap does nothing for 3 days.
- Files / components
iap_service.dart- Fix
- Show the refused banner again; track account_gone too.
- Dependencies
- —
- Acceptance
- Banner shown on repeat tap.
- Tests
- Service test.
- Status
- Done — 83d7a7efe (branch)
F28 LowAndroid in-flight lock stuck on an empty event Done
- Problem
- Empty productID event never clears inFlight.
- Why it matters
- All packs disabled until restart.
- Files / components
iap_service.dart- Fix
- Treat empty productID as the current in-flight; clear stale in-flight on resume.
- Dependencies
- —
- Acceptance
- Packs re-enabled.
- Tests
- Service test.
- Status
- Done — 83d7a7efe (branch)
Admin functionality 7 of 7 done
F7 HighNo manual Spark adjustment tool Done
- Problem
- Support cannot credit a customer who paid but was refused, or undo a wrong clawback.
- Why it matters
- Every such ticket needs raw SQL on production.
- Files / components
(new) POST /v1/admin/wallet/adjust; admin screen- Fix
- Admin-only endpoint: delta + reason, ledger kind admin, audit row, idempotency key, upper limit.
- Dependencies
- Leong decision B
- Acceptance
- Adjustment appears in the customer's history and the audit log.
- Tests
- Endpoint + authz tests.
- Status
- Done — Server eb013e9a1 + app e35f492fa / 64c43e71e (branch); migration 000480
F8 HighNo customer / transaction lookup Done
- Problem
- No way to look up purchases by user, transaction or order ID.
- Why it matters
- Support can't answer tickets.
- Files / components
(new) GET /v1/admin/iap/lookup; admin screen- Fix
- Return transactions, refusals and recent ledger lines.
- Dependencies
- Leong decision B
- Acceptance
- Lookup by any of the three IDs.
- Tests
- Endpoint + authz tests.
- Status
- Done — Server eb013e9a1 + app e35f492fa / 64c43e71e (branch)
F14 MediumIAP risk screen incomplete Done
- Problem
- 5 rows per list, no IDs to copy, refund_reversed list not drawn, "no risk" shown when only reversals exist.
- Why it matters
- Support can't act on the list.
- Files / components
app/lib/widgets/admin_iap_risk.dart- Fix
- Paging, copyable IDs, a refund-reversed block, fixed empty check.
- Dependencies
- —
- Acceptance
- All server lists visible and actionable.
- Tests
- Golden + widget tests.
- Status
- Done — 5f83b5915 (branch): tap a row opens the customer page, expand past 5, refunds-Apple-cancelled block, empty check fixed
F15 MediumWelcome-gift audit can't be read Done
- Problem
- Audit rows are written but never shown.
- Why it matters
- No history of who changed the gift.
- Files / components
iap/admin_wallet.go; admin_welcome_grant.dart- Fix
- Return and show the last N audit rows.
- Dependencies
- —
- Acceptance
- Changes listed with who and when.
- Tests
- Endpoint test + golden.
- Status
- Done — Live 2026-09-30 v2.830 (backend + web); phones with NOVA's next TF
F15.1 MediumServer: return the welcome-gift change history Done
- Problem
- Changes are written to an audit table but never returned.
- Why it matters
- Nobody can see who changed the gift.
- Files / components
iap/admin_wallet.go- Fix
- The welcome-gift settings answer carries the last 20 changes: who, from, to, when.
- Dependencies
- —
- Acceptance
- Two changes → both listed newest first with the admin's name.
- Tests
- DB test on the endpoint.
- Status
- Done — b4c0566a4 (branch tora/iap-last4). Knife: history left out → red
F15.2 MediumApp: show the history under the welcome gift Done
- Problem
- The admin card shows only the current value.
- Why it matters
- Same.
- Files / components
app admin_welcome_grant.dart- Fix
- A short 'Changes' list under the welcome-gift card (who, from → to, date); hidden when empty.
- Dependencies
- F15.1
- Acceptance
- History visible on the admin dashboard.
- Tests
- Widget test + golden, looked at.
- Status
- Done — 887cf3310 (branch). One line + See all dialog; golden looked at (first version cut off See all, reworked). Knife: line hidden → red. Dashboard tests match master
F35 LowPayout approval without an ID returned a blank 200 Done
- Problem
- Empty ID returned nothing; clients read it as success.
- Why it matters
- False "done".
- Files / components
developer/developer.go- Fix
- 400 bad_request.
- Dependencies
- —
- Acceptance
- 400 on empty ID.
- Tests
- payout_empty_id_db_test.go (knifed against the original code)
- Status
- Done — 662b626cf (branch)
Database/data 2 of 2 done
F38 LowBalance check constraint never validated Done
- Problem
- wallets_green_nonneg is NOT VALID.
- Why it matters
- A legacy negative balance would make that user's top-ups fail.
- Files / components
migrations 000475- Fix
- Count negatives on production; if 0, new migration VALIDATE CONSTRAINT.
- Dependencies
- —
- Acceptance
- Constraint valid.
- Tests
- Migration test.
- Status
- Done — Migration 000481 (branch); production has 0 negative balances
F39 Low000475 down migration drops financial tables Done
- Problem
- Unlike 000472/000474 it doesn't refuse.
- Why it matters
- A rollback could delete refusal/grant/refund-match history.
- Files / components
migrations/000475_*.down.sql- Fix
- Refuse when those tables have rows.
- Dependencies
- —
- Acceptance
- Down refuses with data.
- Tests
- Migration test.
- Status
- Done — 52bf32d2f (branch); guard verified with a test row
Documentation 6 of 6 done
F19 MediumApp Privacy / Data safety steps missing Done
- Problem
- Neither setup guide mentioned them.
- Why it matters
- Store rejection.
- Files / components
docs/iap-setup-en.md A9.1 / B7.5- Fix
- Added both steps and checklist rows.
- Dependencies
- —
- Acceptance
- Both declared.
- Tests
- —
- Status
- Done — f9b4433df, live
F20 MediumiPhone test expected five packs Done
- Problem
- TestFlight shows all six complete products.
- Why it matters
- A correct setup looked like a failure.
- Files / components
docs/iap-setup-en.md D1 step 2- Fix
- Expect six in TestFlight.
- Dependencies
- —
- Acceptance
- —
- Tests
- —
- Status
- Done — f9b4433df, live
F21 MediumTester added after buying: purchase never returns Done
- Problem
- Refused test purchases are finished.
- Why it matters
- Tester waits forever.
- Files / components
Troubleshooting table- Fix
- "Add the tester, then buy again."
- Dependencies
- —
- Acceptance
- —
- Tests
- —
- Status
- Done — f9b4433df, live
F22 MediumTerms don't cover Google Play or post-refund clawback Done
- Problem
- Terms say "iOS … Apple" only (dated 2026-08-05); nothing about taking Sparks back after a store refund.
- Why it matters
- Legal text doesn't match behaviour.
- Files / components
deploy/www/terms.html, terms-zh.html; privacy third-party list- Fix
- Draft EN/ZH lines; add Google Play to the privacy third-party list.
- Dependencies
- Leong decision D (legal wording)
- Acceptance
- Approved and published.
- Tests
- —
- Status
- Done — Leong 01:24 'merge the (D) also'. Terms + privacy (EN + ZH) live on hoopcomm.com, byte-checked against git
F23 LowPlan still showed privacy (B9) as waiting Done
- Problem
- Published 2026-09-29 but unticked.
- Why it matters
- Stale plan.
- Files / components
docs/iap-en.md, iap.md- Fix
- Ticked.
- Dependencies
- —
- Acceptance
- —
- Tests
- —
- Status
- Done — f9b4433df, live
F40 LowGuide gaps: restart, Google Cloud snags, step names Done
- Problem
- Restart doesn't re-read .env; org policy blocks key creation; permissions take up to ~24 h; env template step numbers stale.
- Why it matters
- Jeff guessing.
- Files / components
docs/iap-setup*.md, deploy/.env.example- Fix
- Fixed.
- Dependencies
- —
- Acceptance
- —
- Tests
- —
- Status
- Done — f9b4433df, live
Testing 1 of 2 done
T1 HighOn-device end-to-end tests (Part D) Waiting
- Problem
- Store purchases can't be tested without the consoles and real devices.
- Why it matters
- Only proof the full chain works.
- Files / components
docs/iap-setup-en.md Part D- Fix
- Run D1 and D2 after Jeff's setup and the merge.
- Dependencies
- Jeff's console setup; NOVA build; merge
- Acceptance
- D1 and D2 steps 1–8 pass.
- Tests
- Manual, recorded in the room.
- Status
- Waiting — After Jeff's setup, NOVA build and merge
T2 LowFixes without a dedicated test Done
- Problem
- F11 (startup wiring), F31, F32, F34 have no test of their own.
- Why it matters
- Regressions would go unnoticed.
- Files / components
iap tests- Fix
- Add small unit tests.
- Dependencies
- —
- Acceptance
- Each has a test that fails without the fix.
- Tests
- —
- Status
- Done — 64f443a48 (branch): F11, F31, F32, F34 each have a test that goes red with the fix removed
Production readiness 1 of 2 done
P1 HighMerge and deploy tora/iap-final Done
- Problem
- All fixes are on the branch only.
- Why it matters
- Nothing reaches users until merged.
- Files / components
branch tora/iap-final- Fix
- Merge, deploy backend, web version; NOVA build for the app parts.
- Dependencies
- Leong's go
- Acceptance
- Live server on the new build; checks pass.
- Tests
- Full backend + app test runs before merge.
- Status
- Done — Merged 09-30 01:3x (Leong 01:23 go), master 2739871f3 = v2.827. Live DB at 483 (479–483 applied, balance rule validated), backend health 200, new routes answer 401 without login, web v2.827 byte-checked. App: NOVA's next TestFlight build
P2 HighServer settings for keys and testers Waiting
- Problem
- IAP_TEST_UIDS, Apple key, Google key and notification SA are unset on production.
- Why it matters
- Test purchases refused; Android off; refund answers off.
- Files / components
/root/hoop/deploy/.env- Fix
- Tora sets each as Jeff reports (guide Part C).
- Dependencies
- Jeff's console setup
- Acceptance
- All C1 rows ✅; no admin banner.
- Tests
- Admin page check.
- Status
- Waiting — Needs Jeff's reports
Sources: the setup guide /iap-setup-en,
the plan /iap-en, the earlier 62-item review /iap-checklist.
Edit tools/iap_final_review_page.py, not this page.