CFDI40-GentleNaN (Parte II): Modelo de datos y arquitectura fiscal

CFDI40-GentleNaN (Parte II): Modelo de datos y arquitectura fiscal

Ruperto Coronado
Ruperto Coronado

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)
  • clientes
  • productos
  • comprobantes (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:

  1. La fuente de verdad es la tabla/modelo, no el XML; el XML se guarda como artifact.
  2. Preguntó si conviene un comprobante_timbre 1: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

#TemaDecisiónPor qué
1Granularidad del esquema5 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
2Fuente de verdadModelo antes del timbre; XML congelado despuésEl borrador es editable; lo timbrado es inmutable por ley fiscal
3Timbre 1:1No: columnas en comprobantesSe consulta siempre junto; sin beneficio real del join
4Serie y folioEn configuracion (serie, ultimo_folio), no en tabla aparteUna 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
5ImpuestosColumnas, no JSON: exento boolean, tasa_iva numeric CHECK IN (0, 0.08, 0.16), tasa_ieps numeric 0–1Tipado y consultable sin funciones JSON; el dueño rechazó el jsonb propuesto por el agente
6Snapshot de renglonesdescripcion, claves SAT y tasas se copian al renglónLa factura debe sobrevivir intacta a cambios del producto
7ApikeysEn configuracion, cifradas con la maquinaria AES-GCM del CSD (D7)Editables desde la UI (F6); una instancia por cliente
8LogoArchivo en volumen, ruta en configuracionBD liviana; nginx lo sirve directo al frontend
9PKbigint identity; unicidad fiscal por UNIQUE(uuid)Simple y compacto; el UUID fiscal ya existe como dato del SAT
10Convencionestimestamptz UTC, enums text+CHECK, naming_convention, ON DELETE explícito, montos numeric(18,6)Cerradas sin discusión
11Diferidasusers, estatus_consultas (D4) van en F4.5/F5No 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 de docs/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 en docs/modelo-de-datos.md §3.

Lo que la conversación cambió del diseño inicial

  • El agente propuso una tabla folios y 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 jsonb y el dueño pidió columnas tipadas. Razón práctica: los valores del dominio son tres (0, 0.08, 0.16) y un CHECK en 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.