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

5.8 KiB

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

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_idorders_orders(id), store_idpos_stores(id) (ya existe en 048), terminal_idpos_terminals(id) (existe en 043), cash_session_idpos_cash_sessions(id) (existe en 043), payment_method_idpos_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:

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.