Hoja de ruta — Integración TrueLayer (Conciliación bancaria) en MultiFactu
Hoja de ruta — Integración TrueLayer (Conciliación bancaria) en MultiFactu
Estado: runtime implementado en la rama
TrueLayer(P0–P7 en verde: typecheck + ultracite + tests +arch:check+ gate del módulo). Pendientes operativos: aplicar migraciones en cada entorno, alta/contrato comercial con TrueLayer (§17) y ampliar journeys de carga (L10) y E2E. Rama:TrueLayer· Objetivo: integración modular, escalable y profesional con calificación 10/10. Fuente oficial (local):docs-official/TrueLayer/(192 documentos). Índice agéntico: https://docs.truelayer.com/llms.txt. Producto principal: Data API V1 (Account Information / AIS). Módulo destino:src/modules/bank-reconciliation· Schema Prisma:bank-reconciliation. Alcance MVP (decidido): conciliar ingresos y gastos — entradas contra facturas de venta (ProductInvoice, ya existe) y salidas contra gastos/facturas de proveedor (ProductExpense, dominio nuevo a crear). Revisión integral de documentación: 2026-06-24.
0. Estado de implementación (rama TrueLayer)
| Fase | Alcance | Estado |
|---|---|---|
| P0 | Cimientos: módulo en capas, ReconciliationConfigService, value objects, constantes, puertos, catálogo truelayer-api-endpoints.yml, .env.example, wiring en AppModule. |
✅ Hecho |
| P1 | Auth y consentimiento: OAuth2 (auth dialog + /connect/token), refresh, cifrado de tokens en reposo (AES-256-GCM, clave vía scrypt), máquina de estados de la conexión. |
✅ Hecho |
| P2 | Transporte HTTP resiliente (timeout/budget, retry+jitter, circuit breaker, rate limit, X-PSU-IP) + parsing/normalización (cuentas, saldos, transacciones booked+pending). |
✅ Hecho |
| P3 | Persistencia (Prisma bank-reconciliation, RLS real vía withTenantContext) + sincronización: backfill atendido + incremental async (202+polling) + webhook + scheduler + reauth proactiva (hook+métrica). |
✅ Hecho |
| P4 | Documentos conciliables: puerto BILLING_DOCUMENTS_PORT a product (facturas de venta) + entidad de gastos ProductExpense + asignaciones N:M + estado de cobro/pago derivado. |
✅ Hecho |
| P4b | Verificación de titularidad (KYB, opcional): Verification API (ES), detrás de flag. | 🟡 Adaptador presente; flujo opcional |
| P5 | Motor de conciliación: CREDIT↔factura y DEBIT↔gasto; determinista + reglas + scoring; parcial/agrupado (N:M); idempotencia por normalised_provider_transaction_id (+ fingerprint); invariantes con bloqueo pesimista. |
✅ Hecho |
| P6 | Superficie REST + OpenAPI (11 endpoints; alimenta dashboard y Eve), auth por scope reconciliation:*; split-openapi + contract spec + openapi:diff en el gate. |
✅ Hecho |
| P7 | Endurecimiento (mutación, property-based incl. invariante agregada, seguridad/GDPR, aislamiento multi-tenant) + observabilidad (Prometheus) + precision/recall. Carga (L10) pendiente. | 🟡 Mayoría hecho; L10 pendiente |
Bloqueante de go-live (matizado): con el
auth dialog, un cliente no regulado puede operar (TrueLayer actúa de AISP). Solo eldirect bank auth(/v1/authuri) exige ser AISP propio. El MVP usaauth dialog→ no necesita licencia AISP, solo alta/contrato comercial. Ver §17.
1. Resumen ejecutivo
TrueLayer expone una Data API (AIS) PSD2 que permite a un usuario vincular sus cuentas bancarias (con SCA) y leer cuentas, identidad, saldos y transacciones. Sobre esos datos MultiFactu construye la conciliación bancaria: casar automáticamente los movimientos con las facturas de venta (cobros) y los gastos (pagos) de la organización.
1.1 De dónde salen los documentos a conciliar (fuente interna)
Aclaración clave (verificada en el repo). Las facturas no salen de las integraciones fiscales. Verifactu, TicketBAI y AEAT-modelos son tuberías de reporte que leen la factura y la declaran a Hacienda; no son su repositorio.
- Facturas de venta (ingresos): viven en
ProductInvoice(módulosrc/modules/product) —organizationId,customerId, importes en céntimos (totalGrossCents…),issueDate/dueDate,paymentMethod. Existe. ⚠️ Pero no tiene estado de cobro (solo estado fiscal: enviada/aceptada) → hay que derivarlo (ver §1.2 y §11). - Gastos / facturas de proveedor (gastos): no existe ningún dominio hoy. Para el alcance MVP (ingresos+gastos) se crea una nueva entidad
ProductExpenseen el móduloproduct, paralela aProductInvoice(proveedor, importe, fecha, vencimiento). Ver §10.1. - La conciliación NO importa esas tablas directamente. Las consume a través de un puerto
BILLING_DOCUMENTS_PORTimplementado por un adaptador que consultaproduct→ mantiene el módulo de conciliación desacoplado del dominio de facturación.
1.2 Estado de cobro/pago: proyección derivada, sin tocar la factura fiscal
El estado “cobrada/pagada/parcial” no se escribe sobre ProductInvoice (no contaminamos el documento fiscal). Se deriva en el módulo de conciliación a partir de las asignaciones (allocations): el estado de un documento = f(Σ asignaciones confirmadas vs total). Se expone vía API/evento para que la app/Eve y, en su caso, el módulo product lo muestren. Modular y reversible.
1.3 Alcance real: subsistemas
| Subsistema | Rol |
|---|---|
| Consentimiento y tokens | OAuth2 + SCA, refresh (30/90 días), cifrado en reposo, máquina de estados. |
| Sincronización | Backfill atendido + incremental async + webhooks; idempotente, reanudable, respeta límites. |
| Documentos conciliables | Puerto a product: facturas de venta + gastos (ProductExpense, nuevo). |
| Motor de conciliación | Valor diferencial: CREDIT↔venta, DEBIT↔gasto; reglas + scoring; asignaciones N:M. |
| Estado de cobro/pago | Proyección derivada de las asignaciones. |
| Superficie pública | REST + OpenAPI → dashboard y Eve (misma API). |
| Cumplimiento | PSD2 + GDPR; webhooks firmados; RLS. |
1.4 Productos TrueLayer: qué usamos, qué diferimos, qué descartamos
| Producto | Decisión | Motivo |
|---|---|---|
| Data API (AIS) | ✅ Núcleo del MVP | Es la conciliación. |
| Verification API | ✅ Incluido — opcional, flag (P4b) | KYB: titular del IBAN = organización (ES, beta). |
| Payments API v3 | 🟡 Adyacente futura | Cobros (pay-by-bank, SEPA Instant ES). Añade firma JWS + merchant account. |
| VRP / Bank on file | ❌ Fuera de alcance | Solo UK. |
| Signup+ | ❌ Fuera de alcance | Solo UK/FI. |
Detalle en §18.
1.5 Superficie: dashboard y Eve (decisión de peso diferida)
El núcleo (motor de matching) es determinista y auditable — maneja dinero, no puede depender de aciertos de una IA. La superficie REST + OpenAPI (Opción A, como BOE) alimenta ambas sin rehacer nada:
- Dashboard tradicional — ver/confirmar/editar/deshacer asignaciones (lo que una asesoría exige).
- Eve (modo agéntico) sobre la misma API — explicar por qué casa una sugerencia, resolver ambigüedades, “¿qué me queda por cobrar?”, auto-confirmar los matches de alta confianza.
Recomendación: dashboard como base fiable + Eve como capa de potencia encima. No hace falta decidir el peso ahora: el diseño (lógica en application/domain, superficies finas) deja ambas abiertas; se decide al tener la funcionalidad delante. GraphQL/MCP propios siguen diferidos. El MCP de TrueLayer (truelayer_mcp) es no oficial/solo Payments → no se usa.
1.6 Reversibilidad de proveedor
Proveedor aislado tras ACCOUNT_INFORMATION_PORT; el motor opera sobre el dominio normalizado. Permite multi-proveedor (GoCardless BAD, Tink) sin tocar el dominio. TrueLayer es la elección del MVP (familiaridad + normalised_provider_transaction_id estable). Plan B documentado.
2. Rúbrica 10/10
| # | Dimensión | Peso | Criterio de “10” |
|---|---|---|---|
| 1 | Arquitectura en capas | 10% | api → application → domain ← infrastructure; arch:check limpio; proveedor y product tras puertos. |
| 2 | Consentimiento y tokens | 10% | auth dialog + token exchange + refresh (30/90 días) + máquina de estados + tokens cifrados; reauth proactiva. |
| 3 | Cobertura funcional Data API | 6% | Cuentas, identidad, saldos, transacciones booked+pending; opcional direct_debits/standing_orders. |
| 4 | Resiliencia saliente | 8% | Timeout + retry(jitter) + breaker + budget + rate limit + X-PSU-IP; mapeo invalid_grant/access_denied/sca_exceeded. |
| 5 | Sincronización (backfill + incremental) | 10% | Backfill atendido + incremental async + webhooks firmados + polling fallback; dedup; reanudable; sin duplicados. |
| 6 | Motor de conciliación (ingresos+gastos, N:M) | 15% | CREDIT↔venta y DEBIT↔gasto; determinista + reglas + scoring; parcial/agrupado; auditable; precision/recall. |
| 7 | Documentos y estado de cobro/pago | 8% | Puerto a product; entidad de gastos; estado derivado de asignaciones N:M; sin mutar el documento fiscal. |
| 8 | Persistencia y modelo de datos | 6% | Schema bank-reconciliation con RLS, índices, Decimal/céntimos, migración reproducible. |
| 9 | Contrato (REST + OpenAPI) | 6% | Documentado, scopes, baseline sin drift; sirve dashboard y Eve. |
| 10 | Seguridad y cumplimiento | 11% | Cifrado de tokens, RLS, RFC 7807, GDPR, webhooks firmados, secretos fuera del repo. |
| 11 | Testing multicapa (L01–L14) | 10% | Incl. L13 aislamiento multi-tenant y L05 con MSW; mock users de sandbox. |
El “10/10” se alcanza con todas las dimensiones en verde y reconciliation:quality:gate (§16) pasando en CI.
3. Alcance — endpoints upstream (Data API V1)
Catálogo canónico (entregable P0): src/modules/bank-reconciliation/truelayer-api-endpoints.yml. Base: https://api.truelayer.com/data/v1 (sandbox …-sandbox.com); auth https://auth.truelayer.com.
| Familia | Endpoint | Scopes | Notas |
|---|---|---|---|
| Auth (dialog) | GET auth.truelayer.com/?response_type=code&client_id&redirect_uri&scope&state |
— | SCA; provider_id/providers/PKCE opcional |
| Auth (token) | POST auth.truelayer.com/connect/token |
— | authorization_code → access+refresh; refresh_token |
| Identidad | GET /info |
info |
titular (full_name) |
| Cuentas | GET /accounts · /accounts/{id} |
accounts |
IBAN, divisa, tipo |
| Saldos | GET /accounts/{id}/balance |
accounts+balance |
current/available |
| Transacciones | GET /accounts/{id}/transactions (from/to) |
accounts+transactions |
booked; normalised_provider_transaction_id |
| Transacciones | GET /accounts/{id}/pending_transactions |
accounts+transactions |
pending |
| Domiciliaciones | GET /accounts/{id}/direct_debits |
accounts+direct_debits |
recibos (gastos) |
| Órdenes | GET /accounts/{id}/standing_orders |
accounts+standing_orders |
pagos periódicos |
| Verificación | POST /v1/verify (titular) · /v3/account-holder-verifications |
verification |
KYB; /v1 sin firma; /v3 con firma → payouts |
| Proveedores | GET auth.truelayer.com/api/providers (público) |
— | cobertura (country=es) |
| Async | ?async=true&webhook_uri=… |
— | 202 + results_uri + webhook |
| Extend | POST /connections/extend |
— | solo UK (ES: refresh o re-auth) |
Cobertura ES verificada (producción, 2026-06-24): 7 proveedores — Santander, BBVA, CaixaBank, Sabadell, ING (xs2a-redsys-*) + Revolut, Wise. Faltan Bankinter, Kutxabank, Abanca, Unicaja, Ibercaja, Openbank y cajas → refuerza el plan B multi-proveedor.
4. Principios de arquitectura
CLAUDE.md raíz + src/modules/CLAUDE.md; se replica boe (resiliencia, config Zod, caché, scheduler, OpenAPI) y patrones transaccionales de aeat-modelos:
- Módulo en capas, stateful:
api → application → domain ← infrastructure. - Dos puertos hacia fuera del dominio:
ACCOUNT_INFORMATION_PORT(proveedor, TrueLayer) yBILLING_DOCUMENTS_PORT(documentos:product). El motor no conoce ni TrueLayer ni las tablas deproduct. - Controllers/resolvers/tools finos; lógica en
application/domain. - Config Zod (
ReconciliationConfigService); errores RFC 7807 (AllExceptionsFilter). - Secretos fuera del repo (skill
secrets-management). - Data API sin firma (solo Bearer); firma JWS (ES512) solo Payments → futura fase de cobros (§18).
5. Estructura de carpetas propuesta
src/modules/bank-reconciliation/
├── truelayer-api-endpoints.yml
├── bank-reconciliation.module.ts
├── bank-reconciliation.config.ts # Zod + .spec.ts
│
├── api/
│ ├── controllers/ # connections, accounts, transactions, reconciliation
│ ├── dto/ # class-validator + @ApiProperty
│ └── mappers/
│
├── application/
│ ├── use-cases/
│ │ ├── create-connection.use-case.ts
│ │ ├── complete-connection.use-case.ts # callback: code -> tokens (cifrados)
│ │ ├── refresh-connection.use-case.ts
│ │ ├── backfill-account.use-case.ts # primer import atendido (tras SCA)
│ │ ├── sync-transactions.use-case.ts # incremental idempotente (async)
│ │ ├── suggest-allocations.use-case.ts # motor: candidatos movimiento<->documento
│ │ ├── confirm-allocation.use-case.ts # asignación confirmada (parcial/total)
│ │ ├── revoke-connection.use-case.ts # revocación + borrado (GDPR)
│ │ └── verify-account-holder.use-case.ts# KYB opcional (flag)
│ └── services/
│ ├── reconciliation-engine.service.ts # reglas + scoring (ingresos y gastos)
│ ├── settlement-projection.service.ts # estado cobro/pago = f(asignaciones)
│ ├── token-cipher.service.ts # AES-256-GCM
│ ├── reconciliation-cache.service.ts
│ ├── circuit-breaker.service.ts
│ └── reconciliation-metrics.service.ts
│
├── domain/
│ ├── ports/
│ │ ├── account-information.port.ts # ACCOUNT_INFORMATION_PORT (TrueLayer)
│ │ ├── billing-documents.port.ts # BILLING_DOCUMENTS_PORT (product: ventas + gastos)
│ │ ├── connection.repository.port.ts
│ │ ├── transaction.repository.port.ts
│ │ ├── allocation.repository.port.ts
│ │ ├── token-vault.port.ts
│ │ └── account-verification.port.ts
│ ├── value-objects/ # iban.vo, money.vo, transaction-fingerprint.vo
│ ├── state/connection-state.ts
│ └── types/ # Connection, Account, Transaction, BillingDocument, Allocation, MatchCandidate
│
└── infrastructure/
├── adapters/
│ ├── truelayer-data.adapter.ts # ACCOUNT_INFORMATION_PORT (Bearer, sin firma)
│ ├── truelayer-auth.adapter.ts # OAuth2
│ ├── truelayer-verification.adapter.ts# KYB (opcional)
│ └── product-billing-documents.adapter.ts # BILLING_DOCUMENTS_PORT -> módulo product
├── parsers/ # JSON TL -> dominio (number->Decimal seguro)
├── cache/ · crypto/token-vault.adapter.ts
├── db/ # connection / transaction / allocation repos (RLS)
├── webhooks/truelayer-webhook.controller.ts # verificación Tl-Signature (JWS) + encola sync
└── schedulers/transaction-sync.scheduler.ts # @nestjs/schedule + reauth proactiva
6. Modelo de dominio
Iban,Money(céntimos/Decimal; la Data API daamountcomonumber→number→string→Decimal),TransactionFingerprint(preferirnormalised_provider_transaction_id; fallback a hash porque no es obligatorio).BillingDocument(normalizado desdeproduct):kind(sale_invoice/expense),id,organizationId,total,currency,issueDate,dueDate,counterparty,references(nº factura/NIF),outstanding(pendiente).Allocation(asignación, N:M): liga unbank_transactioncon unBillingDocumentpor un importe (permite parcial). Un movimiento → varias asignaciones; un documento → varias asignaciones. Estado:suggested/confirmed/rejected.MatchCandidate: documento + score + motivo (entrada del motor).- Estado de cobro/pago del documento = derivado:
pending/partial/settledsegúnΣ asignaciones confirmadas vs total.
Máquina de estados de la conexión:
PENDING ─(SCA ok)→ ACTIVE ─(90d / invalid_grant / sca_exceeded)→ EXPIRED ─(reauth)→ ACTIVE
│ │ │
└─(SCA fallo)→ ERROR └─(revoke / GDPR)→ REVOKED ←──────────────────┘
Invariantes (testeables): (a) re-sincronizar el mismo rango no duplica (fingerprint); (b) la suma de asignaciones confirmadas de un documento nunca supera su total.
7. Autenticación, consentimiento y tokens (P1)
auth dialog(no regulados):create-connectiongenera el link aauth.truelayer.comconinfo accounts balance transactions offline_access(+direct_debits/standing_orderssi se usan) ystate. No requiere ser AISP. No iframe (CSP) → redirect/ventana.- Token exchange:
POST /connect/token(authorization_code,code5 min) →access_token(1 h) +refresh_token(sioffline_access). - Cifrado en reposo:
TokenVaultAdapter(AES-256-GCM); clave en secretos; tabla separada. - Refresh: usar en los primeros 30 días, reutilizable hasta 90.
400 invalid_grant/403 access_denied→EXPIRED. - 90 días (PSD2):
extendsolo UK; en ES, refresh y, al caducar, repetirauth dialog. Reauth proactiva (reauthDays). - Revocación:
revoke-connectionborra tokens/datos (GDPR, §9).
8. Sincronización: backfill + incremental (P3)
8.1 Backfill (primer import) — decisión: atendido
- Se dispara justo tras el SCA, con el usuario presente → enviamos su
X-PSU-IP→ no cuenta contra el límite de 4/día. Trae el histórico disponible (ventana máxima del banco) en modoasync(?async=true&webhook_uri=…→202+results_uri) por ventanas de fecha. - Idempotente y reanudable (fingerprint + upsert). Estado por cuenta:
backfill_status(pending/running/done) +backfilled_from.
8.2 Incremental — desatendido
- Scheduler diario (patrón
boe-snapshot-refresh.scheduler.ts) + webhook async + on-demand. Cursor por últimobookingDatecon solape; laspendingse reemplazan al pasar abooked. - Guardar la
X-PSU-IPde la última sesión y enviarla; respetarprovider_too_many_requests/provider_request_limit_exceeded(429). Polling deresults_uricomo fallback (TrueLayer reintenta el webhook ~6 veces/24 h). - Caché: respetar
Cache-Control/Last-Modified/update_timestamp.
9. Seguridad y cumplimiento (P7)
- Data API sin firma (Bearer). Firma JWS ES512 solo Payments (§18).
- Webhooks firmados: verificar
Tl-Signature(JWS) +X-TL-Webhook-Timestamp; idempotencia porevent_id. - Cifrado de tokens (AES-GCM) + TLS; secretos fuera de repo/BD.
- GDPR: minimización de scopes, retención + purga, revocación/supresión, sin PII/secretos en logs.
- Multitenancy: RLS en
bank-reconciliation; el adaptador aproductfiltra pororganizationId. - KYB (Verification API, ES) — opcional (P4b):
/v1/verifysobre el consentimiento AIS (sin firma), detrás del flagreconciliationVerifyAccountHolder. No bloquea la conciliación.
10. Persistencia (Prisma)
10.1 Cambios en el módulo product (dominio de facturación)
ProductExpense(NUEVO): gasto/factura de proveedor —id,organizationId,supplier,issueDate,dueDate,currency,totalGrossCents,references,metadata. Paralelo aProductInvoice. (En el futuro alimentará reporte fiscal de gastos — fuera de alcance ahora.)ProductInvoiceno se modifica. El estado de cobro se deriva en conciliación (§1.2).
10.2 Schema bank-reconciliation (RLS, @@schema, Timestamptz, importes en céntimos Int/Decimal)
bank_connection(+bank_connection_tokenseparada con*_enc,iv,last_psu_ip).bank_account(backfill_status,backfilled_from).account_balance.bank_transaction:fingerprint @unique,amount,currency,booking_date,value_date,description,merchant_name?,category,direction(CREDIT/DEBIT),status(booked/pending).reconciliation_allocation(N:M):id,organization_id,transaction_id,document_kind(sale_invoice/expense),document_id,amount_cents,score,state(suggested/confirmed/rejected),confirmed_by?,confirmed_at?; índices(transaction_id),(document_kind, document_id),(organization_id, state).
11. Motor de conciliación (P5 — corazón del producto)
reconciliation-engine.service.ts produce MatchCandidate[] por movimiento, según la dirección:
CREDIT(entrada) → facturas de venta pendientes (ProductInvoiceconoutstanding > 0).DEBIT(salida) → gastos pendientes (ProductExpenseconoutstanding > 0).
Capas: (1) determinista (importe + divisa + ventana de fechas + referencia exacta nº factura/NIF en description); (2) reglas (tolerancia de fechas, parcial y agrupado vía asignaciones, normalización de texto, reglas por organización); (3) scoring (score ∈ [0,1]; sobre umbral = sugerencia; el usuario confirma/rechaza → realimenta).
- N:M real: una sugerencia puede proponer varias asignaciones (un movimiento que paga 3 facturas; o una factura cubierta por 2 transferencias). El
settlement-projection.servicerecalculapending/partial/settled. - ⚠️ Sin enriquecimiento en ES:
merchant_name/classificationsolo UK/IE/FR → el motor se apoya endescription(crudo) +amount+transaction_category+ fecha. Invertir en parsing del concepto (NIF, nº factura, nombre de cliente/proveedor). Tink superior aquí (plan B). - Auditable/reversible: cada asignación guarda motivo, score e importe;
confirm/reject/undo. Métrica: precision/recall sobre fixtures etiquetados (gate, §15).
12. Superficie pública (REST + OpenAPI) — dashboard y Eve
Única superficie activa REST + OpenAPI; sirve dashboard y Eve (§1.5). GraphQL/MCP diferidos.
| Contrato REST | Hace | Scope |
|---|---|---|
POST /reconciliation/connections |
inicia vínculo → auth link |
reconciliation:write |
GET /reconciliation/connections |
conexiones + estado/expiración | reconciliation:read |
DELETE /reconciliation/connections/{id} |
revoca + borra (GDPR) | reconciliation:write |
GET /reconciliation/accounts |
cuentas + saldos + backfill_status |
reconciliation:read |
GET /reconciliation/transactions |
movimientos (filtros, paginado propio) | reconciliation:read |
GET /reconciliation/suggestions |
sugerencias de asignación (ingresos y gastos) | reconciliation:read |
POST /reconciliation/allocations · DELETE …/{id} |
confirma / deshace asignación (parcial/total) | reconciliation:write |
GET /reconciliation/documents/{kind}/{id}/settlement |
estado de cobro/pago derivado | reconciliation:read |
POST /reconciliation/sync |
sincroniza ahora | reconciliation:write |
POST /reconciliation/webhooks/truelayer |
webhooks (firma verificada) | — (público, firmado) |
Controllers finos + DTOs; respuesta JSON normalizada; RestAuthGuard + @RequireScopes. OpenAPI con @nestjs/swagger; bank-reconciliation en scripts/split-openapi.ts + baseline + openapi:diff. Handoff a Eve en docs-official/TrueLayer/README.md (inputSchema zod desde el OpenAPI).
13. Configuración (env vars)
ReconciliationConfigService (Zod), prefijos RECONCILIATION_/TRUELAYER_:
| Variable | Default | Uso |
|---|---|---|
reconciliationEnabled |
false |
habilita el módulo |
trueLayerAuthBaseUrl / trueLayerDataBaseUrl |
prod URLs | OAuth2 / Data API |
trueLayerClientId / trueLayerClientSecret |
— (secreto) | OAuth2 client |
trueLayerRedirectUri |
— | callback (registrado en Console) |
trueLayerEnvironment |
sandbox |
sandbox/live |
reconciliationTokenEncKey |
— (secreto) | clave AES-256-GCM |
reconciliationWebhookPublicKey |
— | verificación Tl-Signature |
reconciliationConsentReauthDays |
7 |
reauth proactiva |
reconciliationVerifyAccountHolder |
false |
KYB opcional (Verification API) |
reconciliationTimeoutMs / reconciliationRetries / reconciliationRatePerSecond |
20000/2/5 |
resiliencia |
reconciliationCacheBackend |
auto |
memory/redis/auto |
(futuro Payments) trueLayerSigningKid / trueLayerPrivateKeyPem |
— (secreto) | firma JWS — solo si se activa Payments |
Validación de arranque si reconciliationEnabled=true. .env.example sin secretos (entregable P0).
14. Observabilidad (P7 / L14)
Prometheus (prom-client): sync (backfill/incremental), matching (precision/recall), estado de conexiones, webhooks. Logs nestjs-pino con correlationId; propagar X-Client-Correlation-Id, registrar X-TL-Correlation-Id. OTel. Client tracking/Debug ID (conversión del consentimiento, retención 60 días). SLO: p95 < 500 ms con caché caliente; error upstream < 1%; éxito de sync > 99%.
15. Estrategia de testing por capa (núcleo del 10/10)
Stack estándar L01–L14, local-first. Cobertura ≥ 85/80; 95/90 en motor, normalizadores y value-objects.
| Capa | Herramienta(s) | Aplicación |
|---|---|---|
| L01 Estático | TypeScript + Ultracite (Biome) | 0 errores en src/modules/bank-reconciliation. |
| L02 Unit | Jest | Value objects, normalizadores, motor (ingresos+gastos, N:M), token-cipher, máquina de estados, settlement-projection, use-cases con puertos mockeados. |
| L03 Mutación | Stryker | Motor, value-objects, normalizadores, token-cipher, proyección de estado. |
| L04 Integración | Testcontainers + Jest | Postgres (repos, asignaciones, RLS); Redis (caché/rate-limit). |
| L05 Contrato | jest-openapi + oasdiff · MSW (TrueLayer) | OpenAPI sin breaking; MSW mockea la Data API (incl. 429/sca_exceeded/202+webhook). |
| L06 Bootstrap DI | @nestjs/testing | El módulo resuelve puertos (ACCOUNT_INFORMATION_PORT, BILLING_DOCUMENTS_PORT, TOKEN_VAULT_PORT…). |
| L07 Arquitectura | dependency-cruiser | TrueLayer y product solo tras puertos; el motor no los importa. |
| L08 Property-based | fast-check | Idempotencia de sync; Money sin pérdida; invariante “asignaciones ≤ total”; Iban/fingerprint. |
| L09 E2E | supertest (+ Testcontainers) | connect→backfill→sync→suggest→confirm (parcial/agrupado); webhook firmado; fetch estilo Eve. |
| L10 Carga | K6 | SLOs de lectura con caché caliente. |
| L11 Microbench | tinybench | Hot path del matching N×M. |
| L12 Seguridad — scanners | GitLab Ultimate | Secret Detection (tokens); DAST/API Fuzzing. Mirror de GitLab. |
| L13 Authz/aislamiento | Jest (*.security.spec) |
CRÍTICO: una organización no ve datos de otra (RLS + matriz authz); el adaptador a product no fuga cross-tenant. |
| L14 Observabilidad | Prometheus + Grafana/Alertmanager | Métricas/alertas (consentimiento por caducar, fallos de sync, 429). |
Fixtures/sandbox: test/fixtures/truelayer/ (accounts/balance/transactions booked+pending/info/providers) + mock users (john/doe, john/eternal, error.*). Set etiquetado movimiento↔documento para precision/recall.
Local-first: pre-commit (L01 + unit afectados) y pre-push (unit+cobertura + L04 + L06 + L07 + L05), coste 0; L03/L09/L10/L11 local/nightly; L12 en GitLab; CI-GitHub re-ejecuta como required checks. SLOs K6 (L10) y lista de journeys E2E (L09): a cerrar en P7.
16. Quality gate del módulo
"reconciliation:test:unit": "jest --testPathPatterns=src/modules/bank-reconciliation",
"reconciliation:test:security": "jest --testPathPatterns=src/modules/bank-reconciliation/.*security",
"reconciliation:test:e2e": "jest --config ./test/jest-e2e.json --testPathPatterns=reconciliation",
"reconciliation:test:int": "jest --testPathPatterns=src/modules/bank-reconciliation/.*integration",
"reconciliation:openapi:contract": "jest --runInBand --testPathPatterns=src/modules/bank-reconciliation/.*openapi-contract",
"reconciliation:quality:gate": "pnpm typecheck && ultracite check src/modules/bank-reconciliation && pnpm arch:check && pnpm reconciliation:test:unit && pnpm reconciliation:test:security && pnpm reconciliation:test:e2e && pnpm openapi:diff"
Cubre L01/L02/L07/L13/L09/L05. L04 en pre-push/CI con infra; L03/L10/L11 local/nightly; L12 en GitLab. Sin gates de MCP/GraphQL (diferidos).
17. Regulación y go-live
auth dialog(MVP): no requiere ser AISP (TrueLayer es el AISP). Requisitos: alta/contrato con TrueLayer, due diligence, pantallas aprobadas. No bloquea sandbox (P0–P7 dev).direct bank auth(futuro): exige ser AISP propio. Solo si se quiere control total de UX.
Acción: confirmar con TrueLayer las condiciones del
auth dialogpara producción ES en paralelo a P0–P6.
18. Add-ons y productos adyacentes
- Payments API v3 — cobros (adyacente futura). Pay-by-bank, payment links, merchant accounts; SEPA Instant ES. Complementa: la Data API responde “¿qué movimientos tiene el cliente?”; Payments, “¿me han pagado?”. Reutiliza infra/webhooks pero añade firma JWS ES512 + merchant account (~2-3 sprints). Diferenciador vs Holded. Diseñar para no cerrar la puerta (puerto separado, webhooks compartidos).
- Verification API — KYB (✅ INCLUIDO, P4b; ES beta).
/v1/verify(sin firma) detrás de flag. Confirmar activación del scopeverificationy host/path en consola. - VRP / Bank on file y Signup+ — ❌ fuera de alcance (UK / UK-FI).
19. Plan de fases
Commits sobre
TrueLayer, gate verde por fase. P0–P6 entregan conciliación productiva en sandbox; P7 eleva a 10/10 y habilita producción.
P0 — Cimientos
Módulo en capas, config Zod, truelayer-api-endpoints.yml, dominio (tipos, value objects, puertos, máquina de estados), .env.example, registro en app.module.ts. DoD: typecheck && arch:check && reconciliation:test:unit verde.
P1 — Auth, consentimiento y tokens cifrados
truelayer-auth.adapter, token-vault.adapter, use-cases connect/complete/refresh/revoke, reglas 30/90 días. DoD: OAuth2+SCA en sandbox; tokens cifrados; expiración detectada. (skills: oauth, secrets-management)
P2 — Transporte Data API + normalización
truelayer-data.adapter (resiliencia + X-PSU-IP, sin firma); parsers (+number→Decimal). DoD: datos normalizados a dominio único.
P3 — Persistencia + sincronización (backfill + incremental)
Schema bank-reconciliation (RLS), repos, backfill-account (atendido, async, reanudable) + sync-transactions (incremental) + scheduler + webhook firmado. DoD: backfill reanudable; dos syncs ⇒ sin duplicados; webhook verificado dispara sync. (skills: prisma-table-design, postgresql-table-design, gdpr-data-handling)
P4 — Documentos conciliables (ventas + gastos)
BILLING_DOCUMENTS_PORT + product-billing-documents.adapter; nueva entidad ProductExpense en product + migración; modelo reconciliation_allocation (N:M) + settlement-projection. DoD: el motor puede leer ventas y gastos pendientes; estado de cobro/pago se deriva correctamente; invariante “asignaciones ≤ total”. (skills: prisma-table-design, api-design-principles)
P4b — Verificación de titularidad (KYB, opcional)
truelayer-verification.adapter + verify-account-holder.use-case, detrás del flag, vía /v1/verify. DoD: con flag activo, titular no coincidente se marca; con flag inactivo, no afecta. (skill: auth-implementation-patterns)
P5 — Motor de conciliación
reconciliation-engine (CREDIT↔venta, DEBIT↔gasto; determinista+reglas+scoring; parcial/agrupado), use-cases suggest/confirm/reject. DoD: precision/recall medidos; asignaciones auditables/reversibles. (skill incl. parsing de concepto ES)
P6 — Superficie REST + OpenAPI (dashboard + Eve)
Controllers/DTOs/mappers, auth/scopes, Swagger; split + baseline. DoD: reconciliation:test:e2e y openapi:diff verdes; consumible por dashboard y por Eve con fetch. (skills: nestjs-best-practices, openapi-spec-generation)
P7 — Endurecimiento, observabilidad, rendimiento y producción
Stryker + property-based ampliado + GDPR + L13; Prometheus/OTel/Client tracking; K6 + microbench; runbook. Cierre del alta/contrato con TrueLayer. DoD: reconciliation:quality:gate verde en CI; SLO medido; vía libre comercial.
20. Riesgos y mitigaciones
| Riesgo | Impacto | Mitigación |
|---|---|---|
| Dominio de gastos no existe (hay que crearlo) | Amplía alcance | ProductExpense mínima en product; captura de gastos puede crecer en fases posteriores. |
| Consentimiento caduca a 90 días (sin extend ES) | Pérdida de acceso | Reauth proactiva; refresh 30/90; máquina de estados. |
4 llamadas/día desatendidas sin X-PSU-IP |
Sync bloqueada | Backfill atendido; guardar IP; respetar 429. |
| Cobertura ES (7 bancos) | Clientes sin su banco | Plan B multi-proveedor; avisar en el alta. |
| Sin enriquecimiento en ES | Peor matching | Parsing del description (NIF/nº factura); Tink plan B. |
| Pagos parciales/agrupados | Conciliación incompleta | Modelo N:M de asignaciones + invariante “≤ total”. |
normalised_* ausente |
Duplicados | TransactionFingerprint con fallback; @unique. |
| Filtración de tokens | Crítico | AES-GCM, secretos fuera de repo, RLS, sin logs. |
| Webhooks falsificados | Datos corruptos | Verificar Tl-Signature + timestamp; idempotencia event_id. |
amount como float |
Céntimos | number→Decimal seguro; property-based. |
Acoplamiento a product |
Coste de cambio | BILLING_DOCUMENTS_PORT; el motor no importa product. |
| GDPR (datos sensibles) | Legal | Minimización, retención, supresión, cifrado. |
21. Definition of Done (checklist 10/10)
- Módulo en capas;
arch:checkverde; TrueLayer yproducttras puertos. -
auth dialog+ token exchange + refresh (30/90) + reauth proactiva; tokens cifrados. - Data API cubierta (cuentas, identidad, saldos, transacciones
booked+pending) normalizada. - Resiliencia (timeout/retry/jitter/breaker/budget/rate-limit/
X-PSU-IP) + mapeo de errores. - Backfill atendido + sync incremental idempotente (async + webhooks firmados + polling); sin duplicados.
- Documentos: puerto a
product;ProductExpensecreada; estado de cobro/pago derivado de asignaciones N:M. - Motor ingresos+gastos (CREDIT↔venta, DEBIT↔gasto; parcial/agrupado), auditable/reversible; precision/recall medidos.
- Schema
bank-reconciliationcon RLS, índices, céntimos/Decimal, migración reproducible. - Contrato REST + OpenAPI (dashboard y Eve); baseline sin drift.
- Seguridad/GDPR: cifrado, RLS, retención, supresión, webhooks verificados, secretos fuera del repo.
- KYB opcional (Verification API) tras flag; desactivado no afecta.
- Observabilidad: Prometheus, logs con correlationId, OTel, Client tracking.
- Tests L01–L14 (incl. L13 aislamiento multi-tenant y L05 con MSW) con mock users de sandbox.
- Cobertura ≥ 85/80 global y ≥ 95/90 en motor/normalizadores/value-objects.
-
reconciliation:quality:gateverde en CI. - Producción: alta/contrato con TrueLayer (modelo
auth dialog) resuelto. - Documentación: este roadmap,
truelayer-api-endpoints.yml,docs-official/TrueLayer/, runbook.
22. Referencias
Documentación oficial (local): docs-official/TrueLayer/ — Data API (Documentation/Data API/**, ApiReference/Data API V1/**), Auth/seguridad (ApiReference/Authentication server/**, Documentation/Get Started/Security best practices.md, firma en Documentation/Payments/Request authentication/**), adyacentes (Payments API V3, Add-on verification + Verification API, Bank on file UK, Signup+ UK/FI).
Repo:
- Fuente de documentos:
src/modules/product/**(ProductInvoice;ProductExpensea crear),packages/prisma/schema.prisma(modeloOrganization,ProductInvoice). - Patrones:
src/modules/boe/**+docs/boe-integration-roadmap.md; transaccionalsrc/modules/aeat-modelos/**. - Core:
src/core/config/*,src/core/prisma/*(RLS),src/core/auth/*,src/core/error/all-exception-filter.ts,src/core/metrics,src/core/otel. - OpenAPI:
scripts/split-openapi.ts,scripts/openapi/check-breaking.ts,test/contract/openapi.baseline.json. Testing:test/support/testcontainers/*,test/verifactu.e2e-spec.ts. - Consumidores: dashboard (app) y Eve (
github.com/ADP-DIGITEK/eve.multifactu.com,agent/tools/*.tscondefineTool+zod). - Skills:
oauth,secrets-management,gdpr-data-handling,prisma-table-design,postgresql-table-design,nestjs-best-practices,openapi-spec-generation,api-design-principles,auth-implementation-patterns. - Diferidos — GraphQL:
src/graphql/integrations/verifactu/**; MCP:src/mcp/tools/verifactu.ts.
23. Cuestiones abiertas — por cerrar
Decisiones aún no tomadas. No bloquean arrancar P0, pero deben cerrarse antes de la fase indicada. Se listan para ser conscientes y no descubrirlas tarde.
| # | Cuestión | Tipo | Bloquea | Recomendación / nota |
|---|---|---|---|---|
| 1 | Captura de gastos: ¿cómo se registran los ProductExpense? (manual, OCR de factura de proveedor, import CSV/banco, e-mail) |
Producto | P4 (gastos) | Sin gastos registrados no hay contra qué conciliar DEBIT. MVP mínimo: alta manual; OCR/import como mejora posterior. |
| 2 | Pricing/coste TrueLayer (por conexión activa / por llamada / por usuario) | Negocio | Go-live (P7) | Pedir condiciones a TrueLayer; afecta a unit economics y al diseño de frecuencia de sync. |
| 3 | Estrategia de rollout (beta cerrada → GA; por organización) | Producto | P6/P7 | Más allá del kill-switch reconciliationEnabled; feature flag por organización + cohorte beta. |
| 4 | Operativo TrueLayer: activación del scope verification (beta ES), condiciones del auth dialog en producción ES, host/path exactos de Verification |
Operativo | P4b / Go-live | Confirmar en consola/soporte en paralelo a P0–P6. |
| 5 | Frontend/UX y peso dashboard vs Eve | Producto | (diferido) | Decidir al tener la funcionalidad delante; el backend ya sirve ambos (§1.5). No bloquea. |
| 6 | Política de auto-confirmación de asignaciones de alta confianza (umbral de score, ¿requiere validación humana?) | Producto | P5 | Empezar conservador: todo sugerido requiere confirmación; auto-confirmar solo match determinista exacto si el cliente lo activa. |
| 7 | Retención de datos (GDPR): cuánto tiempo guardamos transacciones tras revocar/cancelar | Legal | P7 | Definir política + purga programada; alinear con asesoría legal. |
| 8 | Cobertura de bancos ES de la base real de usuarios (7 soportados; falta cola larga) | Producto/negocio | Go-live | Encuestar bancos de los clientes objetivo; activar plan B (GoCardless/Tink) si falta cobertura. |
| 9 | Multidivisa: hoy EUR; ¿soportar cuentas no-EUR? (Revolut/Wise multi-divisa) | Producto | (futuro) | MVP solo EUR; Money ya lleva divisa, así que es extensible sin rediseño. |
| 10 | SLOs de K6 (L10) y lista de journeys E2E (L09) | Técnico | P7 | Cerrar umbrales p95/RPS/error-rate y el set de recorridos críticos. |