Afinidad. — Arquitectura y flujos

Fábrica de programas de afinidad · Hylant Group México · SINDRI

Índice

  1. Qué es el sistema
  2. Arquitectura
  3. Modelo de datos — estructura comercial
  4. Modelo de datos — transaccional
  5. Aislamiento por sponsor
  6. Motor de prima
  7. Flujo de captación y emisión
  8. Ciclo de vida del certificado
  9. Flujo de siniestro
  10. Límite regulatorio de la IA
  11. Roles y visibilidad
  12. Vocabulario

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"]
CapaTecnologíaPor qué
APINode 24 · TypeScript · Fastify · KyselyEl aislamiento por tenant queda impuesto por el compilador, no por disciplina
BaseMariaDB 11Ya operada por el equipo; el esquema es el mismo motor del resto del stack
TiendaSSR ligero desde FastifyUna SPA contradiría el argumento de venta
PanelVue 3 + ViteUsuarios internos; nadie mide su peso
Pruebasnode:test + tsxCero 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
ReglaPor qué
Todo scope lleva una lista explícita y no vacía de sponsorIdsNo 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 requestUn sponsor_id que llegue por query string se ignora.
El scope del ejecutivo se consulta en cada requestRevocar acceso surte efecto de inmediato, sin esperar que expire el token.
Un recurso ajeno responde NOT_FOUND, nunca FORBIDDENUn 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_idLos 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 fraudeDe dónde sale
CFDI ya usado en otro siniestroÍndice único sobre cfdi_uuid por sponsor
Siniestro fuera de vigenciaFechas del certificado
Daño inconsistente con el artículo facturadoOCR del CFDI contra la evidencia
Velocidad por identificador de canalConteo de reportes por número
ReincidenciaHistorial 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í haceLa 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

RolQué veCómo se resuelve
Cliente finalLa tienda del sponsor y su propia conversaciónIdentificador de canal; sin sesión
AseguradoSu certificado y sus siniestrosIdentificador de canal verificado
EjecutivoSolo los sponsors que executive_access le concedeConsulta en cada request; revocar surte efecto inmediato
Administrador del brokerTodos los sponsors activos, enumeradosLista 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 diceNuncaPor qué
CertificadoPóliza (para lo del asegurado)Una póliza maestra al sponsor; certificados de adhesión a cada asegurado
BrokerBroker para una personaEl broker es la firma; la persona es un ejecutivo
SiniestroReclamo, reclamaciónEn México una reclamación es una queja ante CONDUSEF
PrimaPrecio, costoLa prima es el resultado; la tarifa es la regla registrada
Asistente de captaciónBot, chatbotSubvende lo que hace e invita a implicar que asesora
InformaciónAsesoría, "te recomendamos", "te conviene"Asesorar es intermediación con licencia
SponsorCliente (a secas)Ambiguo: el asegurado también es cliente