¿Se puede crear un sistema de facturación para México (CFDI 4.0) solo con modelos de IA chinos?

¿Se puede crear un sistema de facturación para México (CFDI 4.0) solo con modelos de IA chinos?

Ruperto Coronado
Ruperto Coronado

Documentación de un experimento en curso. Primero el stack que vamos a construir, después las decisiones que fuimos tomando sobre la marcha — que resultaron ser la parte interesante.


La pregunta

Todo empezó con una pregunta concreta:

¿Se puede crear un sistema de facturación electrónica para México (CFDI 4.0) usando únicamente modelos de lenguaje chinos como agentes de desarrollo?

— Hilo original en X

Elegí el proyecto durante una caminata en el cerro. El dominio de CFDI es hostil a los errores silenciosos: un comprobante mal sellado produce un XML bien formado que el SAT rechaza con un código genérico hasta que el PAC lo valida. Conozco el terreno porque construí un sistema de facturación electrónica a mano antes de la llegada de los LLMs.

El objetivo: que el sistema terminado sea una instancia desplegable y útil que cualquiera pueda levantar en su VPS con sus propias credenciales.

Lo siguiente fue estructurado principalmente por los modelos con mis revisiones y correcciones, pero antes comparto mi balance:

Resumen del autor

Encuentro a DeepSeek y a GLM-5.3-flash muy maduros para este tipo de proyecto. Aunque definí la descripción inicial, ellos me fueron corrigiendo sobre la marcha y agregando detalles técnicos que pasé por alto. Yo tenía documentación desactualizada del PAC, pero DeepSeek decidió por iniciativa propia consultar el WSDL, comparar contra la documentación y detectar varias discrepancias.

Comprende además con bastante precisión el modelo de facturación CFDI 4.0. De hecho, aunque yo sugerí la librería satcfdi, fue el modelo quien dimensionó su alcance y ventajas; por ejemplo, identificó que ya incluye los catálogos oficiales del SAT listos para poblar la base de datos.

Me deja un gran sabor de boca: que modelos desarrollados en China dominen con tanta soltura la normativa fiscal mexicana de CFDI 4.0 es sumamente llamativo. Yo propuse el stack tecnológico, pero demostraron comprender a fondo cómo conectar e integrar cada pieza.

Los modelos

  • Ejecutor: deepseek-v4-flash — implementación, tests y refactors.
  • Supervisor: glm-5.3-flash — diseño de arquitectura y revisión.

El harness es Pi con Gentle AI para disciplina de proceso: verificación, delegación y artefactos estructurados en lugar de contexto flotante en el chat. Para servir los modelos utilizo NaN de @barckcode.

El código y el documento de definición completo del proyecto están disponibles en el repositorio en GitLab.

Parte 1 — El stack

Backend

FastAPI sobre Python 3.12, con SQLAlchemy 2.0 (async), Alembic y Pydantic v2. La razón: el 80% del sistema son reglas de validación fiscal; FastAPI las transforma en contratos explícitos y autodocumentados.

PostgreSQL 16 como base de datos única para negocio, secretos cifrados y la cola transaccional de emails (ver decisión 10).

El núcleo fiscal: satcfdi

Librería open source de la comunidad (SAT-CFDI/python-satcfdi) que implementa CFDI 4.0 completo: constructores, catálogos del SAT, carga de CSD, sellado criptográfico, validación, representación impresa e interfaz base para PACs. Ahorra semanas de desarrollo: en lugar de implementar criptografía y esquemas a mano, solo construimos el adaptador de PAC y la capa de negocio.

Los PACs: dos proveedores, no uno

En México el timbre no se solicita directo al SAT sino a un PAC (Proveedor Autorizado de Certificación). El proyecto soporta dos:

  • Formas Digitales (Forsedi): PAC tradicional vía SOAP/WSDL, con autenticación de usuario/contraseña y recepción de XML ya sellado.
  • TimbraKora: Wrapper REST moderno de desarrollo propio sobre Formas Digitales y otros PACs. Autentica con una sola API Key, permite sellar localmente o delegar el sellado si tienes los CSD cargados en tu cuenta, y suma historial, PDF, saldo y cancelación unificada. Lo construí justamente para eliminar excepciones y métodos heterogéneos entre proveedores.

La diferencia de barrera es drástica: TimbraKora requiere una API Key; Forsedi exige lidiar con un WSDL.

Frontend

Vue 3 con Vite, TypeScript, Pinia, vue-router y PrimeVue. La elección de UI responde a practicidad: el sistema es 90% tablas de catálogos y formularios con validaciones fiscales complejas. El DataTable de PrimeVue ahorra semanas de trabajo.

Infraestructura

Todo en Docker Compose, con Nginx como único punto de entrada público (frontend, proxy a API y terminación TLS):

nginx  →  api (FastAPI)  →  db (PostgreSQL)
          web (Vue 3)
          worker (emails + estatus)

Solo Nginx expone puertos. La API, la base de datos y el worker conviven en la red interna de Docker, sin exposición pública.

Toda la configuración vive en .env (incluido el puerto público). Quien despliega solo clona, copia .env.example a .env, llena sus credenciales y levanta. Una regla aprendida en proyectos previos: el docker-compose.yml se mantiene inmutable; la variabilidad va entera a las variables de entorno.

Emails: Brevo desde un worker

Envío mediante la API de Brevo a través de un worker desacoplado. Evita que la latencia de terceros bloquee la respuesta al usuario y previene que la IP de un VPS nuevo caiga en listas negras.

PDF

Generación local sin depender del PAC: satcfdi renderiza el HTML cumpliendo la Regla 2.7.1.7 (leyendas obligatorias, cadena original y QR), inyectamos la plantilla con el logo del emisor y WeasyPrint compila a PDF.


Parte 2 — Decisiones sobre la marcha

En un experimento técnico, las decisiones y desvíos imprevistos son el dato real, no el ruido.

1. La “API del PAC” resultó ser un WSDL

El punto de partida prometía una “API de Formas Digitales”. Al leer el WSDL real, encontramos SOAP clásico:

POST https://dev33.facturacfdi.mx/WSTimbradoCFDIService?wsdl

Objetos accesos, comprobante como string y errores devueltos dentro de respuestas HTTP 200 (codigoError: "305"). Nada de JSON ni códigos HTTP semánticos.

Lección: “API” en un documento comercial no garantiza REST. Descargar e inspeccionar el WSDL antes de codificar fue indispensable.

2. El sello es nuestro: el verdadero alcance del proyecto

El método TimbrarCFDI recibe el parámetro comprobante: un XML ya sellado.

El PAC timbra, no sella:

plantilla → cadena original (XSLT oficial)
          → digest SHA-256
          → firma RSA con el CSD (.cer + .key)
          → SelloCFD
          → XML sellado → PAC → TimbreFiscalDigital

Toda la cadena criptográfica quedó de nuestro lado. Ahí nos dimos cuenta de que no estábamos escribiendo un wrapper, sino un motor de facturación. Fue la decisión con más peso inicial — hasta que incorporamos satcfdi (decisión 4).

3. La documentación del PAC tenía tres discrepancias críticas

Leer el WSDL desmintió tres afirmaciones del manual comercial:

DocumentaciónRealidad en WSDL
”Se requiere habilitar el puerto 80”Opera íntegramente sobre HTTPS (:443)
La respuesta incluye pathXML para consultaracuseCFDI solo retorna codigoError, error y xmlTimbrado
(Implícito) Se puede consultar estatusNo existe operación de consulta de estatus

La última fue determinante: el plan contemplaba un worker consultando estados al PAC cada hora. Ese endpoint no existe, por lo que la consulta debe ir directo al SAT.

Lección: la documentación comercial envejece mal; el WSDL es el contrato de verdad.

4. Corrección temprana en la arquitectura de sellado

Cuando vimos que el sellado era propio, la primera alarma fue: “el riesgo principal del experimento es la criptografía, y un modelo rápido va a fallar en silencio”.

Ese diagnóstico era correcto en ese instante y caduco dos horas después al adoptar satcfdi, que redujo el sellado a cuatro líneas. Documentarlo vale la pena: un análisis que no se corrige es solo una opinión con fecha.

DeepSeek detectó estas discrepancias a tiempo al consultar el WSDL por iniciativa propia gracias a las herramientas disponibles en el entorno (Gentle Shell).

5. Entra un segundo PAC: unificar la criptografía

TimbraKora ofrece una barrera de entrada más baja (REST, API Key, timbres mínimos más accesibles) y puede sellar por cuenta propia si se cargan los CSDs en cuenta.

Esto planteó un dilema: delegar el sellado o mantenerlo local. Elegimos sellar siempre local. La consistencia arquitectónica vale más que el ahorro puntual: mantener dos caminos criptográficos distintos implica duplicar suites de tests y vectores de falla. Cuando los proveedores divergen en capacidades, usar lo particular de cada uno suele desembocar en dos arquitecturas incompatibles.

6. El puerto contra los drivers

Dos PACs con protocolos incompatibles (SOAP vs REST) exigen un puerto con dos drivers en lugar de bifurcar la lógica:

TimbradoProvider
 ├── TimbraKoraProvider  → httpx → POST /cfdi/timbrar-xml/
 └── ForsediProvider     → zeep  → TimbrarCFDI

La aplicación desconoce cuál proveedor está activo. satcfdi provee la clase base (pacs.PAC) con issue(), status() y cancel(), sobre la que implementamos ambos adaptadores.

7. Estatus: consulta directa al SAT

Forsedi no expone endpoint de consulta de estado. Para no acoplarnos al historial de TimbraKora y mantener paridad, decidimos consultar directo al SAT (ConsultaCFDIService.svc) para todos los UUIDs. El SAT es la fuente de verdad definitiva y el worker de estatus queda desacoplado del PAC. DeepSeek propuso esta alternativa directamente al advertir la ausencia del método en el WSDL.

8. El CSD no va al sistema de archivos

El CSD (.cer, .key y contraseña) es crítico. En lugar de archivos en disco con permisos restrictivos, optamos por cifrado en PostgreSQL con AES-GCM y clave maestra en .env. Los respaldos de base de datos cubren datos y secretos a la vez, y el material criptográfico nunca toca el disco: se descifra en memoria exclusivamente durante el sellado.

9. Nunca reintentar sobre un error fiscal

El reflejo estándar ante fallos externos es reintentar con backoff. En facturación electrónica es un error: si el PAC devuelve codigoError: "305" (fuera de vigencia) o CFDI40138 (emisor no coincide con el padrón), el fallo es determinista. Reintentar solo quema timbres, oculta el bug y satura los logs.

Solo se reintenta el fallo transitorio de red. El error fiscal se audita con request y response crudos, notificando de inmediato la causa al usuario.

10. Outbox en PostgreSQL en lugar de Redis

En lugar del stack tradicional Celery + Redis para el worker de emails, usamos una tabla email_outbox en PostgreSQL con FOR UPDATE SKIP LOCKED y backoff exponencial.

La ventaja es transaccional: el email se encola dentro de la misma transacción que persiste el comprobante timbrado. Se elimina el riesgo de estados inconsistentes, no hay dependencias volátiles y se ahorra un contenedor en la infraestructura.

11. Tres capas de verificación para la zona ciega

El sellado es el punto donde un error no explota ruidosamente. Definimos tres niveles obligatorios:

  1. Vectores conocidos: Cadena original y sello esperado sobre un CFDI de referencia.
  2. Contrato del adaptador: Validación estricta del payload SOAP/JSON contra el XSD real descargado del WSDL (vital para evitar que un modelo alucine nombres de campos como acuseCFDI vs acuse o codigoError vs codigo_error).
  3. Integración sandbox: Timbrado real contra el entorno de pruebas del PAC.

12. Las métricas del experimento

Un experimento sin métricas es una anécdota. Se registran por tarea:

  • Intervenciones humanas necesarias: Correcciones de rumbo frente a aprobaciones rutinarias.
  • Defectos en revisión: Retrabajo medido en líneas de código reescritas.
  • Errores de contrato: Atributos inventados o formatos asumidos en APIs externas.
  • Disciplina TDD: Porcentaje de tests escritos antes de la implementación.

Sin estos datos sabremos si el sistema funciona, pero no si la respuesta a la hipótesis inicial sobre la autonomía de los modelos es afirmativa.


Lo que viene

Actualmente estoy trabajando en los modelos de datos.