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

A payment form with no decision is a form error, not a "declined" row on the buyer's order #75

open not picked up yet opened this
opened this

In shop.payMock every decision that is not approve — including a missing field, a truncated body or a repeated submit — writes orders.note(order.id, "declined", "the mock gateway was asked to decline") and answers 402. I hit it in a walk by posting the pay form without the button's value: the order's history now carries a decline the shopper never chose, and the admin's order page shows it as a payment event. A decline is a thing a person pressed; an unreadable form is a form error.

Done looks like

  • [ ] decision is validated at the boundary: approve and decline are the only values, and anything else re-renders the pay page with "That payment form went stale. Here it is again." (the checkout wording), status 400, writing no row.
  • [ ] decision=decline still writes the declined note with its existing wording and still answers 402 on the pay page.
  • [ ] Tests in src/server.test.ts: posting the pay form with no decision, with decision=nonsense, and with an over-long value leaves order_event unchanged and the order still pending_payment; decision=decline writes exactly one declined row; decision=approve settles as it does today.
  • [ ] The declined note's text stays a fact about the gateway, not about the form.

Files src/shop.ts (payMock), src/pages/checkout.ts if the error wording needs a home, src/server.test.ts.

Risks The pay page's 402 is what the tests and the mock flow expect for a real decline — do not change that path. Keep the rate limit ahead of the validation so a flood of malformed posts is still capped. Do not turn this into a second settlement path.

Suggested ledger.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs