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