diff --git a/backlog/features.json b/backlog/features.json index 42f2c13..770601f 100644 --- a/backlog/features.json +++ b/backlog/features.json @@ -7257,13 +7257,15 @@ "description": "Accept validated negative quantities to cancel purchases fully or partially and issue linked return receipts.", "priority": "high", "risk": "high", - "status": "pending", + "status": "done", "created_at": "2026-08-22", "gates": { - "reviewer": false, - "security": false, - "qa": false - } + "reviewer": true, + "security": true, + "qa": true, + "close": true + }, + "completed_at": "2026-08-22T21:01:59Z" }, { "id": "F-190", diff --git a/docs/pos/POS_CHECKOUT.md b/docs/pos/POS_CHECKOUT.md index ee1679a..6c44cac 100644 --- a/docs/pos/POS_CHECKOUT.md +++ b/docs/pos/POS_CHECKOUT.md @@ -159,8 +159,14 @@ Por cada asignación se crea: El efectivo esperado aumenta por el importe aplicado, no por el efectivo entregado; el cambio no cuenta como ingreso ni efectivo retenido. +## Devoluciones + +`POST /pos/sales/:id/returns` registra devoluciones parciales o totales que conservan el UUID de la venta, restauran el stock y emiten un ticket asociado con el prefijo `R-`. La operación queda registrada en `pos_returns`, `reporting_payment_lines` (status `refund`/`partial_refund`), `orders_order_history` y el log de auditoría. Las devoluciones solo operan sobre ventas `pos` y exigen la caja (`x-terminal-id`) y el rol POS. El endpoint antiguo `POST /pos/sales/:id/refund` queda desregistrado. + +`GET /pos/sales/:id/items` expone las líneas de la orden con `quantity` y `returnedQuantity` para alimentar el modal de devolución. + ## Próximas ampliaciones -- F-189: cantidades negativas, devoluciones parciales/totales y ticket de devolución. +- F-190: auditoría completa de actualización/refresco de reporting. - F-190: auditoría completa de actualización/refresco de reporting. - F-191: cierre de terminal y cierre diario conciliando efectivo, tarjetas, devoluciones y pendientes. diff --git a/project/apps/pos/src/app/(terminal)/page.tsx b/project/apps/pos/src/app/(terminal)/page.tsx index e1fddf6..52bef13 100644 --- a/project/apps/pos/src/app/(terminal)/page.tsx +++ b/project/apps/pos/src/app/(terminal)/page.tsx @@ -5,6 +5,7 @@ import DiscountPanel from '@/components/DiscountPanel'; import FreeItemModal from '@/components/FreeItemModal'; import PaymentModal from '@/components/PaymentModal'; import ReceiptModal from '@/components/ReceiptModal'; +import ReturnModal from '@/components/ReturnModal'; import { posApi } from '@/lib/api-client'; import { generateIdempotencyKey } from '@/lib/idempotency'; import { formatPrice } from '@/lib/money'; @@ -112,6 +113,9 @@ export default function RegisterPage() { const [loadingPending, setLoadingPending] = useState(false); const [restPaymentFor, setRestPaymentFor] = useState(null); const [processingRest, setProcessingRest] = useState(false); + const [returnOrder, setReturnOrder] = useState<{ orderId: string; receipt: PosReceipt } | null>( + null, + ); const loadConfig = useCallback(async () => { setConfigError(''); @@ -1163,6 +1167,26 @@ export default function RegisterPage() { receipt={receipt} initialEmail={customer?.email ?? receipt.customerEmail ?? ''} onDelivered={resetCashier} + onReturn={ + config?.session && config?.terminal + ? (orderId, latest) => { + setReturnOrder({ orderId, receipt: latest }); + } + : undefined + } + /> + )} + {returnOrder && config?.session && config?.terminal && ( + setReturnOrder(null)} + onReturned={(latest) => { + setReturnOrder(null); + setReceipt(latest); + }} /> )} diff --git a/project/apps/pos/src/components/ReceiptModal.tsx b/project/apps/pos/src/components/ReceiptModal.tsx index 061fd94..493ed2d 100644 --- a/project/apps/pos/src/components/ReceiptModal.tsx +++ b/project/apps/pos/src/components/ReceiptModal.tsx @@ -9,12 +9,14 @@ interface ReceiptModalProps { receipt: PosReceipt; initialEmail?: string; onDelivered: () => void; + onReturn?: (orderId: string, receipt: PosReceipt) => void; } export default function ReceiptModal({ receipt, initialEmail = '', onDelivered, + onReturn, }: ReceiptModalProps) { const [email, setEmail] = useState(initialEmail); const [sending, setSending] = useState(false); @@ -166,6 +168,15 @@ export default function ReceiptModal({ > 🖨️ Imprimir ticket + {onReturn && !receipt.isReturn && ( + + )}