1Qué es el sistema
Una fábrica de programas de afinidad. El broker levanta un programa nuevo por sponsor
—con la marca del sponsor, su tarifa, su tienda, su emisión de certificados y su captura de
siniestros— en semanas en lugar de meses.
No es un comparador de seguros. No compara aseguradoras, no cotiza contra portales.
Un programa equivale a una póliza maestra y por tanto a una sola aseguradora.
La métrica que optimiza: tiempo desde que el sponsor dice sí hasta que la tienda vende.
No el costo de adquisición — el sponsor trae la audiencia.
2Arquitectura
Monorepo pnpm. La tienda del sponsor se renderiza en servidor y no es una SPA: su presupuesto de peso
es parte de la promesa comercial y se prueba automáticamente.
flowchart LR
A["Cliente final"] --> B["Tienda del sponsor
SSR · menos de 150 KB"]
A --> C["Canal
WhatsApp / Telegram"]
B -->|un campo: telefono| C
C --> D["API Fastify"]
B --> D
E["Ejecutivo del broker"] --> F["Panel
Vue 3 + Vite"]
F --> D
D --> G["MariaDB 11"]
D --> H["OCR
Claude Vision"]
D --> I["SAT · CFDI
API Market"]
D --> J["Cobro
Checkout por redireccion"]
D --> K["Boveda
scrypt + AES-256-GCM"]
| Capa | Tecnología | Por qué |
| API | Node 24 · TypeScript · Fastify · Kysely | El aislamiento por tenant queda impuesto por el compilador, no por disciplina |
| Base | MariaDB 11 | Ya operada por el equipo; el esquema es el mismo motor del resto del stack |
| Tienda | SSR ligero desde Fastify | Una SPA contradiría el argumento de venta |
| Panel | Vue 3 + Vite | Usuarios internos; nadie mide su peso |
| Pruebas | node:test + tsx | Cero dependencias de framework de pruebas |
3Modelo de datos — estructura comercial
Lo que se configura una vez por programa. sponsor_id es la frontera de aislamiento y
aparece en todas estas tablas.
erDiagram
SPONSORS ||--o{ PRODUCTS : ofrece
SPONSORS ||--o{ PROGRAMS : patrocina
PRODUCTS ||--o{ MASTER_POLICIES : amparado_por
PRODUCTS ||--o{ TARIFFS : tarifado_por
TARIFFS ||--o{ TARIFF_ROWS : contiene
PROGRAMS }o--|| MASTER_POLICIES : bajo
PROGRAMS }o--|| TARIFFS : usa
SPONSORS {
char36 id PK
varchar slug UK
varchar name
char7 brand_primary
tinyint is_fictional
}
PRODUCTS {
char36 id PK
char36 sponsor_id FK
varchar kind
longtext terms_text
}
MASTER_POLICIES {
char36 id PK
varchar carrier_name
varchar policy_number
varchar cnsf_registration_ref
date valid_from
date valid_to
}
TARIFFS {
char36 id PK
int version
varchar status
}
TARIFF_ROWS {
char36 id PK
decimal12_2 item_price_min
decimal12_2 item_price_max
smallint term_months
decimal12_2 premium
decimal12_2 sum_insured
}
PROGRAMS {
char36 id PK
varchar storefront_slug UK
decimal5_4 commission_rate
varchar status
}
La tarifa es la regla; la prima es el resultado. Los rangos de precio de una misma
tarifa y plazo no se traslapan ni dejan huecos — una invariante que MariaDB no puede expresar como
constraint, así que vive en la aplicación y se prueba con casos frontera al centavo.
4Modelo de datos — transaccional
Lo que se genera por cada cliente final que entra al programa.
erDiagram
CONVERSATIONS ||--o| LEADS : origina
LEADS ||--o{ QUOTES : produce
LEADS ||--o{ PURCHASE_PROOFS : aporta
QUOTES ||--o| PAYMENTS : cobra
QUOTES ||--o| CERTIFICATES : emite
CERTIFICATES ||--o{ CLAIMS : recibe
CLAIMS ||--o{ CLAIM_EVIDENCE : sustenta
CONVERSATIONS {
char36 id PK
varchar channel
varchar channel_user_ref
varchar state
}
LEADS {
char36 id PK
varchar status
char36 assigned_executive_id FK
}
PURCHASE_PROOFS {
char36 id PK
char36 cfdi_uuid UK
varchar issuer_rfc
decimal12_2 total
varchar sat_validation_status
}
QUOTES {
char36 id PK
decimal12_2 premium
decimal12_2 sum_insured
datetime valid_until
longtext input_snapshot
}
PAYMENTS {
char36 id PK
varchar provider
varchar provider_payment_id UK
decimal12_2 amount
varchar status
}
CERTIFICATES {
char36 id PK
varchar certificate_number UK
varchar policyholder_name
varchar insured_name
date valid_from
date valid_to
varchar status
}
CLAIMS {
char36 id PK
smallint fraud_score
varchar status
text assessment
}
CLAIM_EVIDENCE {
char36 id PK
varchar encrypted_ref
char64 sha256
}
No existe una tabla de pólizas. La aseguradora emite una póliza maestra al
sponsor y cada asegurado recibe un certificado de adhesión. Llamarle póliza a lo del asegurado
implicaría un contrato individual que no existe.
La idempotencia del webhook de pago vive en el esquema, no en la lógica:
provider_payment_id es único, así que un pago nunca emite dos certificados.
5Aislamiento por sponsor
Un sponsor es un tenant. La conexión cruda a la base es privada del directorio db/:
los módulos solo reciben un ScopedDb que ya trae el filtro aplicado. Un query sin scope
no está prohibido — no compila.
flowchart TD
A["Request con JWT"] --> B["Middleware sponsorScope"]
B --> C{"Tipo de token"}
C -->|sponsor| D["sponsorIds = un sponsor"]
C -->|executive| E["Consulta executive_access
en cada request"]
C -->|broker_admin| F["Lista enumerada
de sponsors activos"]
E --> G{"Tiene accesos?"}
G -->|no| H["UNAUTHORIZED"]
G -->|si| I["AccessScope"]
D --> I
F --> I
I --> J["createScopedDb"]
J --> K["Todo query lleva
WHERE sponsor_id IN scope"]
L["sponsor_id en query string
body o header"] -.->|SE IGNORA| B
| Regla | Por qué |
Todo scope lleva una lista explícita y no vacía de sponsorIds | No hay comodín, ni valor "todos", ni rol que evada el filtro — ni broker_admin. Elimina la clase de bug por la que se filtran datos en casi todo sistema multi-tenant. |
| El scope se resuelve del token, nunca de un parámetro del request | Un sponsor_id que llegue por query string se ignora. |
| El scope del ejecutivo se consulta en cada request | Revocar acceso surte efecto de inmediato, sin esperar que expire el token. |
Un recurso ajeno responde NOT_FOUND, nunca FORBIDDEN | Un 403 confirmaría que el recurso existe en otro tenant. Y nunca lista vacía donde tocaba rechazo: la lista vacía esconde el bug. |
executives y executive_access son las únicas tablas sin sponsor_id | Los ejecutivos pertenecen al broker, no a un sponsor. Excepción declarada, alcanzable solo por un accesor con nombre propio. |
6Motor de prima
Una interfaz con tres implementaciones. La tercera está declarada y vacía a propósito: es la costura
que se muestra si preguntan por multi-aseguradora.
flowchart LR
A["QuoteInput"] --> B["PremiumEngine"]
B --> C["TariffTableEngine
funciona"]
B --> D["BrokerRequestEngine
funciona"]
B --> E["CarrierFeedEngine
stub declarado"]
C --> F["Prima determinista
desde tabla de tarifa"]
D --> G["Solicitud completa
al ejecutivo humano"]
E --> H["Feed de aseguradora
sin implementar"]
El asistente de captación nunca calcula la prima. Extrae datos a un objeto tipado;
el motor determinista calcula. Es control regulatorio y control de seguridad a la vez: si el modelo
nunca toca la prima, no hay inyección de prompt que consiga un descuento.
BrokerRequestEngine no es un consuelo: para líneas que requieren suscripción —fianzas,
líneas financieras— no existe prima instantánea, y que el ejecutivo reciba un caso completo
y validado en dos minutos en lugar de perseguir al cliente una semana es el producto.
7Flujo de captación y emisión
Producto del demo: garantía extendida. Cierra el ciclo sin depender de ninguna aseguradora.
flowchart TD
A["Tienda del sponsor
un campo: telefono"] --> B["Se abre conversacion
estado en MariaDB"]
B --> C["Pide foto de la factura"]
C --> D["OCR: Claude Vision
Haiku, fallback Sonnet si confianza menor a 0.7"]
D --> E["monto · UUID del CFDI
RFC emisor · fecha"]
E --> F["Valida el CFDI contra el SAT"]
F --> G["Pregunta solo lo que falte
plazo · datos del contratante"]
G --> H["TariffTableEngine
prima determinista"]
H --> I["Confirmacion EN TEXTO
el asegurado ve y corrige"]
I --> J["Emite certificado
PENDING_PAYMENT"]
J --> K["Checkout por redireccion"]
K --> L{"Pago verificado
contra el proveedor?"}
L -->|si| M["Certificado ACTIVE"]
L -->|no| N["Sigue PENDING
se reconcilia por API"]
M --> O["Asignacion a ejecutivo"]
El estado de la conversación vive en la base, no en n8n. n8n es transporte del demo;
si el estado viviera ahí, tendríamos un prototipo que no se puede endurecer.
El certificado nunca pasa a ACTIVE por un webhook sin verificar contra el
proveedor. Un webhook es un aviso, no una prueba de pago.
8Ciclo de vida del certificado
stateDiagram-v2
[*] --> PENDING_PAYMENT : emision
PENDING_PAYMENT --> ACTIVE : pago verificado
PENDING_PAYMENT --> CANCELLED : expira o se cancela
ACTIVE --> EXPIRED : termina la vigencia
ACTIVE --> CANCELLED : cancelacion
ACTIVE --> ACTIVE : siniestro reportado
EXPIRED --> [*]
CANCELLED --> [*]
Un siniestro no cambia el estado del certificado: se registra contra él y sigue vigente hasta que
termine su periodo. Endosos y movimientos sobre certificados vigentes quedan fuera del alcance actual.
9Flujo de siniestro
Alto volumen y bajo monto, con evidencia fotográfica y riesgo alto de fraude. El sistema arma un
expediente; dictamina un humano.
flowchart TD
A["Asegurado por canal
se rompio la pantalla"] --> B["Busca certificado vigente
por identificador de canal"]
B --> C["Pide evidencia: fotos del dano"]
C --> D["Cifrado en reposo + sha256"]
D --> E["Recupera el CFDI ya validado
no se vuelve a pedir"]
E --> F["Validacion tecnica de procedencia"]
F --> G["Fecha dentro de vigencia?"]
F --> H["Articulo coincide con el CFDI?"]
F --> I["Suma asegurada alcanza?"]
F --> J["CFDI ya usado en otro siniestro?"]
G --> K["FraudService: scoring"]
H --> K
I --> K
J --> K
K --> L["Expediente armado"]
L --> M["Asignacion a ejecutivo"]
M --> N["DICTAMINA UN HUMANO"]
| Señal de fraude | De dónde sale |
| CFDI ya usado en otro siniestro | Índice único sobre cfdi_uuid por sponsor |
| Siniestro fuera de vigencia | Fechas del certificado |
| Daño inconsistente con el artículo facturado | OCR del CFDI contra la evidencia |
| Velocidad por identificador de canal | Conteo de reportes por número |
| Reincidencia | Historial del asegurado |
Que el comprobante ya esté validado contra el SAT desde la emisión es el control antifraude más fuerte
del diseño, y es gratis: el fraude clásico en garantía extendida es la factura alterada o reutilizada.
10Límite regulatorio de la IA
Asesorar para la celebración de un contrato de seguro es intermediación con licencia (LISF y
Reglamento de Agentes). El asistente no tiene cédula, así que informa y nunca
asesora. No es una preferencia de estilo: es la línea que define qué puede hacer el sistema.
| La IA sí hace | La IA nunca hace |
| Captar conversacionalmente |
Calcular la prima |
| Extraer datos de documentos con OCR |
Recomendar cobertura u opinar sobre su idoneidad |
| Preguntar lo que falta y validar formatos |
Emitir por su cuenta sin confirmación explícita |
| Informar qué cubre, leyendo del texto registrado del producto |
Dictaminar un siniestro |
| Generar el prospecto y escalar a un ejecutivo |
Tocar datos de otro sponsor |
Comercialización: artículo 102, segundo párrafo de la LISF — contrato de prestación
de servicios para promoción o venta de productos de seguro de adhesión, con registro previo
ante CNSF — más la Circular Única de Seguros y Fianzas, disposición 33.2.10. El registro es del broker
y se referencia en master_policies.cnsf_registration_ref.
11Roles y visibilidad
| Rol | Qué ve | Cómo se resuelve |
| Cliente final | La tienda del sponsor y su propia conversación | Identificador de canal; sin sesión |
| Asegurado | Su certificado y sus siniestros | Identificador de canal verificado |
| Ejecutivo | Solo los sponsors que executive_access le concede | Consulta en cada request; revocar surte efecto inmediato |
| Administrador del broker | Todos los sponsors activos, enumerados | Lista explícita en tiempo de request; nunca un comodín |
Broker es la firma —la entidad con registro ante CNSF—, nunca una persona.
La persona que trabaja un prospecto es un ejecutivo. La distinción importa porque
"agente de seguros" es un rol con cédula y un ejecutivo puede no tenerla.
12Vocabulario
Cerrado. Ninguna palabra fuera de la lista entra al código, al esquema ni a la interfaz. Los términos
prohibidos lo están por razón legal o de modelo de datos, no por gusto.
| Se dice | Nunca | Por qué |
| Certificado | Póliza (para lo del asegurado) | Una póliza maestra al sponsor; certificados de adhesión a cada asegurado |
| Broker | Broker para una persona | El broker es la firma; la persona es un ejecutivo |
| Siniestro | Reclamo, reclamación | En México una reclamación es una queja ante CONDUSEF |
| Prima | Precio, costo | La prima es el resultado; la tarifa es la regla registrada |
| Asistente de captación | Bot, chatbot | Subvende lo que hace e invita a implicar que asesora |
| Información | Asesoría, "te recomendamos", "te conviene" | Asesorar es intermediación con licencia |
| Sponsor | Cliente (a secas) | Ambiguo: el asegurado también es cliente |