I refunded my customer but the money never arrived — Stripe charge.refund.updated webhook (PR #626 GGG F2)
Beatriz Porto Ribeira 38-yo 7-yr Cafe da Praca 42-cover Portuguese tapas + port wine bar narrow lane two streets Douro thMenu Platinum 15 months order tracking + table sessions + customer order tracking + refund flow. Morning 09:00 breakfast trade light Joao bacalhau order refund two weeks ago card statement money still hasn`t come back. Beatriz raised eyebrow thMenu admin dashboard Order #1248 €18 bacalhau Status: refunded Refunded At: 2026-05-12 11:42 12 days ago. Stripe Dashboard Refund ID rf_3PqXyz Status: failed Failure Reason: card_account_closed Joao card account closed Stripe pending Caixa Geral de Depositos report account closed refund failed money bounced back Stripe main balance restaurant balance +€18 customer never received. Beatriz explained Joao Stripe couldn`t reach bank account closed sort it new card details when minute. Support emailed Stripe refund failed thMenu dashboard order still refunded customer didn`t get money panel refunded. Engineering 3 wrong theories (1) RIDS double-write refund flow order POST refund handler /api/orders/[id]/refund Stripe API refunds.create success UPDATE orders SET status=refunded correct Stripe pending thMenu marks refunded webhook surface status transition; (2) marking Stripe pending refund refunded wrong should stay pending partial correct customer-side confusion 5-7 days wait thMenu pattern Stripe API succeeded mark refunded immediately reconcile via webhook later fails; (3) Stripe webhook handler state transition not handled correct charge.refund.updated event Stripe pending refund reaches terminal status (succeeded failed canceled) default branch silent 200 OK. Forensic apps/web-admin/src/app/api/stripe/webhook/route.ts switch case checkout.session.completed + customer.subscription.updated + customer.subscription.deleted + invoice.paid + invoice.payment_failed + charge.dispute.created + charge.refund.created NO charge.refund.updated. Stripe Dashboard 90-day 47 charge.refund.updated default branch silent 200 OK 38 status: succeeded idempotent missing loud-log 6 status: failed Joao customers refunded but money never reached 3 status: canceled refund canceled but still refunded refund never happened. 9 cases thMenu state Stripe state diverged customer money merchant balance refunded. Stripe billing UX refund pending → 3 terminal states succeeded card issuer accepted money to customer + failed card issuer rejection closed account expired card fraud + canceled refund manually cancelled. thMenu only charge.refund.created pending signal failure path not tracked. PR #626 batch GGG F2 3-layer fix Layer 1 charge.refund.updated case webhook handler 3 branches (a) status succeeded silent log idempotent expected [BEACON:refund_succeeded] Logpush; (b) status failed DLT entry refund_failed_restitution + UPDATE orders SET refund_failed_reason WHERE id + operator email refund failed contact customer + customer email your refund could not be processed instructions order status refunded → refund_failed dashboard red banner; (c) status canceled info-log + DLT info row operator review + UPDATE orders SET status = refund_canceled no email manual operation. Layer 2 UI dashboard Refund Failed red banner + manual reissue button new payment method re-trigger manually. Layer 3 90-day backfill 9 cases 6 failed customer outreach + alternate refund methods manual bank transfer + cash + 1-month cafe credit 3 canceled operator review manual refund re-initiation. Beatriz Joao new card details + manual bank transfer €18 + €5 inconvenience credit Joao knew youd sort still see you next week bacalhau. Beatriz Instagram cafe 36 hours fix shipped 1.8k. 9 affected restaurants 1-month Pro/Platinum credit. Damla Izmir Karsiyaka Bostanli Sahil Karsiyaka Boyoz + Lokma 38-cover Aegean breakfast 9-yr same pattern card_account_closed parallel ticket same fix 1-month Pro credit. Pattern every Stripe pending state follow-up event terminal status webhook handler explicit handle + reconcile idempotent terminal state mismatches. Stripe pending states refunds.create → pending charge.refund.updated 3 statuses closed PR #626 GGG F2 + charges.create + 3DS → requires_action payment_intent.succeeded/failed closed CCC F4 + setup_intents.create → processing setup_intent.succeeded/failed not yet closed + payouts.create → pending payout.paid/failed parallel Wise. Implementation each Stripe pending action follow-up event Stripe Dashboard webhook coverage + handler explicit branch + per-terminal-state DLT entry / email / status update + UI dashboard banner + manual reissue button + production backfill 90-day cross-correlate + PR template checkbox. PR #626 reference.
thMenu Team
thmenu.com
Found this helpful? Share it.
Related articles
Why Digital Menus Increase Restaurant Revenue by Up to 30%
Studies show restaurants using digital QR menus see measurable increases in aver…
When a Customer Downgrades, What Happens to Old Features? — The Silent Feature-Drift Problem in SaaS
Most SaaS apps run a single line of code when a customer downgrades — but old fe…
JWT alg-confusion attack — why Supabase's HS256 → RS256/JWKS migration breaks legacy verifiers
Verifiers that never decode the JWT header are wide open to `alg=none` and alg-c…