3.6 KiB
3.6 KiB
F-189 — POS negative returns and return receipts
Allow POS cashiers to fully or partially return previously sold items, restore stock and issue a linked return receipt while preserving historical attribution.
Scope
- Migration
056_pos_return_lines.js: addorders_items.returned_quantity integer NOT NULL DEFAULT 0withCHECK (returned_quantity >= 0 AND returned_quantity <= quantity). Existing rows stay at 0. - New
ApplyPosReturnUseCaseconsumesPOST /pos/sales/:id/returns. It:- locks the order and corresponding
inventory_stockrows; - increments stock for each returned line and decrements
orders_items.returned_quantity; - emits
reporting_payment_lineswithstatus='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_centsby the cash portion of the refund; - transitions the order to
REFUNDED(fully returned) orPARTIALLY_REFUNDED; - records an
orders_order_eventsrow and apos.sale.returned/pos.sale.partial_returnedaudit event.
- locks the order and corresponding
- Replacement of the legacy
POST /pos/sales/:id/refundendpoint with the new return contract. The legacy route is removed. POST /pos/sales/:id/returnsrequires POS roles and the same terminal binding check used elsewhere (x-terminal-idmust 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 mirrorsbuildPosReceiptbut uses negative quantities, prefixesR-on the receipt number and shows the original receipt reference. - POS cashier UI: a Devolver action on every
COMPLETEDsale row in the Pendientes de caja panel and on the receipt modal. OpensReturnModal(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
idempotencyKeyonPOST /pos/sales/:id/returnsreturns 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 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
- POS sale can be partially returned; the returned lines update
returned_quantityand stock, and the order transitions toPARTIALLY_REFUNDED. - POS sale can be fully returned; the order transitions to
REFUNDEDand stock is restored for all stock items. - Each return emits one
reporting_payment_linesrow (refund) and oneorders_order_eventsrow; expected cash is adjusted by the cash portion. - Free items can be returned only fully (no stock movement).
- Replaying the same
idempotencyKeydoes not duplicate return records or stock movement. - Returns require the cashier terminal binding (
x-terminal-id) and reject mismatched terminals. - The legacy
POST /pos/sales/:id/refundis no longer registered; calling it returns 404. - Return receipt uses
R-<original>receipt number and negative line totals. - 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.
- Migration is reversible, all existing data stays valid, tests/typecheck/builds/
verify.share green.