HoopSpark IAP — review checklist
Asked for by Leong Sen Fong, 2026-09-29 08:01 MYT, to track the IAP review back item by item. Companion to the full review: hoop-docs.pages.dev/iap-review (evidence with file and line for every finding in docs/iap_review_findings.md). Branch reviewed: origin/liva/iap @ 880f05626. Nothing has been fixed yet; this page is updated in the same commit as each fix.

Part A — every §9 item, as the review found it

“Branch says” is the tick on the branch's own plan. “Review verdict” is what reading the code against the spec found: an item can be ticked done and still be wrong.

✅ Correct: 8⚠️ Built, with problems: 29☐ Not done: 3👤 A person's task: 16

Phase 0 — consoles (Jeff)

§9 itemWhat it asksBranch saysReview verdictFindingsNote
P0.1Check whether com.hooptech.hoop.green.* exist in App Store Connect—👤 A person's taskS1-1Not done yet (Leong 09-29). Decides the product ids.
P0.2Paid Apps Agreement in effect—👤 A person's task
P0.3Apply to the Small Business Program (15%)—👤 A person's taskGates L2 only.
P0.4Play developer + merchant accounts; 15% fee—👤 A person's task
P0.5Malaysia storefront; read the proceeds—👤 A person's task
P0.6Pick Apple price points; create / re-price products—👤 A person's taskS1-2Entering 4.90 will turn the tier guard test red (S1-2).

Phase 1 — pricing

§9 itemWhat it asksBranch saysReview verdictFindingsNote
B1products.go in MYR, no price to clients, guard tests, IAP_GREEN_ENERGY.md updated✅⚠️ Built, with problemsS1-1, S1-2, S1-4, S1-5Prices / amounts / badges correct; ids chosen before P0.1; guard will break at P0.6; doc only partly updated.

Phase 2 — database & backend

§9 itemWhat it asksBranch saysReview verdictFindingsNote
B2Migration A (columns, Google index, delete FK)✅⚠️ Built, with problemsS2-1, S2-2, S2-7, S2-8SQL correct, but the number (466) can't deploy.
B3Apple verify stores price, currency, account token✅✅ CorrectS3-10 (low)Price milliunits → micros correct.
B4Receipt-verifier interface before Google✅✅ CorrectS3-8 (low)Extracted before G2, as required.
B5Refund() for deleted accounts✅⚠️ Built, with problemsS5-1 / S2-3Path never runs in production: users are only soft-deleted.
B6GET /v1/wallet/ledger paging + price details✅✅ CorrectS9-4, S9-9 (low)Paging correct; needs an index.
W1Welcome grant from wallet_settings; GrantGreen removed; guard test✅⚠️ Built, with problemsS4-5, S4-1, S4-2, S2-9Guard test misses grouped const blocks.
W2GET / PUT admin wallet settings + audit✅✅ CorrectS4-6, S4-11 (low)No upper bound on the grant.
B7Test purchases only for listed testers (IAP_TEST_UIDS)✅⚠️ Built, with problemsS3-3 / S6-3Checks the caller, not the credited account.
B8Migration C: spend_id, releasable_at, backfill✅⚠️ Built, with problemsS2-1, S2-5Backfill correct; number (468) can't deploy.
B9Privacy policy: Apple refund consent + 7-year retention🟡👤 A person's taskS12-6, S5-7Drafted; waiting for Leong's OK. Purge job it promises not built.

Phase 3a — Google

§9 itemWhat it asksBranch saysReview verdictFindingsNote
G1Service-account credentials on the server🟡⚠️ Built, with problemsS12-3, S6-5, S6-13Code ready; setup guide puts the key where the server can't read it.
G2purchases.products.get and state handling✅⚠️ Built, with problemsS1-3 / S6-1, S6-4Product id from the phone trusted; unknown states credited.
G3POST /v1/iap/google/verify + account binding✅⚠️ Built, with problemsS3-3 / S6-3Binding correct; tester check on wrong account.
G4RTDN notify + Pub/Sub OIDC → Refund on voided✅⚠️ Built, with problemsS3-2 / S5-3 / S7-5, S7-3Auth sound; void-before-credit dropped; partial refunds.
G5Daily Voided Purchases poll✅⚠️ Built, with problemsS7-1, S7-472-hour window, no cursor, first page only.
G6Fake-server tests copying real auth / errors✅⚠️ Built, with problemsS6-6Auth copied; most error codes not.
G7Backend acknowledges every credited Google purchase, with retry✅⚠️ Built, with problemsS6-2 / S7-2, S12-4Retry in memory only; Play permission in guide too narrow.
G8Credit from the 'purchased' notification✅⚠️ Built, with problemsS6-2 / S7-2Correct, but depends on G7's acknowledge.

Phase 3b — refunds & payouts

§9 itemWhat it asksBranch saysReview verdictFindingsNote
R1Refund reverses creators' shares (§11.1 rule)✅⚠️ Built, with problemsS5-4, S5-10Both worked examples exact; spends with no share re-matched.
R2Payout hold: withdrawable balance only✅⚠️ Built, with problemsS8-1, S11-5, S11-6SQL exact; settle race (also on master); screen headline wrong.
R3CONSUMPTION_REQUEST reply within 12 h✅⚠️ Built, with problemsS5-5, S5-6, S5-7 / S12-5Old V1 API; no retry; runs before B9 is approved.
R4Refund-risk list✅✅ CorrectS8-10 (low)
R5Bad-debt list✅⚠️ Built, with problemsS8-4, S8-5, S2-3Wrong rows / double counts; totals over 200 rows only.
R6Android refund path ships with Android billing—⚠️ Built, with problemsS7-1, S7-3, S7-5Present (G4 + G5) with the issues above.
F1Payout list free_funded_share + reason_required✅⚠️ Built, with problemsS8-2, S8-3, S8-6Wrong window; deleted payer counts 0%; any note passes.

Phase 4 — app

§9 itemWhat it asksBranch saysReview verdictFindingsNote
C1buy() passes the HOOP user id✅✅ Correct
C2Android: consume only after 200; iOS unchanged✅⚠️ Built, with problemsS10-1, S10-12Correct for new purchases; Android never redelivers at start-up.
C3Event handling per the §2.3 table; ⑧ retries✅⚠️ Built, with problemsS10-3, S10-7⑥ broken; permanent rejections loop forever.
C4pending vs awaitingPayment✅⚠️ Built, with problemsS10-4, S10-9Android launch failure can lock the screen.
C5States ⑥ ⑧ ⑪ ⑫ ⑬ wording en + zh; footer✅⚠️ Built, with problemsS11-8, S11-9Wording correct; ⑥ label; zh untested.
C6Balance loads on open; History link✅⚠️ Built, with problemsS11-1 / S10-6, S11-13Stale / previous-account balance.
C7Route to apple/verify or google/verify✅✅ CorrectBodies match the backend exactly.
C8Goldens: six / five cards; states✅⚠️ Built, with problemsS11-7, S11-10No ⑥ or footer golden.
W3Welcome-grant control on the admin dashboard✅⚠️ Built, with problemsS11-2, S11-14, S11-15, S11-3Hard-coded MYR fails a guard test; golden has a missing glyph.
C9Money-risk lists, free-funded share, creator hold line✅⚠️ Built, with problemsS11-5, S11-4, S11-19Headline shows total, not withdrawable.
Android buildRelease build on the internal track; Play products☐☐ Not doneNot done.

Phase 5 — Spark history

§9 itemWhat it asksBranch saysReview verdictFindingsNote
H1spark_history_screen.dart✅⚠️ Built, with problemsS9-1, S9-2, S9-3, S9-6No thousands separators; '0 Refund' shown green; no cache.
H2'History ›' entry on the balance card✅✅ CorrectSingle entry point (D12).

Phase 6–7 — testing & launch

§9 itemWhat it asksBranch saysReview verdictFindingsNote
Phase 6Run all of §8 on both platforms☐☐ Not doneAfter the fixes.
L1Malaysia only on both consoles☐👤 A person's task
L2…34000 only once 15% is in effect☐👤 A person's taskFive-card grid already supported.
L3IAP_TEST_UIDS set; IAP_ALLOW_SANDBOX removed☐👤 A person's taskS3-11Code no longer reads the old switch.
L4One real notification received on both endpoints☐👤 A person's taskS12-4
L5One real-money purchase end to end☐👤 A person's taskOnly after all of Phase 3b is live.
L6Price re-check scheduled (owner Leong)☐👤 A person's task
L7Invite daily_cap set☐👤 A person's taskMaster 619e06b6c: every new account now uses a daily place, so the cap does bound grants.
L8Products submitted with a new app version☐👤 A person's task
L9Privacy labels / Data safety declare purchases☐👤 A person's task
L10Billing Library version meets Play's minimum☐☐ Not donePlugin bundles Billing 8.0.0 — compare with Play's current minimum.

Found by the review, outside the §9 list

IDWhat
S3-1Apple certificate-type check (App Store OIDs)
S5-2Failed Apple refund still answers 200
S8-1Payout settle race — also on master today
S10-1Android: re-check unfinished purchases at start-up
S10-2Start the purchase listener right after login
S10-6 / S11-1Reset purchase state on logout
S5-9REFUND_REVERSED policy
S4-4Delete + re-sign-up earns a new welcome grant
Q4Permanent rejections: record server-side, finish on iOS, don't consume on Android (Leong asked for the best solution, 09-29)

Part B — every finding, with its fix status

Fixed: 42 of 62 lines Low findings are grouped per component, one line each. Status is “open” until the fix is committed; the commit is written next to it.

FindingSeverityAreaWhatStatus
☑S2-1 / S12-1CriticalMigrationsThe three IAP migrations are numbered 000466–000468, but production is already at 000471. The branch also lacks master's 000469–000471.fixed · e8d6aef3f (tora/iap-fixes): migrations renumbered 000472–000474, master merged in
☑S3-1HighApple verificationReceipts and notifications are accepted if their certificate chain reaches Apple Root CA G3 with any key usage. The App Store certificate-type OIDs (leaf 1.2.840.113635.100.6.11.1, intermediate …6.2.1) are never checked. Apple's own verification library checks them.fixed · e5ef792a8 (tora/iap-fixes): App Store certificate marks required; forged purchase + refund notice tests
☑S3-2 / S5-3 / S7-5HighRefunds (both stores)A refund / void for a purchase we have not credited yet is dropped with a 200 and nothing stored. If the purchase is credited afterwards (old signed receipt, or a credit racing the refund), it is never clawed back.fixed · 41066eb8c (tora/iap-fixes): refunded placeholder; later credit → 400 revoked
☑S5-2HighApple refundsWhen Refund() fails (database error, deploy restart), the Apple notification handler still answers 200.fixed · 41066eb8c (tora/iap-fixes): failed Apple clawback answers 500 so Apple retries
☑S1-3 / S6-1HighGoogle validationThe Spark amount for an Android purchase is looked up from the product id the phone sends. Google's returned productId is never compared with it; the fake Google server ignores the product too.fixed · fc175b249 (tora/iap-fixes): Google productId must match
☑S6-2 / S7-2HighGoogle acknowledge (G7/G8)The backend's acknowledge to Google runs in an in-memory goroutine (now, 5 s, 30 s, 2 min) and nothing about it is stored.fixed · 79ce554ae (tora/iap-fixes): acked_at stored; retried from the DB every 15 min for 3 days
☑S7-1HighGoogle voided backstop (G5)The daily voided-purchases poll always looks back 72 hours, keeps no record of the last successful run, and reads only the first page.fixed · 79ce554ae (tora/iap-fixes): voided poll resumes from its saved cursor (≤30 days)
☑S8-1HighPayouts — also live on master todaySettling a payout reads the pending row without a lock and updates it without checking it is still pending.fixed · 403386609 on master, live 2026-09-29 08:27: row lock + status check on settle
☑S10-1HighApp (Android)The app never asks Google for unfinished purchases at start-up; only the manual Restore button does.fixed · a03e57ab1 (tora/iap-fixes): unfinished purchases asked for after login and on resume (Android + iOS)
☑S2-3 / S5-1HighDeleted accounts (B5, R5, D4)HOOP never deletes a users row (deletion blanks the fields), so 'user_id set to NULL' on deletion never happens. The B5 test hard-deletes a user, which production never does.fixed · 4d9a52445 (tora/iap-fixes): deleted = soft-deleted; refund takes normal clawback, loss flagged account_deleted; test models production
☑S5-5HighApple consumption reply (R3)R3 calls Apple's V1 Send Consumption Information endpoint with V1 fields.fixed · 52b5453c2 (tora/iap-fixes): Apple V2 consumption API and fields
☑S5-7 / S12-5HighPrivacy (R3 vs B9)R3 always tells Apple customerConsented = true, and switches on as soon as the App Store key is installed. The setup guide already asks Jeff to create that key; the privacy-policy line (B9) is only drafted.fixed · 52b5453c2 (tora/iap-fixes): IAP_REFUND_CONSENT_LIVE switch, off until B9 is published
☐S12-3HighSetup guideThe guide tells Jeff to copy the Apple and Google key files to /root/hoop/deploy/, but the backend container only mounts deploy/secrets as /secrets.open
☐S12-4HighSetup guide (Play permissions)The guide says to grant the service account only 'View financial data' because 'the app consumes purchases itself'. Since 09-28 the backend acknowledges purchases (G7/G8), which (to be confirmed on Google's permission table) needs 'Manage orders'.open
☐S1-1 / S12-8HighProduct IDs (B1 / P0.1)B1 kept com.hooptech.hoop.green.* as a 'P0.1 default'. The spec has no default: the ID set depends on what Jeff finds in App Store Connect (P0.1), and §1.2.1 lists spark.*.open
☑S10-6 / S11-1HighApp (both)The purchase service keeps its per-account state (balance, banners, retries) across logout and account switch, and the top-up screen keeps the first balance it saw (??=).fixed · a03e57ab1 (tora/iap-fixes): per-account state reset on account change; balance read applies unless a credit landed meanwhile
☑S11-2HighApp translationsSix admin strings hard-code 'MYR' (en + zh), which the existing translation-currency guard test forbids.fixed · f66babefb (tora/iap-fixes): currency out of the admin strings; translation_currency_test green
☑S2-2 / S12-2HighRegistry & progress notes§9's progress note, WIP.md and LEDGER all say the branch 'waits for 464/465 to reach production' — those numbers were voided and will never exist. The branch's LEDGER pointer (000469) also conflicts with master's (000472).fixed · LEDGER c400f5c1b; WIP 4883a75b4; §9 progress note rewritten 5a3f2b017 (master) + d1c88da45 (branch)
☑S3-3 / S6-3MediumTester list (B7)The test-purchase tester check looks at the logged-in caller, not the account that receives the Sparks (the Google notification path checks the owner instead).fixed · fc175b249 (tora/iap-fixes): credited account must be a tester too
☑S6-4MediumGoogle validationOnly purchaseState 1 and 2 are refused; any other or missing value is credited.fixed · fc175b249 (tora/iap-fixes): only purchaseState 0 credits
☑S6-5MediumGoogle configIf the credentials file is set but can't be loaded, the server still starts with Android purchases off (one log line).fixed · 33cd705a4 backend + 6e9c1376b admin banner (tora/iap-fixes): unreadable store credentials shown in red on the money-risk page
☑S7-3MediumGoogle refundsPartial (quantity) refunds are not handled: the void notification claws back the whole purchase; the poll never sees partial refunds.fixed · f11d15623 (tora/iap-fixes): Google multi-quantity refused (no ack → Google refunds; recorded); keep it off in Play Console
☑S5-4MediumRefund attribution (R1)Spends with no creator share (AI assistant, self-purchase, shares rounded to 0) are matched again by every later refund.fixed · 41b246a23 (tora/iap-fixes): each refund's spend matches stored (iap_refund_matches); a spend is matched once in total
☑S5-6MediumApple consumption reply (R3)The reply is sent once, in a goroutine, with no retry or persistence (Apple does not re-ask after we answered 200).fixed · 52b5453c2 (tora/iap-fixes): requests stored and retried every 10 min within 12 h
☑S5-8MediumApple notificationsEnvironment is never checked; the bundle only if present; inner and outer payloads are not compared.fixed · 55f029940 (tora/iap-fixes): notification bundle required on both layers; inner/outer environment must match
☑S5-9MediumApple notificationsREFUND_REVERSED is not handled.fixed · eb60076f7 + 0dd94ab36 (tora/iap-fixes): Leong 09-29 — buyer's clawed Sparks given back automatically; creator shares listed for a person
☑S8-2MediumPayout review (F1)The free-funded share is computed over earnings since the previous payout up to now, not the earnings inside this payout.fixed · 7bbe9c5a3 (tora/iap-fixes): share covers only the earnings inside this payout (by release date); stored at settle
☑S8-3MediumPayout review (F1)A payer who deletes their account counts as 0% free (their wallet ledger cascades away).fixed · 7bbe9c5a3 (tora/iap-fixes): payer with shares but no spend history counts as 100% free
☑S8-4MediumBad-debt list (R5)Deleted-refund rows show the full amount even when the refund lost nothing; short refunds are double-counted; unclaimed-then-refunded rows count as loss.fixed · 4d9a52445 (tora/iap-fixes): loss list uses unattributed only, no duplicate list, refunded-unclaimed not loss, totals over all rows
☑S8-6MediumPayout review (F1)Any non-empty note (e.g. a bank reference) satisfies reason_required; approver and share at approval are not stored.fixed · 7bbe9c5a3 (tora/iap-fixes): separate reason (note = bank ref no longer passes); approver + share stored
☑S4-4MediumWelcome grantDeleting an account and signing up again with the same email / phone creates a new user and a new welcome grant.fixed · 35182316b (tora/iap-fixes): Leong 09-29 once per email/phone forever — HMAC fingerprints in welcome_grant_claims; needs IDENTITY_FINGERPRINT_KEY
☑S4-5MediumWelcome grant guard (W1)The 'no Go file defines the grant amount' guard misses grouped const/var blocks (including ledger.go's own), locals and SQL; it only runs with a database.fixed · ad113ada1 (tora/iap-fixes): guard rewritten on go/ast, DB-free, proves itself on bad samples
☑S10-2MediumAppThe purchase listener starts only at a cold start while logged in, or when the top-up screen opens — not right after login.fixed · a03e57ab1 (tora/iap-fixes): service bound to the account; starts right after login
☑S10-3 / S11-12MediumAppThe ⑥ 'Try again' state is broken: iOS StoreKit 2 never emits an error event (failures are only logged), and on Android the error event has an empty product id, so Try again does nothing.fixed · a03e57ab1 (tora/iap-fixes): ⑥ from buy() errors and the in-flight tier; already-owned / duplicate → restore
☑S10-4MediumApp (Android)If Google's buy flow fails to launch, the false return is ignored and the screen stays locked (all cards and Restore disabled) until restart.fixed · a03e57ab1 (tora/iap-fixes): buy flow not launched → ⑥, screen unlocked
☑S10-5MediumApp (iOS)Restore uses current entitlements (which exclude consumables, to confirm on a device), and restored events are credited but never finished.fixed · a03e57ab1 (tora/iap-fixes): iOS restore reads unfinished transactions; restored ones are finished
☑S10-7 / S3-7MediumApp ↔ backendPermanent rejections (bad_signature, wrong_env, unknown_product) are treated as ⑧ and never finished.fixed · 44bedb57c server record + a03e57ab1 app (tora/iap-fixes): 'couldn't add' banner; iOS finishes, Android left for Google's refund; not resent
☑S11-5MediumCreator payout screen (C9)The big 'Available' figure is the total balance including Sparks still on hold; the withdrawable amount is only in a small line.fixed · f39a062d9 (tora/iap-fixes): wallet headline shows the withdrawable amount
☑S11-4MediumApp errorsTwo backend messages (on_hold, reason_required) are Chinese and shown verbatim to English users; the app doesn't pre-check amount ≤ withdrawable.fixed · f39a062d9 (tora/iap-fixes): on_hold / reason_required translated; amount checked before submit
☑S11-7 / 9 / 10MediumGoldens (C8)No golden for ⑥, the footer, the Android ⑬ text, or any Chinese screen.fixed · 457279a52 (tora/iap-fixes): goldens for ⑥, footer, Android ⑬, the rejected banner and a Chinese screen
☑S11-14MediumGoldens (W3)The welcome-grant golden has a missing-glyph box (Lucide font not loaded in that test).fixed · f66babefb (tora/iap-fixes): Lucide loaded in the welcome-grant golden; regenerated
☑S9-1 / S9-2MediumSpark historyAmounts have no thousands separators; a refund that recovered nothing shows as a green '0 Refund'.fixed · 7d5305e69 (tora/iap-fixes): thousands separators; zero refund row neutral
☑S9-3 / S11-13MediumResilienceHistory and balance have no cache, no backoff retry, no pull-to-refresh; offline is shown as 'store unavailable'.fixed · a0e4c3ef0 (tora/iap-fixes): history + balance cached, backoff retry, pull-to-refresh, offline message
☑S9-4MediumSpark history APINo (user_id, id) index for the paging query.fixed · 55f029940 (tora/iap-fixes): wallet_ledger (user_id, id DESC) index in 000475; plan test
☑S2-4MediumRetentionwallet_ledger / wallets / song_tips still cascade on user delete (harmless only while users are never hard-deleted).fixed · 5ef3d0958 (tora/iap-fixes): guard test — no code or migration may DELETE FROM users
☐S12-6MediumPrivacy (B9)The drafted wording promises a 7-year purge job that doesn't exist; merging would put unapproved wording on master (deploy/www is served live).open
☐S12-7MediumMergeMerge conflicts in en.json, zh.json and admin_dashboard_screen.dart, where master changed _loadInviteStats to take a range.open
☐S1-2 / S1-8MediumPricing guardThe tier guard measures badges against the entry pack's own price, so entering Apple's expected MYR 4.90 will turn it red.open
☐S1-4MediumDocsIAP_GREEN_ENERGY.md §一 still has the old USD rate, 'don't restrict to Malaysia' (contradicts D11) and 're-anchor not now'.open
☐S3-3b / S4-9MediumBindingA just-deleted caller (within the 15-min access token) can still be credited; the liveness check runs outside the credit transaction.open
☐S1-5, S1-6, S1-7Low—Pricing: payload guard doesn't call the handler; Apple Type/ownership not checked; admin entry-pack price computed from the anchor.open
☐S2-5 … S2-12Low—Migrations: run a read-only match count before the spend_id backfill; recent earnings go on hold at migration time; no CHECK constraints; spec §6.2 omits two columns; welcome_grant row deletable / no cap; down migrations delete financial rows; brief locks; ownerless line on rejecting a NULL-dev payout.open
☐S3-5, S3-8 … S3-12Low—Apple: whitelist Type/Environment; caller passed implicitly; no rate limit; price per unit vs quantity; stale IAP_ALLOW_SANDBOX docs; §5.2 error-body shape.open
☐S4-1, S4-2, S4-6 … S4-12Low—Crediting: IAP-first wallet never gets the grant; grant not atomic on pool paths; no welcome-grant ceiling; any 23505 = already; 'already' ignores stored owner/status; no CHECK green ≥ 0; granted_this_month error shows 0; stale docs. S4-3 (daily_cap doesn't bound grants) is resolved by master commit 619e06b6c (every new account now uses a daily place).open
☐S5-10 … S5-13Low—Refunds: rounding can hide loss; NULL-dev reversals; matching anchored on purchased_at; consumption status uses the whole wallet.open
☐S6-6 … S6-14Low—Google: fake server error coverage; 401 doesn't clear token cache; ack races consume (false alarms); error mapping loops; order id from client; regionCode/promo; tokens in logs; env vars undocumented; Android unclaimed rows.in progress · S6-8 (ack false alarms) fixed 79ce554ae; the rest open
☐S7-4, S7-6 … S7-12Low—Google RTDN: poll single page; cert cache can be wiped; iat / iss / email_verified untested; ack deadline; sweeper per instance; double ack; logging; exact Play permissions.in progress · S7-4 paging fixed 79ce554ae; the rest open
☐S8-5, S8-7 … S8-12Low—Payouts: totals over first 200 rows; index-defeating casts; share error shows unflagged; negative withdrawable; R4 flags vanish on deletion; ambiguities about ad revenue.open
☐S9-5 … S9-12Low—History: refunded top-up not marked; dates without year; Load-more guard; unknown kinds only logged locally; loose params; raw refs; footer 'August 2026' date; 'Spark' inside Chinese strings.open
☐S10-8 … S10-15Low—App lifecycle: per-transaction guard; inFlight cleared by other events; single banner; Android price 0 on early redelivery; Android may acknowledge uncredited; retry-key collision; buy() gaps; wrong ⑫ text for deleted account.open
☐S11-3, S11-6, S11-8, S11-15 … S11-23Low—Screens: duplicated Spark→MYR rate; hold line wording; '⑥ Retry' vs 'Try again'; W3 audit view / cap / confirm; W3/C9 hidden when dashboard fails; risk-row details and plural; share error shows 0%; payout flow tested only via helpers; formatting; ⑬ URL not tappable; stale header comment; fixture prices.open
☐S12-9 … S12-14Low—Launch: wrong commit citations; env vars missing from .env.example; doc drift (IAP_GREEN_ENERGY §九 'no Android', the assistant's knowledge base says 'Apple only'); withdrawable field name; CHANGELOG/version at merge.open