Skip to content

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.

src/domain/conciliacion/service.go
func MakeConciliacion(
rfcSolicitante string,
uuidSolicitud string,
facturasERP []factura.FacturaERP,
facturasProdigia []factura.FacturaProdigia,
previousConciliacionUUIDs []string,
) Conciliacion

La 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ónOrigenConciliado
UUID en ERP y en SAT, sin cancelaciónAMBOStrue
UUID solo en ERPERPfalse
UUID solo en SATPRODIGIAfalse
UUID en ERP con FechaCancelacion != nilAMBOS o ERP + EstatusERP = CANCELADOfalse

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.

// Correcto
if strings.EqualFold(cfdi.UUID, uuidBuscado) { ... }
// Incorrecto — sensible a mayúsculas
if cfdi.UUID == uuidBuscado { ... }
// Incorrecto — normalización manual no garantizada
if 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 SAT
rfcParaSAT := empresa.GetProdigiaRFC()
// Incorrecto — puede ser el RFC de prueba (TST)
rfcParaSAT := empresa.RFC

Conciliació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/manual
GET /api/conciliacion/manual/xml/{uuid}
GET /api/conciliacion/manual/solicitud/{uuid}/xmls
GET /api/conciliacion/manual/solicitud/{uuid}/download-zip

Exportació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}/export
GET /api/conciliacion/{id}/columnas_impuestos

El 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

  • MakeConciliacion es una función pura del dominio: sin efectos secundarios, determinística y testeable sin infraestructura.
  • El algoritmo clasifica cada factura en AMBOS, ERP, PRODIGIA o CANCELADO usando UUID como llave con strings.EqualFold.
  • Los arrays ImpuestosERP e ImpuestosProdigia nunca se mezclan; esta invariante permite detectar discrepancias exactas de tasas entre fuentes.
  • El RFC para consultas SAT es siempre empresa.GetProdigiaRFC(), no empresa.RFC.
  • El estado Conciliada es el único estado final de una solicitud; ninguna operación puede revertirlo.