Script a mock failure

Make the payment processor decline on purpose, replay a webhook, and read the wire log.

Owned by Admin · 9 steps · about 18 minutes

Why this exists

Every external service on this platform — the card processor, Telegram, the wallet push services, the print-and-mail house, the social networks — sits behind an adapter with an in-repo mock. The whole system therefore runs and tests offline, and, more usefully, failures are scriptable. You do not need a real declined card to practise a declined card.

A mock behaviour is a small instruction: for this service, for this method, behave this way, this many times. The behaviours are success, error, timeout and decline. They are consumed as they fire, which is what makes them safe — you script one decline, the next call declines, and the call after that succeeds again. Nothing is left permanently broken because you forgot to undo it.

Inbound events work the other way round. Real providers call webhooks; in training you simulate one, and every simulated event is stored exactly like a real one, which means you can replay it. Replay is how you prove idempotency: a settled payout replayed twice must not pay twice. Seven inbound events have handlers registered in this build — counted from the registry on 2026-08-30, and corrected from "three": payments charge.succeeded, charge.failed, payout.settled and payout.failed; pos.sales_snapshot; sound.reading_batch; and the Telegram callback_query. Anything else is stored but answered with 422 no_handler rather than being silently swallowed.

The point of all of it is that failure paths get rehearsed. A tab settlement that declines freezes a tab and leaves the balance owed; a filing whose mail partially fails leaves the package in a specific state you must recognise. You want to have seen those in training, not for the first time in production.

Before you start

  • An admin session with debug endpoints enabled.
  • A non-production environment — scripted failures are for demo and training databases.
  • A VIP with an unsettled tab if you want to watch a decline land somewhere real (vip@club.test has one).

Practise with

PersonaEmailPasswordNote
adminadmin@club.test admin123scripts the behaviour and reads the wire log
vip_membervip@club.test vip123seeded with a $250 tab limit and $80 used — the balance a declined settlement will strand

Steps 1–9 — Admin

their manual →
  1. 1
    List the behaviours currently scripted, before you add another. Leftovers from someone else's demo explain a surprising number of mysteries.
    Expected result Any queued behaviours with their service, method and remaining count.
    Watch out for An empty list means every adapter is behaving normally. That is the state you should leave it in.
  2. 2
    Script one decline: service payments, the charge method, behaviour decline, one time.
    Expected result The stored behaviour row.
    Watch out for Only five services and four behaviours are accepted; anything else is 400 bad_behavior. The count matters — script one, not one hundred, unless you want to spend the afternoon wondering why nothing works. And know what those five are NOT: the adapter registry holds EIGHT, and email, pos and push are refused by a hand-written list that predates them. So the one failure you cannot rehearse here is email — the channel carrying the signup code, the sign-in link and every invoice. Plan 677.
  3. 3
    Now fire something that charges a card: run the tab settlement for a period, for the one user you scripted the decline against.
    Expected result The settlement for that user is recorded as failed, and their tab is frozen. The balance is still owed.
    Watch out for Freezing a tab is a credit decision, not forgiveness. Nothing is written off, and the person keeps the debt — they simply cannot add to it.
  4. 4
    Confirm what the failure looks like from the operational surface you would actually be sitting in front of.
    Expected result A failed settlement row for that user and period.
    Watch out for Settlements are unique per user and period, so the retry path is a retry — not a second settlement. This is why a failed row is a thing to act on rather than delete.
  5. 5
    Read the wire log filtered to the payments service and find the declined charge, with the request and the response the mock returned.
    Expected result The outbound call recorded even though it failed.
    Watch out for Declines still commit their wire log. If a call is missing here, it never left — that is a different bug from a call that was rejected.
  6. 6
    Now go the other way: simulate an inbound event. Send a payments payout settlement with a transfer id that exists.
    Expected result The event is stored, dispatched to its handler and marked handled.
    Watch out for An event type nobody handles is stored and answered 422 no_handler. CORRECTED 2026-08-30: SEVEN event types have handlers, not three — payments charge.succeeded, charge.failed, payout.settled and payout.failed, pos.sales_snapshot, sound.reading_batch, and the Telegram callback_query. So a 422 means you named something outside that list; check it against the list before concluding the platform is missing a handler.
  7. 7
    Replay the same stored event and watch nothing happen twice.
    /debug/webhooks/{webhook_id}/replay POST core
    Expected result The handler runs again and reports a no-op; the payout is not paid a second time.
    Watch out for That no-op is the whole point of the exercise. If a replay ever does move money twice, you have found a real bug and it belongs in an incident, not in a training note.
  8. 8
    Delete any behaviour you scripted but did not consume.
    /debug/mock-behaviors/{behavior_id} DELETE core
    Expected result Confirmation that it is gone.
    Watch out for This is the tidy-up nobody remembers. An unconsumed decline sitting in the queue will ambush the next person to demo a purchase.
  9. 9
    Finish by reading your own trail: every behaviour you scripted, every webhook you simulated and replayed, with the bodies you sent.
    Expected result Your session's debug actions, in order.
    Watch out for Append-only. Scripting a failure is a legitimate thing to do and a recorded thing to do — both at once.