← back to Costa Rica
cycle 19 docs: YOLO_NOTES ledger + GO-LIVE migrate_011 prerequisite — double-charge race fix
d0912c81029c2423f1a49bb7d40fdbc6111e62d8 · 2026-09-24 01:18:21 -0700 · Steve
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TouFkmUGKHtwqpZwgReVic
Files touched
M YOLO_NOTES.mdM docs/GO-LIVE.md
Diff
commit d0912c81029c2423f1a49bb7d40fdbc6111e62d8
Author: Steve <steve@designerwallcoverings.com>
Date: Thu Sep 24 01:18:21 2026 -0700
cycle 19 docs: YOLO_NOTES ledger + GO-LIVE migrate_011 prerequisite — double-charge race fix
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TouFkmUGKHtwqpZwgReVic
---
YOLO_NOTES.md | 23 +++++++++++++++++++++++
docs/GO-LIVE.md | 2 ++
2 files changed, 25 insertions(+)
diff --git a/YOLO_NOTES.md b/YOLO_NOTES.md
index 995f120..007cb78 100644
--- a/YOLO_NOTES.md
+++ b/YOLO_NOTES.md
@@ -422,3 +422,26 @@ Canonical apply order now: `004_marketplace → 005_apple → 006_contacts → 0
**Cost:** $0.
**Backlog:** approval-enforcement decision on Steve's desk (memo unanswered 5 cycles running — still leaving it as a genuine business decision); provider-honored idempotency key + sig-encoding (live-only, CR-KYC blocked); processorFee schema prerequisite (documented); per-event webhook auto-reply try/catch. The provider-agnostic hardening surface (fetch-timeout, async-error, fail-closed, money-invariant, SQL-correctness, pool hygiene, test infrastructure) is now broadly saturated across 18 cycles. Next cycle: a fresh Cody-driven "what's the single most likely remaining bug" audit, or add more inline-route coverage now that server.js is testable, or the per-event webhook try/catch follow-up.
+
+## /yoloforever CYCLE 19 (2026-09-24, yf-costa) — COLD audit found a real double-charge race
+
+**Ran a fresh, open-ended Cody cold audit** ("find the single most likely remaining money/data/security/crash bug; say so plainly if there's nothing"). It found — and REPRODUCED at the DB layer — a genuine money-losing bug the targeted cycles missed.
+
+**BUG:** `POST /bookings/:code/pay` did a lock-free check-then-insert — `SELECT` "any in-flight payment for this booking?" → `INSERT` a `'processing'` row → `provider.createCharge()`. Two truly concurrent pay requests for one booking (double-tap "Pay" on flaky CR mobile data, or a client auto-retry racing a manual retry) BOTH pass the SELECT, BOTH insert, BOTH call the processor → **the traveler's card is charged TWICE for one booking**. No adapter sends a provider idempotency key, so the processor doesn't dedupe either. The payout leg had the mirror guard (`payouts_one_per_booking_rail`); the charge leg was missing it. (Everything else Cody checked — JWT/Apple auth, threads/messages/payout-method IDOR, webhook logic, admin gating, money fields — held up.)
+
+**Fix:**
+1. `migrate_011_payments_one_inflight.sql` — a PARTIAL unique index: at most ONE in-flight (`processing`/`requires_action`) payment per booking. Retries after a `'failed'` attempt still work (terminal states aren't in-flight).
+2. `routes/app.js` — the pre-charge INSERT is wrapped; on a `23505` from THIS index (checked via `e.constraint`) the race-loser REUSES the winner's payment instead of firing a second charge. Postgres serializes it (the loser blocks on the winner's row, then 23505s against the committed winner).
+
+**Cody gate (red-teamed its own find) — logic correct (traced PG locking), 3 deploy/edge fixes applied:**
+- **CONCURRENTLY:** the migration was plain `CREATE UNIQUE INDEX` → a SHARE lock on the HOTTEST write table (every checkout). Changed to `CREATE UNIQUE INDEX CONCURRENTLY` — the exact lesson migrate_010 already codified, applied to the wrong table. (apply-migrations.sh runs 011 outside a txn, so CONCURRENTLY is safe.)
+- **Pre-flight dupe check:** prod may ALREADY have ≥2 in-flight rows per booking (from this very bug), which would fail the index build. Added the `GROUP BY HAVING count>1` check + remediation to the migration runbook + `docs/GO-LIVE.md §3`.
+- **The resolved-winner 500 (real, was untested):** if the winner's charge resolves fast (sandbox/live instant succeed) before the loser's catch runs, an in-flight-ONLY re-SELECT misses it → the loser 500s though its sibling's payment SUCCEEDED. Fixed: the catch re-SELECTs the booking's most-recent payment (NO status filter) and returns its real status → the loser always gets a payment_id to poll, never a 500.
+
+**Tests (+3, suite 190 → 193):** DB-level index rejects a 2nd in-flight insert (23505) + allows retry-after-failed; route reuses an in-flight winner (0 createCharge); route handles a RESOLVED winner without a 500.
+
+**Cost:** $0 (local + two Cody passes — the cold audit found the bug + reproduced it, then a second pass red-teamed the fix and caught the CONCURRENTLY + resolved-winner-500 gaps).
+
+**Prod-apply gated** (go-live migration pass; runbook has the pre-check + CONCURRENTLY). Dev-applied (reversible).
+
+**Backlog / NEW ticket (pre-existing, Cody-flagged):** nothing (webhook/poll aside) moves a truly-STUCK `'processing'` payment off that status — a dropped webhook + an abandoned app session = a permanently-unpayable booking. Needs a reconciler/TTL. The in-flight SELECT gate already had this exposure; the index hardens it, doesn't introduce it. Plus: approval-enforcement decision on Steve's desk; provider-honored idempotency key + sig-encoding (live-only); processorFee schema prerequisite; per-event webhook auto-reply try/catch. The cold audit is proof the loop still surfaces real bugs past cycle 18 — next cycle: the stuck-payment reconciler, or another cold audit.
diff --git a/docs/GO-LIVE.md b/docs/GO-LIVE.md
index 5e00d2d..5b675c9 100644
--- a/docs/GO-LIVE.md
+++ b/docs/GO-LIVE.md
@@ -41,6 +41,8 @@ psql "$DATABASE_URL" -c "SELECT conname FROM pg_constraint WHERE conname IN ('bo
```
NEVER `--baseline` on prod (marks pending files applied WITHOUT running → would skip the double-book/integrity guards); the runner refuses it unless `FORCE=1`. For partial adoption use `--baseline-through <file>`.
+**`migrate_011_payments_one_inflight.sql` prerequisite (double-charge backstop):** builds a partial UNIQUE index on the HOT `payments` table. BEFORE applying, run the pre-flight duplicate check (in the migration header) — if any booking already has ≥2 in-flight payments the build fails; remediate (keep newest in-flight, mark the rest `failed`, verify against the processor which actually charged). The file uses `CREATE UNIQUE INDEX CONCURRENTLY` so it won't block checkout writes; an interrupted build leaves an INVALID index → `DROP INDEX CONCURRENTLY IF EXISTS payments_one_inflight_per_booking;` and re-run.
+
**`migrate_010_search_trgm.sql` prerequisite:** it runs `CREATE EXTENSION pg_trgm`, which needs **SUPERUSER / rds_superuser**. If the app's DB role isn't a superuser on prod, run the extension line once as the superuser BEFORE the migration pass: `psql "$SUPERUSER_URL" -c 'CREATE EXTENSION IF NOT EXISTS pg_trgm;'`. The three `CREATE INDEX CONCURRENTLY` lines then build without blocking writes to `places`; if a build aborts it leaves an INVALID index → `DROP INDEX CONCURRENTLY IF EXISTS idx_places_<col>_trgm;` and re-run.
## 4. Deploy code to prod — SURGICAL only (do NOT use canonical /deploy)
← 08a9915 costa-rica: fix double-charge race on /pay — DB unique index
·
back to Costa Rica
·
costa-rica: stale-payment reconciler (dropped-webhook rescue b86e7b6 →