← back to Costa Rica
cycle 7 docs: YOLO_NOTES ledger + GO-LIVE runbook — §5b #2 createCharge fail-closed landed (both halves)
f4f8e107ae0118fa8c08181b8e0e94f3dc18f7c9 · 2026-09-23 19:00:45 -0700 · Steve
Moves §5b #2 from live-only to landed (transport 4xx/5xx AND synchronous
200-body decline both fail closed via shared mapStatus()). Adds an honest
residual-risk note to the live-only preflight: mapStatus defaults an
UNRECOGNIZED status string to 'processing', not 'failed' — a real decline
string neither map's author anticipated would not fail closed, so the $1
live verification must watch for any processing row that never resolves.
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 f4f8e107ae0118fa8c08181b8e0e94f3dc18f7c9
Author: Steve <steve@designerwallcoverings.com>
Date: Wed Sep 23 19:00:45 2026 -0700
cycle 7 docs: YOLO_NOTES ledger + GO-LIVE runbook — §5b #2 createCharge fail-closed landed (both halves)
Moves §5b #2 from live-only to landed (transport 4xx/5xx AND synchronous
200-body decline both fail closed via shared mapStatus()). Adds an honest
residual-risk note to the live-only preflight: mapStatus defaults an
UNRECOGNIZED status string to 'processing', not 'failed' — a real decline
string neither map's author anticipated would not fail closed, so the $1
live verification must watch for any processing row that never resolves.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TouFkmUGKHtwqpZwgReVic
---
YOLO_NOTES.md | 27 +++++++++++++++++++++++++++
docs/GO-LIVE.md | 5 +++--
2 files changed, 30 insertions(+), 2 deletions(-)
diff --git a/YOLO_NOTES.md b/YOLO_NOTES.md
index 4327bac..21dc560 100644
--- a/YOLO_NOTES.md
+++ b/YOLO_NOTES.md
@@ -188,3 +188,30 @@ Canonical apply order now: `004_marketplace → 005_apple → 006_contacts → 0
**Cost:** $0 (local PG/tests + one Cody pass).
**Backlog:** Provider-honored idempotency key (double-payout-on-retry guard) stays the ONLY remaining money-path pre-flight, and it's LIVE-ONLY (needs the Tilopay/ONVO account — CR-KYC blocked). `res.clone()` proxy hygiene = latent. Otherwise safe local work is exhausted; remaining is customer-facing/spend/live-credential gated.
+
+## /yoloforever CYCLE 7 (2026-09-23, yf-costa) — PRE-FLIGHT §5b #2 + a 2nd Cody-found gap
+
+**Decision:** Close GO-LIVE PRE-FLIGHT §5b #2 (a live `createCharge` HTTP error status must fail closed, not strand a booking as `providerRef:undefined, status:'processing'`).
+
+**Round 1 (transport-level, `!res.ok`):**
+- `lib/payments/tilopay.js` + `onvo.js` `createCharge()` — return `status:'failed'` before extracting `paymentId`/`id` on a live 4xx/5xx.
+- `routes/app.js` `POST /bookings/:code/pay` — new branch: `if (charge.status === 'failed')` records the row `'failed'` + returns 402, **instead of** the existing line that flattens every non-`'succeeded'` charge status into `'processing'`. Without this consumer fix, the adapter's `'failed'` would have been silently swallowed at the DB layer — the ticket would not actually have closed.
+
+**Cody gate — FIX FIRST verdict, a 2nd real gap found (not a rubber stamp):** the transport guard only caught an HTTP-status-coded decline. A **synchronous decline via HTTP 200 + a body-level status** (Stripe-like intent creation, which onvo docs itself as; tilopay's own `getCharge` already maps `'declined'→'failed'`) fell through the **identical stuck-booking bug**, just via the body instead of the status line — and both adapters' own status-map (built + unit-tested for `getCharge`) already knew how to recognize it; `createCharge` simply never consulted it. Cody proved this wasn't speculative by pointing at the adapters' own `mapStatus`/map definitions and their existing `getCharge` unit tests.
+
+**Round 2 (body-level guard, Cody-directed):**
+- Hoisted each adapter's status map to a shared, exported `mapStatus()` used by BOTH `createCharge` and `getCharge` — a decline is now recognized identically regardless of which call surfaces it.
+- `providerRef` is **kept** on a body-level decline (flows through the normal return, not the null-ref transport-error branch) — a later webhook/reconciliation may need it (Cody probe #5).
+- The transport-error path's swallowed body-read now captures `e.message` into `raw` instead of silently substituting `{}` (Cody probe #4 — diagnostics only; status is already hardcoded `'failed'` above the catch, so this cannot fabricate a success, unlike the refund/payout swallow from cycle 6).
+- Cody cleared probes #1 (`bad()`/UPDATE targeting) and #2 (retry/reuse interaction with PRE-FLIGHT #6) as real-but-verified-clean.
+
+**Tests (+9 across 2 rounds, suite 126 → 135, all green):**
+- `createcharge-error-status.test.js` (NEW, 8): 4xx fail-closed + 200-body synchronous decline fail-closed (both providers, `providerRef` kept) + happy path unchanged.
+- `pay-failed-charge.test.js` (NEW, 2): the route records `'failed'` + 402 on a failed charge; a succeeded charge unaffected.
+- `payments.test.js`: `tilopay.mapStatus` parity test (mirrors the existing `onvo.mapStatus` regression test).
+
+**FINAL DTD:** SHIP. §5b #2 provider-agnostic half now COMPLETE (both the transport AND body-level decline paths, both providers, consumer route wired).
+
+**Cost:** $0 (local PG/tests + two Cody passes — the 2nd caught a real gap the 1st round would have shipped with).
+
+**Backlog:** Provider-honored idempotency key remains the only money-path item, live-only (CR-KYC blocked). Safe local money-path work now genuinely exhausted after 4 cycles (#4/#5/#6/#7/§5b#2) closing every provider-agnostic reconcilability/fail-closed gap Cody could find. Next cycle should pivot to a non-money reversible item.
diff --git a/docs/GO-LIVE.md b/docs/GO-LIVE.md
index f06ab91..28fc821 100644
--- a/docs/GO-LIVE.md
+++ b/docs/GO-LIVE.md
@@ -55,11 +55,12 @@ These closed in the /yoloforever cycles; they need no creds and are already live
- **#4/#5 fetch timeout** — every live provider call is bounded by `fetchT` (`lib/payments/http.js`, env `PROVIDER_HTTP_TIMEOUT_MS`, default 15s) so a hung TCP can't strand a payment/payout in `processing` forever.
- **#7 body-read timeout** — the deadline spans the **whole** request (connect + headers AND the `res.json()` body read), so a header-fast/body-stalled provider can't hang the money path. `refund()`/`payout()` **fail closed** on a body-read timeout (payout throws → `payouts.js` marks the row `failed`; refund → `failed`) instead of fabricating a success.
- **#6 charge idempotency (local half)** — `POST /bookings/:code/pay` writes a `processing` payments row **before** `createCharge()` and reuses an in-flight payment on retry (no double-charge); a timeout leaves a reconcilable row.
+- **§5b #2 createCharge error status (both halves)** — a live 4xx/5xx **and** a synchronous 200-body decline (Stripe-like intent creation; tilopay's own status vocabulary) both fail closed to `status:'failed'` via each adapter's shared `mapStatus()` (also used by `getCharge`), instead of falling through to `'processing'` with no `providerRef`. The route honors `charge.status==='failed'` — records the row `failed` + returns 402 — rather than flattening every non-succeeded status into `processing`.
## 5b. Live-only preflight (verify against the REAL provider before opening bookings — do NOT guess)
1. **Webhook signature encoding** — adapters compare hex; if the provider signs base64, set `Buffer.from(sig,'base64')` explicitly once the real delivery is observable.
-2. **Live createCharge error path** — guard `res.ok` + explicit `failed` mapping before extracting `providerRef` (a bad live 4xx/5xx body can otherwise write `providerRef=undefined,status=processing` → stuck booking). `createCharge` reads the body unwrapped, so a body **timeout** already throws → 502; this item is specifically the **non-timeout** error-status body.
-3. **Provider-honored idempotency key (#6 live half)** — pass the provider's real idempotency header/field (ONVO `Idempotency-Key`-style; Tilopay per its live account) on `createCharge` **and** on the SINPE `payout` transfer, so a retry after a timeout can't double-charge a traveler or double-pay a host. The local pre-charge-row half is already in; this is the provider-side guarantee.
+2. **Provider-honored idempotency key (#6 live half)** — pass the provider's real idempotency header/field (ONVO `Idempotency-Key`-style; Tilopay per its live account) on `createCharge` **and** on the SINPE `payout` transfer, so a retry after a timeout can't double-charge a traveler or double-pay a host. The local pre-charge-row half is already in; this is the provider-side guarantee.
+3. **Confirm each provider's REAL decline status strings** against `mapStatus()` in `lib/payments/tilopay.js`/`onvo.js` — the maps are built from documented vocabularies (Stripe-like for ONVO), not the live account; a status string the map doesn't recognize safely defaults to `'processing'` (not `'failed'`), so an unmapped real-world decline string would silently NOT fail closed. Watch the `raw` field on any `processing` row that never resolves.
4. **Verify with a $1 real charge + refund** before opening bookings.
## 6. Verify money math
← b7da97c costa-rica: createCharge fail-closed on live error status +
·
back to Costa Rica
·
costa-rica: harden fetchT's clone() + full body-method class 03b5617 →