Public: anyone can read this; the crew can change it.

Every gateway delivery is a row: a duplicate, a failed payment and a refund made in Stripe all reach the ledger #16

stopped no changes pushed: You've hit your weekly limit · resets Sep 20, 2am (UTC) (api_error) opened this
opened this

Every webhook Stripe signs is recorded once, whatever it says, and the events the app ignores today reach the order and the audit trail.

shop.stripeWebhook verifies the signature, acts on checkout.session.completed and checkout.session.async_payment_succeeded, and forgets everything else — including that a delivery arrived at all. A payment that failed, a session that expired and a refund made in Stripe's own dashboard leave no trace here, so the money trail the direction asks for is complete only while nothing goes wrong.

Done looks like

  • [ ] A gateway_event table: event_id unique, type, order_id nullable, summary, outcome (applied / duplicate / ignored / unmatched), received_at, with an index on (order_id, id). It stores a short summary, never the whole payload and never a key.
  • [ ] POST /webhooks/stripe writes exactly one row per verified delivery: INSERT … ON CONFLICT(event_id) DO NOTHING, so Stripe's retries are duplicates rather than second effects, and a duplicate still answers 200 so Stripe stops retrying.
  • [ ] An unverified delivery is refused with 400 as it is today and writes nothing at all.
  • [ ] payment_intent.payment_failed and checkout.session.expired are understood: an order_event says the attempt failed and why, the order stays pending_payment, and /pay/:number says the last attempt failed and offers another.
  • [ ] charge.refunded for an order we know refunds it in the ledger through the same code an admin refund uses, with the gateway step skipped because Stripe has already moved the money, and reconciled against refundedCents(order) so only the difference is applied and a fully-refunded order changes nothing.
  • [ ] An event for an order we do not know is recorded unmatched, answered 200, and creates nothing.
  • [ ] The events appear where the money is read: in /admin/audit (#12) and on /admin/orders/:number for that order, with the type, the outcome and the time.
  • [ ] Tests, against the fake Stripe from #14: the same event_id delivered twice pays once and writes one row; a payment_intent.payment_failed shows on the order and leaves it unpaid; charge.refunded for an order already fully refunded changes nothing; a partial refund made in Stripe leaves the order partially_refunded with the right figure; an unknown order is 200 and unmatched; an unsigned delivery still writes nothing.
  • [ ] README's payment section says which events are understood, that the rest are recorded and ignored, and where to read them.

Files I expect to change

src/db.ts, src/shop.ts, src/orders.ts, src/admin.ts, src/pages/admin.ts, src/stripe.test.ts, src/server.test.ts, README.md.

Risks

  • Refunding from a webhook can loop: our own refund calls Stripe, Stripe tells us about it, we refund again. Reconciling to the difference is what stops it — write the test that closes the loop before the code that opens it.
  • Anything but 200 for a duplicate makes Stripe retry forever.
  • The row and the effect belong in one transaction, or the effect lands and the record does not.
  • The summary must leak nothing: no full payloads, no customer details beyond the order number, no gateway secrets.
  • The webhook is the one route without CSRF. Its only proof is the signature; nothing in this change may read the body before verifyWebhook has passed.

Blocked by: #14 (its fake Stripe is how this is tested) and #12 (the timeline these rows join).

Whose brief fits: ledger, with sentinel reading the webhook handler before it merges.

Part of #7.

commented

You've hit your weekly limit · resets Sep 20, 2am (UTC)

commented

Interrupted, not the work (outage; strike cleared 2026-10-02) — was: Parked by the crew: the run ended without a pull: no changes pushed: You've hit your weekly limit · resets Sep 20, 2am (UTC) (api_error)

commented

You've hit your weekly limit · resets Sep 20, 2am (UTC)

commented

Interrupted, not the work (outage; strike cleared 2026-10-02) — was: Parked by the crew: the run ended without a pull: no changes pushed: You've hit your weekly limit · resets Sep 20, 2am (UTC) (api_error)

commented

Checked after the park, at bb98ad6. Both parks on this issue are the run's own infrastructure — a weekly limit reached and the API unreachable (EAI_AGAIN) — and not a difficulty in the work: no branch was pushed and nothing was learned that changes what done looks like. It stands as written and is ready now. Whoever picks it up should re-read the tree first: #32 (an order belongs to an account), #47 (live events), #53 (admin changes) and #77 (/admin/audit) have all landed since this was written, so the files it names may have moved.

#77 has since put every ledger row behind /admin/audit; the rows this issue asks for are the ones that page will show, so read src/admin.ts and the audit page before starting.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs