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:
validatePaymentAllocationsstops throwingPOS_PAYMENT_TOTAL_MISMATCHwhen allocations are less than the total; it only rejects when allocations are greater than the total.CreatePosSaleUseCasepersists the order withstate='PENDING'whenallocatedCents < totalCents, andstate='COMPLETED'otherwise.outstandingCentsis 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:
PENDING→COMPLETEDwhen the next payment bringspaidCents === totalCents.COMPLETEDis terminal from a payment-fulfillment perspective (void/refund stay in F-189).PENDINGsales 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
paymentssums tototalCents→ order isCOMPLETED(existing behavior). - If
paymentssums to less → order isPENDING.outstandingCentsreturned. - If
paymentssums to more → rejected withPOS_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
PENDINGfrom thepossource. - Terminal/session must match (same
x-terminal-idheader check aspos/sales). - Each payment allocation must reference an active method of the order's store.
- Cash-only
tenderedCents; change istendered - applied. - Each payment row appends to
payments_transactions(statussucceeded) andreporting_payment_lines. expected_cash_centsincreases by cash applied.- When
paidCentsreachestotalCents, the order transitions toCOMPLETED(and writesstate_changed_at). - Idempotency: the same
idempotencyKeyreturns 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
PENDINGPOS sales for the samestoreId, 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:
- Create POS order with partial payment →
PENDINGandoutstandingCents. - Rest payment that covers → transitions to
COMPLETED. - Overpayment rejected at creation and at rest-payment.
- Rest payment on a
COMPLETEDorder rejected withPOS_SALE_NOT_PENDING. - Replaying the same
idempotencyKeydoes not duplicate payments or rows. - Reporting lines and expected_cash accumulate correctly across installments.
- Backend typecheck/build, POS typecheck/build, verify.sh.