Files
mercadodevida/work/artifacts/F-188/architect.md
2026-08-22 22:44:37 +02:00

4.6 KiB

F-188 — Architecture

Decision

Allow POS orders to be created with or without full payment. Allow appending more payments until the order is fully paid.

The schema and state machine already support PENDING and COMPLETED. No new migration is required. Two additive changes:

  1. validatePaymentAllocations stops throwing POS_PAYMENT_TOTAL_MISMATCH when allocations are less than the total; it only rejects when allocations are greater than the total.
  2. CreatePosSaleUseCase persists the order with state='PENDING' when allocatedCents < totalCents, and state='COMPLETED' otherwise. outstandingCents is computed at response time.

Semantics

A POS sale has:

  • totalCents: authoritative server figure.
  • paidCents: sum of accepted payments (payments_transactions.amount_cents).
  • outstandingCents = totalCents - paidCents.

Transitions:

  • PENDINGCOMPLETED when the next payment brings paidCents === totalCents.
  • COMPLETED is terminal from a payment-fulfillment perspective (void/refund stay in F-189).
  • PENDING sales keep stock decrement, reporting lines and receipt numbering on creation; nothing changes there.

API

POST /pos/sales (modified)

Body remains compatible with F-186. New behavior:

  • If payments sums to totalCents → order is COMPLETED (existing behavior).
  • If payments sums to less → order is PENDING. outstandingCents returned.
  • If payments sums to more → rejected with POS_PAYMENT_OVERPAYMENT (new).

Response gains:

{
  "orderId": "…",
  "state": "PENDING" | "COMPLETED",
  "paidCents": 800,
  "outstandingCents": 200,
  ...
}

POST /pos/sales/:id/payments (new)

Body:

{
  "idempotencyKey": "rest-pay-1",
  "cashSessionId": "uuid",
  "terminalId": "uuid",
  "payments": [
    { "methodCode": "cash", "amountCents": 200, "tenderedCents": 250 }
  ]
}

Rules:

  • Order must be PENDING from the pos source.
  • Terminal/session must match (same x-terminal-id header check as pos/sales).
  • Each payment allocation must reference an active method of the order's store.
  • Cash-only tenderedCents; change is tendered - applied.
  • Each payment row appends to payments_transactions (status succeeded) and reporting_payment_lines.
  • expected_cash_cents increases by cash applied.
  • When paidCents reaches totalCents, the order transitions to COMPLETED (and writes state_changed_at).
  • Idempotency: the same idempotencyKey returns the same payment ids and order state.
  • Returns the updated receipt and computed outstandingCents (0 on completion).

GET /pos/sales (modified)

querystring gains state (PENDING | COMPLETED) and outstandingOnly boolean. Response rows gain state, paidCents, outstandingCents.

Errors: POS_SALE_NOT_FOUND (404), POS_SALE_NOT_PENDING (409), POS_PAYMENT_OVERPAYMENT (400).

Race safety

A single transition wraps payments_transactions inserts + reporting_payment_lines inserts + pos_cash_sessions update + orders_orders update in one transaction with SELECT ... FOR UPDATE on the target order. Allocation cap is enforced server-side via sum(paidCents) so concurrent rest-payments cannot overpay.

Reporting

Every payment — initial or rest — emits reporting_payment_lines with payment_method_id, terminal_id, cash_session_id, store_id. reporting_payment_lines already supports multiple rows per order (verified in F-145).

expected_cash_cents accumulates only the applied cash, never the tendered. Cash-only tenderedCents still informs the cashier UI change.

POS cashier UI

  • The "Cerrar ticket" button label flips to Cobrar e imprimir or Guardar pendiente depending on whether allocations cover the total.
  • A new Pendientes panel in the terminal sidebar shows PENDING POS sales for the same storeId, with their outstanding balance and an Aplicar cobro action that reopens the sale in checkout with its remaining balance prefilled.
  • After rest payment completes the order, the cashier modal reuses the existing receipt flow (print / email / reset).

Admin and reporting updates are tracked separately in F-190.

Tests

Integration coverage:

  1. Create POS order with partial payment → PENDING and outstandingCents.
  2. Rest payment that covers → transitions to COMPLETED.
  3. Overpayment rejected at creation and at rest-payment.
  4. Rest payment on a COMPLETED order rejected with POS_SALE_NOT_PENDING.
  5. Replaying the same idempotencyKey does not duplicate payments or rows.
  6. Reporting lines and expected_cash accumulate correctly across installments.
  7. Backend typecheck/build, POS typecheck/build, verify.sh.