Files
mercadodevida/work/current.md
2026-08-22 23:02:00 +02:00

3.6 KiB
Raw Blame History

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: 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 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. 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.