# MVP-TASK-028 — Definitive checkout failure/cancellation

**Status:** In progress  
**Parent:** MVP-PAY-001-S02; MVP-SALES-001-S07

## Objective and required behavior

Handle a deterministic definitive rejection/cancellation as not paid: no successful commercial queue
entry or sales, with visible safe continuation and retained/correctly recoverable cart state.

## Scope, areas, and non-goals

Expected fake configuration, application outcome handling, API/UI feedback, persistence as required,
and tests. No unknown result, automatic retry, or live provider.

## Evidence and acceptance

Use both IDs in E2E and integration evidence. Accept when no successful finalized sale is produced,
the operator can continue without losing certainty, traceability/contracts are current, and
inherited DoD passes. Handoff records cancellation versus rejection semantics.

## Implementation evidence

- `OrderService` no longer collapses every non-success outcome into one error. A definitive refusal
  raises `OrderCheckoutDeclinedError`, which carries the provider-neutral class
  (`DefinitiveCheckoutRefusal` = `rejected` | `cancelled`); `unknown` and a confirmed charge that
  could not be committed keep `OrderCheckoutIncompleteError`, because "unpaid" would be a false
  claim about money the application does not know. `order-service.test.ts` proves both branches and
  asserts the two classes never cross.
- `POST /api/orders/commercial` maps the refusal to `409` with a new stable code
  `CHECKOUT_NOT_PAID`, distinct from the generic `CONFLICT`. The body carries no provider detail and
  does not distinguish rejection from cancellation: the two are the same certainty, so exposing the
  split would invent a distinction the operator cannot act on. The endpoint's `409` is documented
  inline in `docs/api/openapi.yaml` with both example bodies.
- The KDS cart renders `errors.checkoutNotPaid`, stating the three certainties the operator acts on:
  nothing was charged, nothing went to the kitchen, and the draft is kept. Every other failure keeps
  the previous generic wording, which claims nothing either way; `KdsPage.test.tsx` asserts the
  unresolved case never shows "declined" or "nothing was charged".
- **Persistence is unchanged, deliberately.** The `order_checkout_attempts_outcome` check constraint
  already restricts stored attempts to `succeeded`, so a declined attempt is structurally at all.
  The integration case proves that absence rather than assuming it: no order, item, finalization, or
  attempt row is written, the active queue is byte-identical, and today's gross and product counts
  are unchanged across repeated reads. It then finalizes successfully, proving the operator's
  continuation is genuinely productive rather than merely unblocked.
- The deterministic fake gained optional amount-keyed refusals (`CHECKOUT_FAKE_REJECT_AMOUNT_MINOR`,
  `CHECKOUT_FAKE_CANCEL_AMOUNT_MINOR`). A refusal is a pure function of the requested gross total
  rather than of call order, which is what lets one deterministic backend process serve a declined
  checkout and a confirmed sale in the same run, in either test order and in both browser projects.
  Both settings are absent by default, validated as chargeable integers, and rejected when equal —
  otherwise the reported certainty would depend on an arbitrary precedence rule. They apply only to
  the fake: with no adapter configured the result stays `unknown` rather than becoming a fabricated
  "declined".
