Files
mercadodevida/work/artifacts/F-145/architect.md
2026-08-22 12:47:16 +02:00

98 lines
5.8 KiB
Markdown

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