← back to Costa Rica
cycle 29 docs: YOLO_NOTES ledger — payment-webhook refund-path fixes
d94c613aa5d93fe870b58ad8dd5be4df60eb621a · 2026-09-24 07:08:20 -0700 · Steve
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TouFkmUGKHtwqpZwgReVic
Files touched
Diff
commit d94c613aa5d93fe870b58ad8dd5be4df60eb621a
Author: Steve <steve@designerwallcoverings.com>
Date: Thu Sep 24 07:08:20 2026 -0700
cycle 29 docs: YOLO_NOTES ledger — payment-webhook refund-path fixes
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TouFkmUGKHtwqpZwgReVic
---
YOLO_NOTES.md | 22 ++++++++++++++++++++++
1 file changed, 22 insertions(+)
diff --git a/YOLO_NOTES.md b/YOLO_NOTES.md
index ed5c1cb..35ae97c 100644
--- a/YOLO_NOTES.md
+++ b/YOLO_NOTES.md
@@ -615,3 +615,25 @@ Canonical apply order now: `004_marketplace → 005_apple → 006_contacts → 0
**Cost:** $0 (local + one Cody audit pass). No re-gate on the 1-line CSRF content-type guard — the surface was just Cody-audited this cycle, fix is minimal + tested + bypass-reasoned; proportionate.
**Backlog:** SEVEN gated items on Steve's desk now (host-claim-approval, reconciler-cron, payout-visibility, place-xss-DEPLOY [security-urgent], meic-dedup, createcharge-idempotency, admin-auth-coupling [HIGH]) + 2 SIWA GO-LIVE decisions. Next cycle: fresh cold audit on the last big un-audited surfaces — routes/webhooks.js payment-webhook state machine (replay/out-of-order), lib/whatsapp.js inbound handling, or lib/money.js money-math invariants.
+
+---
+
+## Cycle 29 — cold Cody audit of the payment-webhook state machine: refund path fixed (TK-10346)
+
+**Surface:** routes/webhooks.js payment-webhook + confirmBooking — replay / out-of-order / duplicate resilience. Confirm path is genuinely sound (Cody verified idempotent even under Postgres row-lock concurrency; confirmBooking guards WHERE status='pending'). The REFUND path had a live asymmetric gap. Three fixes:
+
+1. **ONVO refunds silently swallowed** — `lib/payments/onvo.js` STATUS_MAP had NO refund key, so `getCharge()` could never return 'refunded'; a refund mapped to 'processing' (the `|| default`), REGRESSING payments.status and never flipping the booking. tilopay already maps `reversed->refunded`. Added `refunded/reversed->refunded` + a hard GO-LIVE gate: ONVO's real post-refund vocabulary is UNVERIFIED (may be Stripe-like where the PI stays 'succeeded' and getCharge must inspect a refund object). Not live today (tilopay is default) but fires the instant PAYMENT_PROVIDER=onvo.
+
+2. **Idempotency-key collision (silent refund loss)** — `evId` could equal the charge id (Tilopay's `{paymentId, status}` shape has no distinct event id), so a 'succeeded' then a genuinely distinct 'refunded' event for the SAME charge both keyed `firstTime()` on the same id -> the 2nd hit ON CONFLICT DO NOTHING, treated as 'dup', refund NEVER processed (booking confirmed forever, silently, clean 200 to the provider). `evId` now prefers a distinct event id, else a `paymentId:type` composite -> lifecycle events get distinct keys while true replays still dedupe. Reorder also fixed `event_id` being shadowed by `paymentId`.
+
+3. **Refunded UPDATE** — added `updated_at=NOW()` (every other status mutation sets it; a reconcile keyed off it would miss refunds) + `status<>'refunded'` idempotency guard. Booking status is a single 'refunded' enum, so the guard only no-ops redundant events.
+
+**Test:** `test/webhooks-refund-idempotency.test.js` (arg-capture harness + getCharge stub) — distinct keys for succeeded-vs-refunded, replay dedupes to one payments UPDATE, a refunded getCharge reaches the guarded UPDATE, ONVO mapStatus. Suite 230->**234**, serial green. Commit `762091f`.
+
+**Cody-gate (money path) confirmed the fix + caught a doc contradiction:** GO-LIVE §5b called Tilopay's `reversed` path "the verified reference" while item 3 says NEITHER map is verified against a live account — and `reversed` can be an auth VOID (money never moved), not a refund. Folded Tilopay into the same unverified + void-vs-refund caveat; whichever provider goes live first must pass the $1 refund test.
+
+**Cost:** $0 (local + two Cody passes — the audit found the refund gaps, the gate confirmed the fix and caught the doc contradiction).
+
+**Latent (noted, not fixed):** the refund UPDATE has no clawback link to the payouts row — moot while completion/payout is unwired; tracked with the refund-after-payout deferred item.
+
+**Backlog:** 7 gated memos + 2 SIWA + 1 admin-auth GO-LIVE decisions on Steve's desk. Audited surfaces so far: charge leg (c19), payout leg (c23), ingest (c24), SIWA auth (c27), admin authZ (c28), payment-webhook state machine (c29). Next cycle: lib/whatsapp.js inbound handler (phone-spoof / impersonation / send-loop cost) OR lib/money.js money-math invariants — both still un-audited.
← 762091f costa-rica: fix payment-webhook refund path — ONVO status ma
·
back to Costa Rica
·
costa-rica: WhatsApp inbound hardening — auto-reply cost gua 1f25a1f →