feat(F-189): completed feature

This commit is contained in:
chattie
2026-08-22 23:01:59 +02:00
parent 0e3c488c85
commit 6b93e91ef4
24 changed files with 1535 additions and 63 deletions

View File

@@ -1,30 +1,39 @@
# F-188 — POS pending payment orders and completion states
# F-189 — POS negative returns and return receipts
Allow POS orders to be saved with an outstanding balance, ensure fully paid POS orders reach `COMPLETED`, and let cashiers apply later payments to clear the outstanding balance.
Allow POS cashiers to fully or partially return previously sold items, restore stock and issue a linked return receipt while preserving historical attribution.
## Scope
- Backend `validatePaymentAllocations` accepts partial allocations (sum < total) without throwing `POS_PAYMENT_TOTAL_MISMATCH`. Overpayment remains rejected.
- `POST /pos/sales` persists orders with `state='PENDING'` when allocated total < order total, and `state='COMPLETED'` when allocations cover the order total.
- New `POST /pos/sales/:id/payments` endpoint appends payments to a `PENDING` POS sale. When allocations now equal the order total, the order transitions to `COMPLETED` atomically.
- Stock, reporting lines, payment lines and expected cash balance are updated by every payment, including rest-payment installments.
- Sale list exposes `state`, `paidCents` and `outstandingCents`. Cashier can fetch `PENDING` POS sales for the current session.
- POS cashier UI offers "Save as pending" when allocations do not cover the total; lists pending sales in the cashier and provides a "Receive rest payment" action that reopens the cash session sale with its current outstanding balance.
- Audit log records partial creation and pending fulfillment transitions.
- `bounded shipments` and refunds remain out of scope (F-189).
- Migration `056_pos_return_lines.js`: add `orders_items.returned_quantity integer NOT NULL DEFAULT 0` with `CHECK (returned_quantity >= 0 AND returned_quantity <= quantity)`. Existing rows stay at 0.
- New `ApplyPosReturnUseCase` consumes `POST /pos/sales/:id/returns`. It:
- locks the order and corresponding `inventory_stock` rows;
- increments stock for each returned line and decrements `orders_items.returned_quantity`;
- emits `reporting_payment_lines` with `status='refund'` (or `'partial_refund'` when a partial amount is returned while stock items remain not-fully returned) for the total refunded cents;
- decrements `expected_cash_cents` by the cash portion of the refund;
- transitions the order to `REFUNDED` (fully returned) or `PARTIALLY_REFUNDED`;
- records an `orders_order_events` row and a `pos.sale.returned` / `pos.sale.partial_returned` audit event.
- Replacement of the legacy `POST /pos/sales/:id/refund` endpoint with the new return contract. The legacy route is removed.
- `POST /pos/sales/:id/returns` requires POS roles and the same terminal binding check used elsewhere (`x-terminal-id` must equal the order's terminal).
- A free-item can be returned only as a full-return (it had no stock movement).
- Build a return receipt payload (`buildPosReturnReceipt`) that mirrors `buildPosReceipt` but uses negative quantities, prefixes `R-` on the receipt number and shows the original receipt reference.
- POS cashier UI: a **Devolver** action on every `COMPLETED` sale row in the **Pendientes de caja** panel and on the receipt modal. Opens `ReturnModal` (new) with item rows and `+ / ` quantity steppers. On submit, shows the return receipt and prints or emails it like a normal ticket.
- Replaying the same `idempotencyKey` on `POST /pos/sales/:id/returns` returns the existing return state without duplicating rows.
- Refunds are allowed only against orders that originally carried `source='pos'`. Ecommerce/admin sales follow their own refund paths (out of scope).
- Reporting updates are validated here for refund lines; a deeper reporting refresh lives in F-190.
## Out of scope
- Refunds, partial refunds and return tickets (F-189).
- Daily close reconciliation on top of pending balances (F-191).
- Customer credit, deposit or deposit-paper workflows.
- Refunds on ecommerce or admin sales.
- Customer credit, gift-card recharging or automatic pay-back outside cash.
- Multi-currency refunds.
- Customer-driven (post-sale) returns triggered from the storefront.
## Acceptance
1. Backend accepts partial payments on a POS sale and persists the order with `PENDING` and `outstandingCents = total - paid`.
2. Backend still marks fully paid POS orders as `COMPLETED` and rejects overpayment.
3. Pending POS sales appear in the cashier's session list with the outstanding balance.
4. Cashier can apply a follow-up payment to a `PENDING` sale; reaching the total transitions it to `COMPLETED` atomically.
5. Stock is reserved on POS sale creation (not on full payment) and reporting captures every partial payment.
6. Replaying the same idempotency key keeps the original order, its state and its payments intact.
7. Non-admin/non-POS users cannot create or modify pending POS sales.
8. Migration is reversible (or no migration is needed) and existing fully paid sales stay `COMPLETED`.
9. Tests, typecheck, affected builds and `verify.sh` are green.
1. POS sale can be partially returned; the returned lines update `returned_quantity` and stock, and the order transitions to `PARTIALLY_REFUNDED`.
2. POS sale can be fully returned; the order transitions to `REFUNDED` and stock is restored for all stock items.
3. Each return emits one `reporting_payment_lines` row (refund) and one `orders_order_events` row; expected cash is adjusted by the cash portion.
4. Free items can be returned only fully (no stock movement).
5. Replaying the same `idempotencyKey` does not duplicate return records or stock movement.
6. Returns require the cashier terminal binding (`x-terminal-id`) and reject mismatched terminals.
7. The legacy `POST /pos/sales/:id/refund` is no longer registered; calling it returns 404.
8. Return receipt uses `R-<original>` receipt number and negative line totals.
9. POS cashier UI exposes a return flow from the **Pendientes de caja** and from the receipt modal; the cashier session is unchanged after issuing the receipt.
10. Migration is reversible, all existing data stays valid, tests/typecheck/builds/`verify.sh` are green.