# MVP-TASK-029 — Unknown checkout outcome

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

## Objective and required behavior

Represent an indeterminate checkout durably/visibly, never silently downgrade it to failure, prevent
blind retry, and exclude it from confirmed sales until explicit reconciliation semantics exist.

## Scope, areas, and non-goals

Expected application state/persistence/API/UI/fake tests. Do not invent Surfboard reconciliation or
let the operator create another charge through the same attempt. No generic payment state machine.

## Evidence and acceptance

Use all three IDs in integration and E2E. Accept when reload retains uncertainty, controls prevent
blind retry, copy avoids paid/unpaid claims, traceability/contracts are current, and inherited DoD
passes. Handoff states operational limitation and future reconciliation need.

## Implementation evidence

- **The three non-success outcomes are now three separate things.** `unknown` was previously folded
  into `OrderCheckoutIncompleteError` alongside a confirmed-but-uncommitted charge, so "we do not
  know" and "we know the money was captured" shared one error. `unknown` now raises
  `OrderCheckoutUnresolvedError` and is recorded durably before it is reported; a definitive refusal
  keeps `OrderCheckoutDeclinedError`; a confirmed charge that could not be committed keeps
  `OrderCheckoutIncompleteError`. Handling `unknown` first also made the old
  `isDefinitiveCheckoutRefusal` ternary unreachable — TypeScript now narrows the remainder to the
  two refusals, so that dead branch was deleted rather than left behind as untestable code.
- **Durable means its own fact, with no `orders` row.** The new append-only
  `order_checkout_unresolved_attempts` table holds the application-owned `operationId` (unique), the
  `unknown` outcome, the provider's optional reference, the requested gross amount, the currency,
  and the database time. It deliberately has **no** foreign key to `orders`: an unresolved attempt
  finalizes nothing, and that absence is the mechanism that keeps it out of the kitchen queue and
  out of commercial sales — the exclusion is by construction, not by a filter someone could forget.
  A check constraint restricts the stored outcome to `unknown`, mirroring how
  `order_checkout_attempts` restricts its own to `succeeded`.
- **Failure to record is raised, never swallowed.** If the unresolved row cannot be written, the
  persistence error propagates instead of reporting a tidy "unresolved" while dropping the very fact
  the operator needs — that would be the silent downgrade this task exists to prevent.
- **"Cannot be blindly retried" is enforced in three places.** Durably, the unique `operationId`
  means one operation cannot produce a second record, the row is append-only under a trigger, and
  _no_ repository or HTTP operation resolves, retries, or settles an attempt — reconciliation
  semantics do not exist and inventing them is an explicit non-goal. In the UI,
  `CHECKOUT_UNRESOLVED` disables "Pay and send to kitchen" for the affected draft while the
  non-commercial control stays available, since that one takes no payment. In the API, the read-only
  list is the whole surface: `order-http.test.ts` asserts exactly one `/api/checkout/` route exists
  and that it is the GET.
- **The uncertainty survives the page.** `GET /api/checkout/unresolved` reconstructs the list from
  durable rows, and the KDS renders it as a warning region on _every_ view — not only inside the
  cart that produced it — so a reload, a second tablet, or a restarted process all still show it.
  Each entry renders only the amount _requested_ and the recorded time; no provider reference and no
  outcome verdict is displayed, so nothing on screen can be read as a claim that money was captured.
- **Copy makes no claim in either direction.** `errors.checkoutUnresolved` states the uncertainty
  and the two facts the operator can rely on (nothing went to the kitchen, nothing entered today's
  sales) and points at the payment provider. It never reuses the refusal wording, which would assert
  "nothing was charged". `CHECKOUT_UNRESOLVED` is a new stable code so a client can never conflate
  this with the certain refusal (`CHECKOUT_NOT_PAID`) or a plain conflict.
- **The fake's unconfirmed outcome is amount-keyed.** `CHECKOUT_FAKE_UNKNOWN_AMOUNT_MINOR` extends
  the reject/cancel pattern so one deterministic backend process can serve an unconfirmed attempt, a
  definitive refusal, and a confirmed sale in the same run, in either test order and in both browser
  projects. It is a separate setting rather than another value of `CHECKOUT_FAKE_SCENARIO` because
  `unknown` is not a certainty about money and must be reproducible without making every charge in
  the run indeterminate. Configuration validation now rejects _any_ two of the three amounts being
  equal, so `unknown` can never be shadowed by a refusal by an arbitrary precedence rule.

### Deliberate limitations

- **There is no reconciliation.** No endpoint, command, or UI control resolves an unresolved
  attempt, and none was invented. Unresolved attempts therefore accumulate for the lifetime of the
  database and are always shown, which is honest: each one is a real unresolved money question.
- **The retry block is scoped to the affected draft.** It prevents re-charging the same attempt
  through the same draft. A cross-device guarantee that a repeat of the _same_ operation identity
  cannot initiate a second charge belongs to MVP-TASK-030 (stable operation identity and
  idempotency), which depends on this task's durable record.

## Validation for MVP-TASK-029

`pnpm quality` passes (exit 0), retaining only the known non-fatal Redocly `/health` advisory and
the third-party Redux TypeDoc warning. `pnpm test` passes with 268 frontend tests and 313 backend
unit/API tests; `pnpm test:coverage` holds 100% statements/branches/functions/lines in both
workspaces; `pnpm test:ci` passes all 40 CI contract tests; `pnpm test:integration` passes 77
real-PostgreSQL tests; `pnpm build`, `pnpm secrets:check`, and `pnpm containers:lint` pass; and the
full dev-mode `pnpm e2e` suite passes with 68 tests across desktop/mobile Chromium (the new journey
included). Hosted CI and its artifact contract were not run in this session, so the task stays **In
progress** rather than Done. `pnpm db:check`'s migration-drift comparison passes; its final "no
uncommitted drizzle output" assertion can only pass once migration `0013` is committed, and no
commit was requested.
