feat(F-190): completed feature

This commit is contained in:
chattie
2026-08-23 07:48:20 +02:00
parent 6b93e91ef4
commit eb3322e309
22 changed files with 287 additions and 67 deletions

View File

@@ -1,39 +1,22 @@
# F-189 — POS negative returns and return receipts
# F-190Reporting updates from POS sales and returns
Allow POS cashiers to fully or partially return previously sold items, restore stock and issue a linked return receipt while preserving historical attribution.
Fix reporting capture and refresh so POS sales payments returns pending and completed states update reports.
## Scope
- 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.
- POS sales (POST /pos/sales) already emit `reporting_payment_lines` rows on payment — these are verified to capture correctly.
- POS returns (POST /pos/sales/:id/returns) already emit `reporting_payment_lines` with status=`refund`/`partial_refund` — these are verified to capture correctly.
- PENDING-payment sales (F-188) when they transition to COMPLETED must emit a payment line to `reporting_payment_lines` so the report shows the sale.
- Orders in `PARTIALLY_REFUNDED` and `REFUNDED` must reflect the updated totals in `reporting_payment_lines`.
- A refresh mechanism for `reporting_payment_lines` for a given order_id exists (for correction scenarios) — or a clear note that manual correction is required.
- Any gaps in `expected_cash_cents` calculation for returns are verified and fixed.
## Out of scope
- 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.
- Ecommerce or admin order refunds.
- Automatic reconciliation of discrepancies (manual correction only).
## Acceptance
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.
1. A PENDING sale that transitions to COMPLETED emits exactly one `reporting_payment_lines` row with the correct amount and status.
2. A fully-returned sale shows a `refund` payment line in reporting with negative amount.
3. A partially-returned sale shows a `partial_refund` payment line in reporting with the partial amount.
4. Reporting summary totals match the sum of `reporting_payment_lines` for the date range.
5. `verify.sh` green, typecheck green, all tests pass.