feat(F-189): completed feature
This commit is contained in:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user