Conciliación Fiscal
La conciliación fiscal es el proceso central de GM Fiscal. Consiste en cruzar las facturas registradas en el ERP interno de GM Transporte contra las facturas oficiales emitidas o recibidas por la empresa según el SAT, y clasificar cada comprobante según el resultado del cruce.
Función MakeConciliacion
La función MakeConciliacion es la implementación central de la conciliación. Su característica más importante es que es pura: no tiene efectos secundarios, no accede a base de datos ni a servicios externos, y produce el mismo resultado para los mismos argumentos de entrada. Esta propiedad la hace completamente testeable sin infraestructura.
func MakeConciliacion( rfcSolicitante string, uuidSolicitud string, facturasERP []factura.FacturaERP, facturasProdigia []factura.FacturaProdigia, previousConciliacionUUIDs []string,) ConciliacionLa función recibe las dos listas de facturas y devuelve una Conciliacion con todas las facturas clasificadas. El parámetro previousConciliacionUUIDs permite ignorar UUIDs de conciliaciones anteriores en la misma solicitud, evitando duplicados en reprocesos.
Algoritmo de conciliación
El algoritmo sigue tres pasos secuenciales con una complejidad lineal O(n):
flowchart TD
A[Indexar facturas ERP por UUID\nstrings.EqualFold como llave] --> B
B[Indexar facturas Prodigia por UUID\nstrings.EqualFold como llave] --> C
C[Para cada UUID en ambos lados]
C --> D{UUID en ERP\ny en Prodigia?}
D -->|Sí| E["Origen = AMBOS\nConciliado = true\nEstatusERP = VIGENTE"]
D -->|Solo ERP| F["Origen = ERP\nConciliado = false"]
D -->|Solo Prodigia| G["Origen = PRODIGIA\nConciliado = false"]
C --> H{FechaCancelacion\nen ERP != nil?}
H -->|Sí| I["EstatusERP = CANCELADO"]
H -->|No| J["EstatusERP = VIGENTE"]
Paso 1 — Indexación ERP. Se construye un mapa map[string]FacturaERP usando el UUID como clave, normalizado con strings.EqualFold. Este método es obligatorio porque los UUIDs SAT no tienen case definido y pueden aparecer en mayúsculas, minúsculas o mixtos según el proveedor.
Paso 2 — Indexación Prodigia/SAT. Se construye el mismo tipo de mapa para las facturas del SAT.
Paso 3 — Clasificación. Se itera sobre la unión de todos los UUIDs y se asigna la categoría de conciliación:
| Condición | Origen | Conciliado |
|---|---|---|
| UUID en ERP y en SAT, sin cancelación | AMBOS | true |
| UUID solo en ERP | ERP | false |
| UUID solo en SAT | PRODIGIA | false |
UUID en ERP con FechaCancelacion != nil | AMBOS o ERP + EstatusERP = CANCELADO | false |
Invariantes de impuestos
La regla más importante de la conciliación es que los arrays de impuestos de cada origen nunca se mezclan. Cada factura conciliada contiene dos arrays completamente independientes:
ImpuestosERP []Impuesto— impuestos tal como los registra el ERP.ImpuestosProdigia []Impuesto— impuestos tal como los registra el SAT.
Esta separación es la que permite detectar discrepancias exactas entre fuentes: un mismo comprobante puede tener IVA al 16% en el ERP pero al 8% en el SAT, lo que indica un error de captura o una diferencia en la clasificación de zona fronteriza.
La invariante es verificable en tests unitarios:
for _, fc := range result.FacturasConciliadas { for _, imp := range fc.ImpuestosERP { assert.Equal(t, "ERP", imp.Origen) // nunca "PRODIGIA" } for _, imp := range fc.ImpuestosProdigia { assert.Equal(t, "PRODIGIA", imp.Origen) // nunca "ERP" }}Precisión en la comparación de UUIDs
Los UUIDs SAT deben compararse siempre con strings.EqualFold. El uso del operador == o de strings.ToLower manual produce falsos negativos cuando el UUID de una fuente está en mayúsculas y el de la otra en minúsculas.
// Correctoif strings.EqualFold(cfdi.UUID, uuidBuscado) { ... }
// Incorrecto — sensible a mayúsculasif cfdi.UUID == uuidBuscado { ... }
// Incorrecto — normalización manual no garantizadaif strings.ToLower(cfdi.UUID) == strings.ToLower(uuidBuscado) { ... }RFC multi-empresa en conciliación
Cuando se ejecuta una conciliación, el RFC que se usa para consultar el SAT es siempre empresa.GetProdigiaRFC(), no empresa.RFC. Esto es especialmente relevante en el modo TST, donde la empresa tiene un RFC de prueba en la base de datos pero el RFC real de la empresa en las consultas externas:
// Correcto — usa el RFC configurado para el proveedor SATrfcParaSAT := empresa.GetProdigiaRFC()
// Incorrecto — puede ser el RFC de prueba (TST)rfcParaSAT := empresa.RFCConciliación manual
Además del flujo automático, el sistema soporta conciliación manual de XMLs individuales. Esta funcionalidad permite al contador cargar XMLs obtenidos por otros medios y cruzarlos contra el ERP sin pasar por el proceso de descarga del SAT.
POST /api/conciliacion/manualGET /api/conciliacion/manual/xml/{uuid}GET /api/conciliacion/manual/solicitud/{uuid}/xmlsGET /api/conciliacion/manual/solicitud/{uuid}/download-zipExportación de resultados
El resultado de una conciliación puede exportarse en formato Excel para revisión detallada antes de generar el reporte fiscal oficial:
GET /api/conciliacion/{uuid}/exportGET /api/conciliacion/{id}/columnas_impuestosEl endpoint columnas_impuestos devuelve la estructura dinámica de columnas de impuestos presentes en la conciliación, que varía según los tipos de facturas incluidas y las tasas de IVA detectadas.
Testing de la conciliación
Por ser una función pura, MakeConciliacion se puede probar con tests unitarios sin ninguna dependencia de infraestructura. El patrón recomendado es table-driven con casos que cubran todas las categorías:
func TestMakeConciliacion_FacturaEnAmbos_ConciliacionCorrecta(t *testing.T) { uuid := "550e8400-e29b-41d4-a716-446655440000" facturasERP := []factura.FacturaERP{ {UUID: uuid, Total: 1160.00}, } facturasProdigia := []factura.FacturaProdigia{ {UUID: strings.ToUpper(uuid), Total: 1160.00}, // UUID en mayúsculas }
result := MakeConciliacion("RFC123", "sol-uuid", facturasERP, facturasProdigia, nil)
require.Len(t, result.FacturasConciliadas, 1) assert.Equal(t, "AMBOS", result.FacturasConciliadas[0].Origen) assert.True(t, result.FacturasConciliadas[0].Conciliado)}Resumen
MakeConciliaciones una función pura del dominio: sin efectos secundarios, determinística y testeable sin infraestructura.- El algoritmo clasifica cada factura en
AMBOS,ERP,PRODIGIAoCANCELADOusando UUID como llave constrings.EqualFold. - Los arrays
ImpuestosERPeImpuestosProdigianunca se mezclan; esta invariante permite detectar discrepancias exactas de tasas entre fuentes. - El RFC para consultas SAT es siempre
empresa.GetProdigiaRFC(), noempresa.RFC. - El estado
Conciliadaes el único estado final de una solicitud; ninguna operación puede revertirlo.