98 lines
5.8 KiB
Markdown
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.
|