110 lines
4.6 KiB
Markdown
110 lines
4.6 KiB
Markdown
# 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:
|
|
|
|
- `PENDING` → `COMPLETED` 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:
|
|
|
|
```json
|
|
{
|
|
"orderId": "…",
|
|
"state": "PENDING" | "COMPLETED",
|
|
"paidCents": 800,
|
|
"outstandingCents": 200,
|
|
...
|
|
}
|
|
```
|
|
|
|
### `POST /pos/sales/:id/payments` (new)
|
|
|
|
Body:
|
|
|
|
```json
|
|
{
|
|
"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.
|