Ir al contenido

ADR-009 · Cashpaya como primer cliente DJ — modelo de integración

CampoValor
StatusAccepted
Date2026-06-05 (Draft) → 2026-06-05 (Accepted)
Authorstech-lead agent + Jonhjar Guerrero
Supersedes
Superseded by

Cashpaya — la fintech CO que jguerrero ya opera en producción legacy con ~50 usuarios reales — pasa a ser el primer tenant productivo de Digital Jungle. La pregunta arquitectónica de fondo no es técnica sino de frontera de producto: ¿qué hace DJ y qué retiene cashpaya?

El gap analysis cierra esa pregunta con datos (docs/conceptos/integracion-cashpaya-gap-analysis.md, secciones 1.6 + “Decisiones confirmadas por jguerrero 2026-06-05”):

  • De 73 funcionalidades inventariadas, 8 son 🔵 Out-of-scope para DJ y todas son financieras: saldos, transferencias P2P, recargas, comisiones, integración bancaria/PSE, pasarela de pago, QR. Decisión jguerrero #1: “cashpaya maneja dinero real e integra Mercado Pago → la lógica financiera queda en cashpaya. DJ no la replica”.
  • De las otras 65, 27 están equivalentes en DJ, 21 son parciales y 12 no existen — distribuidas en identidad, KYC, SARLAFT, firma electrónica y wallet SSI. Todo eso es lo que DJ provee como servicio.
  • Decisión jguerrero #4: “los componentes deben servir para 5-10 tenants sin re-trabajo — nada cashpaya-specific en módulos DJ”.

La consecuencia es clara: cashpaya no es un fork ni una variante de DJ. Es un cliente que consume DJ vía REST, igual que como mañana lo hará otro fintech, una EPS o un operador de telcos. Si en algún módulo de DJ aparece la palabra cashpaya hardcoded, hicimos algo mal.

Este ADR formaliza la frontera para que el resto de Waves (A → D) tenga una referencia única sobre qué endpoint corresponde a qué dominio y qué credencial autentica qué llamada.

Vamos a tratar cashpaya como un tenant más de DJ, sin asimetrías de código. Específicamente:

DominioOwnerNotas
Identidad legal (cédula, OCR, hash de doc)DJ — bmonkeypersiste identity_subjects, RLS por tenant
KYC orchestration multi-stepDJ — bmonkeyflow_engine, sesiones con cursor en jsonb
Biometría (liveness + face match + face enroll)DJ — bmonkey + services/arcfacearcface default (decisión #1), no Rekognition Collection
Screening SARLAFT (PEP + sanctions)DJ — bhawkreglas versionadas en validations
Firma electrónica de docsDJ — bsealKMS tenant + circuits multi-firmante
Wallet SSI + VCs + passkeysDJ — bmonkey (global)invisible al usuario hasta uso cross-tenant
Saldo de cuenta cashpayaCashpayaclientes.tx_realizadas queda en cpy.*
Transferencias P2P / depósitos / retirosCashpayatoda la lógica fiat
Mercado Pago marketplace cashpayaCashpayadistinta de ADR-010 (que es topup Açaí)
Comisiones + integración bancaria + PSE + QRCashpayaOOS por diseño
Créditos Açaí (cuota de uso de DJ)DJ — platformcashpaya compra Açaí vía MP topup público (ADR-010)

Cashpaya consume DJ por dos canales distintos, cada uno con su credencial:

El backend Fiber de cashpaya (mientras siga vivo en Wave A→D) o cualquier servicio futuro que reemplace a Fiber, llama a DJ con la API key del tenant cashpaya:

POST /v1/flows/sessions (bmonkey)
POST /v1/screenings/dry-run (bhawk)
POST /v1/signatures (bseal)
POST /v1/acai/topup/checkout (platform — ver ADR-010)

Header obligatorio: X-API-Key: dj_live_<...>. Scopes mínimos al issue de la key (ADR-004):

  • bmonkey:flows:read, bmonkey:flows:write, bmonkey:sessions:create, bmonkey:sessions:read
  • bhawk:screenings:read, bhawk:screenings:write
  • bseal:signatures:read, bseal:signatures:write
  • acai:write (para topup; opcional acai:read para balance)

Sin *. Sin scope admin. Sin acceso cross-tenant.

2.b · OIDC end-user con Authorization: Bearer

Sección titulada «2.b · OIDC end-user con Authorization: Bearer»

Cuando un usuario final de cashpaya se loguea en la app cashpaya y ésta quiere actuar en nombre del usuario contra DJ — por ejemplo, para que el usuario consulte su wallet o consienta una presentación — cashpaya actúa como RP (Relying Party) del IdP nativo de bmonkey (implementado en Fase 3, ADR-003 / sign-in-with-bjungle).

Flujo:

[Usuario en app cashpaya]
│ click "Iniciar sesión con bjungle"
[bmonkey OIDC IdP] ── PKCE + nonce ──▶ id_token + access_token
[Cashpaya guarda access_token del usuario]
│ Authorization: Bearer <user_access_token>
[DJ wallet endpoints — /v1/wallet/me/*]

X-API-Key y Authorization: Bearer no se mezclan en la misma request. Si la operación es “en nombre del usuario” → Bearer. Si la operación es “del tenant sobre sus subjects” → X-API-Key.

identity_subjects, face_patterns, verifiable_credentials, wallet_passkeys, hashes de documento — todo eso vive en DJ. Cashpaya solo persiste:

  • cpy.users.dj_subject_id (UUID, FK lógica al subject en DJ)
  • cpy.users.dj_wallet_id (UUID, opcional, lookup del wallet del subject)
  • Cualquier cosa estrictamente financiera (saldo, ledger, tx)

Cashpaya no copia la cédula, ni el OCR completo, ni la selfie, ni el embedding facial. Si necesita el nombre o la fecha de nacimiento para UI, los pide a DJ vía GET /v1/subjects/{dj_subject_id} con su X-API-Key.

Esto reduce el alcance regulatorio de cashpaya: el día que SuperFinanciera audite custodia de PII, el inventario de cashpaya es cortísimo.

Decisión jguerrero #3: “wallet visible + educativa”. Cuando un usuario hace onboarding por cashpaya, el flow incluye un step wallet_create con copy explícito (“respaldamos tu identidad para usarla en otros servicios bjungle”) y un step passkey_enroll opcional (decisión #4).

Pero la navegación al wallet en sí (UI wallet.bjungle.com, gestionar passkeys, ver VCs, revocar grants) no aparece en la app cashpaya. El usuario que quiera usar su wallet cross-tenant lo descubre cuando un segundo RP — fintech distinta, EPS, telcos — le ofrezca “Iniciá con bjungle”. Ahí recién entra a la UI del wallet.

Esto evita que cashpaya tenga que diseñar UI de gestión de wallet que no es de su dominio.

5. Ningún módulo de DJ conoce a cashpaya por nombre

Sección titulada «5. Ningún módulo de DJ conoce a cashpaya por nombre»

Regla dura del PR review: si en una migration, un handler, un service o un test de bmonkey/bhawk/bseal/platform aparece la string cashpaya (salvo en seeds explícitamente etiquetados como “ejemplo cashpaya”), el PR se rechaza.

Los únicos lugares legítimos donde puede aparecer cashpaya:

  • Seeds de SARLAFT rules y onboarding flow ejecutados vía API contra el tenant, no como código embebido (Wave A2 + A6).
  • Templates de bseal “Términos Cashpaya” subidos al S3 bucket del tenant cashpaya (Wave A7).
  • Doc del repo (este ADR, gap analysis, backlog).

Esto garantiza la reusabilidad (decisión jguerrero #4) por construcción y no por buena voluntad.

  • Scope claro por PR: cuando un dev abre PR en bmonkey, sabe que NO está tocando lógica de saldos. Cuando uno abre PR en cashpaya, sabe que NO está tocando KYC. La frontera reduce el blast radius de cada cambio.
  • Reusabilidad por construcción: el tenant #2 (otra fintech, EPS, telcos) onboardea con el mismo flow.seed + rules.seed que cashpaya, solo cambiando el branding y el set de pasos. Cero refactor.
  • Audit regulatorio acotado: cashpaya audita lo financiero, DJ audita lo identidad/compliance. Cada uno responde por su slice. La SFC ya no tiene que mirar dos sistemas mezclados.
  • Multi-canal de credenciales explícito: X-API-Key vs Bearer no se ambigua. Cada endpoint declara en su doc OpenAPI cuál acepta.
  • Migración de Wave C trivial: los 50 users hacen re-onboarding ellos mismos (decisión jguerrero #9). No hay ETL bcrypt-cost-mismatch porque no se copian passwords — cada user setea password nueva en DJ.
  • Doble red round-trip en hot path de cashpaya: cuando cashpaya necesita decidir “puede este user transferir $X?”, hace al menos 2 llamadas: (1) GET /v1/subjects/{id} para confirmar KYC approved, (2) según política, POST /v1/flows/sessions {type:step_up} para forzar face_match. Latencia adicional ~150-300ms vs monolito. Mitigación: el step-up se cachea via step_up_token 5 min (ADR-002) y el subject status se cachea en cashpaya con TTL corto (≤ 60s).
  • Datos parcialmente duplicados: nombre del subject lo vamos a leer desde cashpaya frecuente para UI. Cashpaya puede cachearlo, pero si DJ lo actualiza (corrección admin de OCR) cashpaya queda stale. Documentar invariante: el nombre canónico vive en DJ; cualquier UI cashpaya que muestre nombre lo refrescá desde DJ cuando el usuario abre su perfil.
  • Dependencia operacional bidireccional: si DJ se cae, cashpaya no puede registrar usuarios nuevos ni firmar docs. Si cashpaya se cae, DJ funciona pero los users no tienen frontend para usarlo. Mitigación: monitoreo Sintético por endpoint pareado + circuit breakers en cashpaya con fallback “intentá luego”.
  • Costo de Açaí transparente al tenant: cada screening, signature_pdf, face_step_up debita Açaí de cashpaya. Cashpaya tiene que comprar topups (ADR-010) y ajustar su pricing al usuario final. No hay “todo incluido”. Esto es positivo regulatoriamente pero requiere que el equipo cashpaya entienda la unit economics.
  • Wave C de migración de 50 users (re-onboarding auto-servicio) es consecuencia directa de no compartir DB. Si compartiéramos schema sería instantáneo, pero rompería la independencia regulatoria.
  • bseal-tenant signing certs (ADR-013) permiten que la firma corporativa de cashpaya use el cert que cashpaya ya tenga, sin que DJ se entere de quién emitió el cert.
AlternativaPor qué se descartó
DJ absorbe pagos (saldos, transferencias, MP marketplace)Convertiría a DJ en fintech regulada por SFC bajo SEDPE/EFNV. Cierra puertas para que DJ sea consumida por otras fintechs que compiten con cashpaya — ninguna se integraría a su competidor. Decisión jguerrero #1 lo cerró.
Cashpaya hace KYC + SARLAFT propios (DJ solo da wallet)Replicar OCR Bedrock + arcface + opensanctions + multi-step flow en Fiber es ~3 meses de trabajo y no agrega valor — ya está hecho en DJ. Además rompe el value-prop de DJ como identidad portable.
Monolito unificado (DJ + cashpaya en un solo backend)Imposible: bjungle se vende como plataforma multi-tenant a 5-10 fintechs. Si cashpaya está fundida con la plataforma, los competidores nunca se integran.
Federation peer-to-peer (cashpaya y DJ se federan como pares iguales con DIDs cruzadas)Over-engineering para 2026. Modelo hub-and-spoke (DJ = hub, tenants = spokes) es lo que la industria opera hoy (Stripe, Plaid, Truora). Federation P2P espera a que aparezca un segundo issuer de identidad de calibre comparable, lo que no va a pasar en CO en los próximos 24 meses.
Compartir schema postgres cpy + bmonkey en una DBRomperíamos RLS multi-tenant (cpy no tiene tenant_id) y el aislamiento regulatorio. Además complica la migración Wave C de 50 users — si compartiéramos schema, no haríamos re-onboarding, copiaríamos rows. Pero entonces el bcrypt cost mismatch ataca, los hashes de doc colisionan, etc. Peor en todo.

Esto es un ADR macro de frontera — no toca código directamente. Los siguientes ADRs implementan slices concretos:

Y las waves del backlog (docs/proyecto/cashpaya-integration-backlog.md):

  • Wave A (A1..A11) — setup del tenant + flows + seeds
  • Wave A+ (F1..F10) — MP Açaí topup
  • Wave B (B1..B10) — frontend cashpaya consume DJ APIs
  • Wave C (C1..C9) — re-onboarding auto-servicio 50 users
  • Wave D (D1..D9) — SARLAFT continuo + decom legacy

Patrones que debe respetar todo PR de Wave A→D:

  • RLS deny_all + SECURITY DEFINER para tablas globales (wallet, audit_log, etc.) — ya canónico, ver 0005_wallet_global.up.sql.
  • Tx + tenant en cada handler /v1/*: tenancy.Middleware abre tx
    • setea app.current_tenant. Toda query va por esa tx.
  • Errores application/problem+json vía writeProblem.
  • Tests integration roundtrip contra postgres dev por cada handler nuevo de Wave A6, A7b, D1-D5.

Tests obligatorios:

  • E2E de Wave A10 (cédula CO real → onboarding → KYC approved + wallet + VC + passkey) corre sin que el código mencione cashpaya — configurable por env TENANT_SLUG.
  • Smoke regression: tomar el seed de cashpaya, cambiarle el slug a acme-fintech, ejecutar Wave A entero contra ese slug y verificar que funciona idéntico. Si falla, hay acoplamiento ilegal.

Riesgos a vigilar:

  • Dev que copia código de bmonkey y deja un literal if tenant.slug == "cashpaya": rechazo de PR + linter futuro.
  • Cashpaya empieza a almacenar PII “por convenience”: revisar cpy.* schema en cada PR de cashpaya legacy para que no agregue columnas nombre, cedula, selfie_url.
  • SFC (Colombia): cashpaya queda dentro del perímetro SFC por saldos. DJ queda fuera del perímetro SFC porque no custodia dinero. DJ sí queda dentro de la Ley 1581 (HABEAS DATA) y Decreto 338 (entidades de certificación digital, cuando aplique firma legal — ver ADR-013).
  • Ley 1581 / Decreto 1377 (HABEAS DATA CO): DJ es responsable del tratamiento de PII de los subjects. Cashpaya es encargado que delega en DJ ese tratamiento. Acuerdo de transferencia internacional no aplica (DJ + cashpaya = ambos en CO).
  • PCI-DSS: no aplica directamente. cashpaya custodia tarjetas en su pasarela (o las pasa a MP); DJ no toca card data en ningún flow.
  • UIAF (Unidad de Información y Análisis Financiero CO): reportes ROS los hace cashpaya porque ve la transacción financiera. DJ provee el evidence trail (audit_log + screenings result), cashpaya lo consume para armar el ROS. Automatización del ROS = backlog Wave E+ (decisión jguerrero #8).

Open questions — Resoluciones (2026-06-05)

Sección titulada «Open questions — Resoluciones (2026-06-05)»

Decididas en review de ADR (Jonhjar Guerrero, 2026-06-05):

  1. Sincronización de correcciones OCR DJ → cashpaya: ✅ Resuelto. DJ emite evento NATS subject.updated (subject bmonkey.subject.updated) cuando un campo del subject cambia (corrección manual, re-OCR, status change). Cashpaya consume con consumer durable. Shape del evento queda por definir en Wave B (frontend cashpaya) cuando se implemente el consumer; mientras tanto cashpaya consulta GET /v1/subjects/{id} al abrir perfil del usuario (pattern read-through cache).

  2. SLA DJ → cashpaya: ✅ Resuelto. Best-effort hasta que entre un tercer tenant productivo. SLA formal (99.9% uptime, RTO < 4h, RPO < 15min) se define cuando haya un cliente que lo exija contractualmente. Mientras tanto monitoreo Sintético + alertas internas son suficientes.

  3. Pricing Açaí enterprise: ⏭️ Diferido a ADR-010 (ese ADR ya toca pricing tiers de topup, mejor centralizar la decisión ahí). El default Açaí USD 0.015/unit se mantiene; descuento volumen para cashpaya se evalúa cuando ADR-010 esté Accepted.

  4. Revocación de wallet por fraude reportado por cashpaya: ✅ Resuelto. Flujo: cashpaya envía POST /v1/subjects/{id}/report-fraud {reason, evidence_ref} (endpoint nuevo Wave D). DJ marca el VC identity-kyc-loa3 como suspended en la status list (ADR-008 blockchain anchoring lo soporta vía status list bitstring). El subject puede recuperar active status tras revisión humana conjunta DJ + cashpaya — propuesta de endpoint POST /v1/subjects/{id}/restore-status con tag de auditoría dual (operator DJ + operator cashpaya). Detalle de ese flow queda como sub-tarea de Wave D backlog.

  5. Costo de SARLAFT continuo: ✅ Resuelto provisional. Sale del balance Açaí normal del tenant. Se monitorea durante los primeros 3 meses post-launch. Si el costo mensual de re-screening supera el 20% del consumo total Açaí del tenant, se introduce plan “compliance continuo” con tarifa flat (ADR-012 tiene la decisión técnica de frequency parametrizable; el modelo de pricing queda en este ADR-009 hasta tener datos).