CFDI40-GentleNaN (Parte II): Modelo de datos y arquitectura fiscal
Resumen del autor
Aciertos
Los modelos GLM 5.3 Flash y Deepseek compenden muy bien la estructura de datos de un sistema de CFDI 4.0. Ellos me propusieron una estructura de 13 tablas. Yo propuse 7.
Donde aflojan
Por alguna razón se inclinan por tener como fuente de la verdad el archivo XML timbrado como fuente de la verdad. Es decir, los campos mínimos en la base de datos y para el resto consultar o leer directamente el archivo XML. No es práctico. No es indexable. Es mejor agregar esos campos (como uuid, fecha_timbrado, rfc_pac…) a la base de datos.
Al planear los modelos que tendrá la base de datos pude notar que DeepSeek se estaba inclinando demasiado por tecnologías tipo NoSQL, aunque tenía claros los datos que tenía que guardar quería usar por ejemplo datos JSONB, esto supongo porque son campos dinámicos en el XML (no siempre vienen) como los impuestos. Mi opinión es que esto no es práctico. Es mejor tener los campos con default=0 que no tenerlos para mantener la estructura de datos.
También quería crear una tabla específica para los folios complicando o exagerando el alcance del proyecto. Decidí guardar este dato en la tabla configuracion.
El registro de los agentes
Registro de la conversación de diseño entre el dueño y el agente (2026-10-05), previa a F4.1. Muestra cómo se negoció un diseño de BD a dos voces antes de escribir una sola línea de código. El contrato final vive en
docs/modelo-de-datos.md.
El punto de partida del agente
El primer diseño propuesto (ventana anterior) tenía 14 tablas: la decisión estructural era tratar al XML sellado/timbrado como fuente de verdad fiscal y guardar en tablas solo columnas derivadas (uuid, folio, montos, estatus) más conceptos como renglones. Cualquier nodo del CFDI que no se necesitara consultar quedaba fuera del esquema. Sobre ese esqueleto se pidieron aprobar cuatro decisiones: XML canónico vs mapeo relacional completo, PK bigint vs UUID, tabla única de comprobantes vs borradores separados, y dónde viviría el CSD cifrado.
La contra-propuesta del dueño
El dueño respondió con un modelo más chico y más suyo — 5 tablas:
configuracion(empresa, rfc, domicilio, logo, apikeys)clientesproductoscomprobantes(fecha, folio, uuid, FK a cliente, datos de timbrado)comprobante_productos(FK a producto, cantidades, precios, impuestos, descripción editable copiada del producto)
Y dos posturas explícitas:
- La fuente de verdad es la tabla/modelo, no el XML; el XML se guarda como artifact.
- Preguntó si conviene un
comprobante_timbre1:1 para los datos del timbre.
Cómo se reconciliaron las dos fuentes de verdad
El agente aceptó la base del dueño y matizó la verdad partida en dos:
- Antes del timbre: el modelo manda. El borrador es editable y de él se genera el XML.
- Después del timbre: el comprobante se congela — nadie edita renglones de algo ya
timbrado; lo que corresponde es cancelar o relacionar. El XML timbrado queda guardado y
uuid, sellos y cadena original se extraen del acuse hacia columnas. Ante discrepancia, gana el XML.
Sobre comprobante_timbre 1:1: no. Se lee siempre junto al comprobante y un join 1:1 no
compra nada — los datos del timbre van como columnas de comprobantes.
Decisiones cerradas, una por una
| # | Tema | Decisión | Por qué |
|---|---|---|---|
| 1 | Granularidad del esquema | 5 modelos del dueño + 3 que el alcance exigía (comprobantes_relacionados, intentos_timbrado, folios→luego absorbida) | El CFDI de egreso exige CfdiRelaciones 01, D13 exige auditoría de intentos, y el folio hay que numerarlo |
| 2 | Fuente de verdad | Modelo antes del timbre; XML congelado después | El borrador es editable; lo timbrado es inmutable por ley fiscal |
| 3 | Timbre 1:1 | No: columnas en comprobantes | Se consulta siempre junto; sin beneficio real del join |
| 4 | Serie y folio | En configuracion (serie, ultimo_folio), no en tabla aparte | Una instancia = una empresa = una serie; la reserva usa SELECT FOR UPDATE en su propia transacción. Si algún día hay series múltiples, se migra |
| 5 | Impuestos | Columnas, no JSON: exento boolean, tasa_iva numeric CHECK IN (0, 0.08, 0.16), tasa_ieps numeric 0–1 | Tipado y consultable sin funciones JSON; el dueño rechazó el jsonb propuesto por el agente |
| 6 | Snapshot de renglones | descripcion, claves SAT y tasas se copian al renglón | La factura debe sobrevivir intacta a cambios del producto |
| 7 | Apikeys | En configuracion, cifradas con la maquinaria AES-GCM del CSD (D7) | Editables desde la UI (F6); una instancia por cliente |
| 8 | Logo | Archivo en volumen, ruta en configuracion | BD liviana; nginx lo sirve directo al frontend |
| 9 | PK | bigint identity; unicidad fiscal por UNIQUE(uuid) | Simple y compacto; el UUID fiscal ya existe como dato del SAT |
| 10 | Convenciones | timestamptz UTC, enums text+CHECK, naming_convention, ON DELETE explícito, montos numeric(18,6) | Cerradas sin discusión |
| 11 | Diferidas | users, estatus_consultas (D4) van en F4.5/F5 | No bloquean el diseño ni las migraciones base |
Enmienda del 2026-10-05:
email_outbox(D6) figuraba en esta lista como diferida a F5, pero el dueño decidió adelantarla a F4.3. El motivo está en el flujo de timbrado: el encolado tiene que ser atómico con el timbrado (paso 10 dedocs/PROJECT.md), y eso es infraestructura de datos, no integración con Brevo. La tabla y el worker ya existen; el adaptador de Brevo sigue en F5. Contrato actualizado endocs/modelo-de-datos.md§3.
Lo que la conversación cambió del diseño inicial
- El agente propuso una tabla
foliosy el dueño la derrumbó con un argumento de simplicidad: “si al final son configuraciones”. La migración futura a series múltiples quedó anotada como escape, no como necesidad. - El agente propuso impuestos
jsonby el dueño pidió columnas tipadas. Razón práctica: los valores del dominio son tres (0, 0.08, 0.16) y unCHECKen la base vale más que flexibilidad que nadie va a usar. - El agente propuso 14 tablas y se cerró en 7: el restante de la conversación fue distinguir lo que el alcance exige (relacionados, intentos, folio) de lo que era diseño prematuro (users, outbox, estatus — diferidos sin drama).
Estado final
Diseño aprobado el 2026-10-05. Siete tablas núcleo: configuracion (fila única, con CSD y
apikeys cifradas), clientes, productos, comprobantes, comprobante_productos,
comprobantes_relacionados, intentos_timbrado.