# F-145 — Architect ## Feature Reporting: payment lines and POS cash-safe capture. ## Background El architecture doc §5.3 (REPORTING_ARCHITECTURE.md) identifica que `payments_transactions` no tiene método de pago, tienda, terminal ni sesión: no permite filtrar por método ni hacer cuadre de caja POS. El doc pide crear `order_payments`/`orders_payment_lines` como líneas de pago inmutables. ## Objetivo Persisitir líneas de pago inmutables por pedido que capturen: método, tienda, terminal, sesión, importe, provider y referencia. Sin datos de tarjeta (PAN/CVV). Estas líneas alimentan `GET /reporting/payments` y `GET /reporting/cash-sessions` en F-146/F-148. ## Diseño ### Nueva tabla: `reporting_payment_lines` ```sql CREATE TABLE reporting_payment_lines ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), order_id uuid NOT NULL REFERENCES orders_orders(id) ON DELETE RESTRICT, store_id uuid NOT NULL REFERENCES pos_stores(id) ON DELETE RESTRICT, terminal_id uuid REFERENCES pos_terminals(id) ON DELETE SET NULL, cash_session_id uuid REFERENCES pos_cash_sessions(id) ON DELETE SET NULL, payment_method_id uuid REFERENCES pos_payment_methods(id) ON DELETE SET NULL, provider text NOT NULL, -- 'stripe', 'cash', 'card', 'bizum' amount_cents integer NOT NULL CHECK (amount_cents != 0), currency text NOT NULL DEFAULT 'EUR' CHECK (currency = 'EUR'), status text NOT NULL CHECK (status IN ('payment', 'refund', 'partial_refund')), provider_ref text, -- stripe payment_intent_id / cash receipt created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now() ); -- reporting por tienda/fecha CREATE INDEX reporting_payment_lines_store_created_idx ON reporting_payment_lines (store_id, created_at); -- un pago completo = línea payment + posibles líneas refund CREATE INDEX reporting_payment_lines_order_idx ON reporting_payment_lines (order_id); -- cash-safe: por sesión CREATE INDEX reporting_payment_lines_session_idx ON reporting_payment_lines (cash_session_id) WHERE cash_session_id IS NOT NULL; ``` ### Notas de diseño 1. **Inmutable**: no hay UPDATE en la tabla; solo INSERT (un refund es una nueva línea con status='refund'). Un `updated_at` se mantiene por trazabilidad de inserciones desde múltiples procesos (ON INSERT SET updated_at = now()). 2. **provider**: distingue Stripe (ecommerce), cash/card (POS). No se guarda PAN ni CVV. 3. **cash_session_id**: NULL para ecommerce (no hay caja física). Para POS, la sesión abierta vincula el pago a la caja. 4. **amount_cents != 0**: rechaza líneas de 0€ (no tiene sentido). payment o refund siempre tienen importe. 5. **Backfill**: la migración no backfillea `payments_transactions` existente porque faltan store_id/terminal_id/method — los campos necesarios. Los pedidos pre-F-145 sin método de pago explícito quedan con dataAvailability=false en los filtros de pago. 6. **Orden de FK**: `order_id` → `orders_orders(id)`, `store_id` → `pos_stores(id)` (ya existe en 048), `terminal_id` → `pos_terminals(id)` (existe en 043), `cash_session_id` → `pos_cash_sessions(id)` (existe en 043), `payment_method_id` → `pos_payment_methods(id)` (existe en 043). Circular: no hay (reporting_payment_lines no tiene FK hacia otra tabla nueva). ### Cómo se inserta (contracto para implementación) El módulo de checkout/orders es responsable de llamar a un nuevo `ReportingPaymentLinesRepository.insert(input)` tras confirmar el pago: ```typescript interface InsertPaymentLineInput { orderId: string; storeId: string; terminalId?: string; cashSessionId?: string; paymentMethodId?: string; provider: string; // 'stripe' | 'cash' | 'card' | 'bizum' amountCents: number; status: 'payment' | 'refund' | 'partial_refund'; providerRef?: string; } ``` Para **ecommerce**: `storeId` viene del pedido (ya backfillable tras 048); `terminalId`/`cashSessionId` son NULL. Provider se determina del webhook (`StripePaymentProvider`). Para **POS**: `storeId`/`terminalId`/`cashSessionId`/`paymentMethodId` se pasan desde el flujo POS en el checkout. Provider = `cash` o `card`. ### Decisiones rechazadas - **No crear `order_refunds` separada**: un refund es una línea con status='refund' en la misma tabla (agrupable por order_id). - **No guardar PAN**: requisito PCI-DSS mínimo. Provider_ref es el id externo (payment_intent_id), no datos de tarjeta. - **No INSERT en `payments_transactions`**: esa tabla es de eventos de provider (Stripe webhooks); `reporting_payment_lines` es una capa de reporting que existe independientemente de qué provider envió el evento. ## Consecuencias - Migration 049 idempotente (`ADD TABLE IF NOT EXISTS` + `DO $$` guard para cada constraint si se re-ejecuta en PG<16). - El servicio de reporting en F-146 puede hacer `JOIN reporting_payment_lines` con `orders_orders` para enriquecer métricas de pago por tienda/método/terminal. - F-147/F-148 (dashboard) consumirán estas líneas para el panel de caja y métodos de pago. ## Acceptance Criteria AC1: La tabla `reporting_payment_lines` existe con las columnas, constraints e índices diseñados (PK, FK validadas, CHECK amount_cents!=0, CHECK status IN, tres índices). AC2: Un INSERT con todos los campos FK válidos se completa sin error. AC3: Un INSERT con amount_cents=0 falla con constraint violation. AC4: Un INSERT sin order_id falla con FK violation. AC5: La migración es idempotente (re-ejecutar up() es no-op) y reversible (down() elimina la tabla y los índices). AC6: El módulo `reporting` puede consultar la tabla (unittest con mock DB o integración contra migración aplicada). AC7: No hay regresión en los flujos existentes (checkout, POS, payments — no se modifica su comportamiento). AC8: Los gates (reviewer, security, qa) + verify.sh pasan.