Skip to content

Reglas de Precisión SAT

El SAT mexicano tiene tolerancia cero a desviaciones en los cálculos fiscales. Cualquier violación —redondeo incorrecto, comparación imprecisa de montos, mezcla de arrays de impuestos— produce diferencias en las declaraciones que obligan a presentar declaraciones complementarias. Este artículo documenta las reglas absolutas que todo código de GM Fiscal debe seguir.

Tabla de reglas absolutas

OperaciónMétodo correctoMétodo prohibido
Redondear montosR21MathUtils.RedondearMonto(x) — half-upmath.Round(x) · fmt.Sprintf("%.2f")
Comparar montosR21MathUtils.CompararMontos(a, b) — tolerancia 0.005a == b · a - b < 0.01
Comparar UUIDsstrings.EqualFold(uuid1, uuid2)uuid1 == uuid2 · strings.ToLower manual
RFC para SAT/Prodigiaempresa.GetProdigiaRFC()empresa.RFC
RFC para ERP/Importaempresa.GetServicesRFC()empresa.RFC
RFC para BDempresa.GetDatabaseRFC() o empresa.RFC
Parsear XML SATRemoveNamespaces(xml)xml.Unmarshalxml.Unmarshal sin RemoveNamespaces
Impuestos ERPArray ImpuestosERP separadoMezclar con ImpuestosProdigia
Impuestos SATArray ImpuestosProdigia separadoMezclar con ImpuestosERP
CFDIs cancelados en R21Excluir siempreIncluir aunque sea como 0

Redondeo half-up SAT

El SAT utiliza el método de redondeo half-up (también llamado redondeo comercial): cuando el dígito a redondear es exactamente 0.5, se redondea hacia arriba. Las funciones nativas de Go usan el método round half to even (también llamado banker’s rounding), que redondea hacia el número par más cercano. Esta diferencia produce montos incorrectos en declaraciones fiscales.

// Correcto — half-up SAT
mathUtils := reporte.NewR21MathUtils()
total := mathUtils.RedondearMonto(1160.005) // → 1160.01 (half-up, sube)
total2 := mathUtils.RedondearMonto(1160.015) // → 1160.02 (half-up, sube)
// Incorrecto — math.Round usa round half to even
total := math.Round(1160.005*100) / 100 // → 1160.00 (round half to even, baja)
// Incorrecto — fmt.Sprintf no garantiza half-up
total := fmt.Sprintf("%.2f", 1160.005) // comportamiento no definido por SAT

Comparación de montos con tolerancia

Los montos en punto flotante nunca deben compararse con el operador ==. Los errores de precisión de float64 producen comparaciones falsas para montos que matemáticamente son iguales.

// Correcto — tolerancia de 0.005
iguales := mathUtils.CompararMontos(1.00, 1.004) // → true (dentro de tolerancia)
iguales2 := mathUtils.CompararMontos(1.00, 1.006) // → false (fuera de tolerancia)
// Incorrecto — float64 precision errors
if factura.Total == 1160.00 { } // puede fallar con montos idénticos
// Incorrecto — tolerancia arbitraria no SAT
if math.Abs(a-b) < 0.01 { } // tolerancia incorrecta

Interfaz R21MathUtils

src/domain/reporte/r21_math.go
type R21MathUtils interface {
RedondearMonto(x float64) float64
// Half-up según normativa SAT. 0.005 → 0.01, 0.015 → 0.02.
CompararMontos(a, b float64) bool
// Igualdad con tolerancia 0.005. Para comparar totales e impuestos.
CalcularBaseIVA16(iva float64) float64
// Base imponible para IVA 16%: iva / 0.16
CalcularBaseIVA8(iva float64) float64
// Base imponible para IVA 8% (zona fronteriza): iva / 0.08
AcumularMontos(montos []float64) float64
// Suma de montos con redondeo correcto en cada paso intermedio.
}
// Uso:
math := reporte.NewR21MathUtils()
base := math.CalcularBaseIVA16(160.00) // → 1000.00
total := math.RedondearMonto(1160.005) // → 1160.01
igual := math.CompararMontos(1.00, 1.004) // → true

Parsing XML CFDI

El XML de los CFDIs SAT incluye prefijos de namespace (cfdi:, tfd:, imp:) que interfieren con xml.Unmarshal de Go. El parseo sin limpiar estos prefijos puede completarse sin error pero con campos vacíos, produciendo datos incorrectos de forma silenciosa.

Flujo obligatorio de parsing

// 1. Siempre RemoveNamespaces antes de Unmarshal
xmlLimpio := xml_utils.RemoveNamespaces(xmlRaw)
// 2. Unmarshal al struct CFDI
var cfdi dtos.CFDI
if err := xml.Unmarshal([]byte(xmlLimpio), &cfdi); err != nil {
return fmt.Errorf("parsear CFDI: %w", err)
}
// 3. UUID siempre con EqualFold
if strings.EqualFold(cfdi.UUID, uuidBuscado) { ... }
// 4. Montos: parsear como float64 con TrimSpace
monto, err := strconv.ParseFloat(strings.TrimSpace(cfdi.Total), 64)
if err != nil {
return fmt.Errorf("parsear monto Total: %w", err)
}
// 5. Redondear inmediatamente después de parsear
montoRedondeado := mathUtils.RedondearMonto(monto)

Estructura del XML CFDI v4.0

<cfdi:Comprobante
Version="4.0"
TipoDeComprobante="I|E|P|N|T"
Total="1160.00"
SubTotal="1000.00"
NoCertificado="...">
<cfdi:Emisor Rfc="RFC_EMISOR" Nombre="..." RegimenFiscal="601"/>
<cfdi:Receptor Rfc="RFC_RECEPTOR" UsoCFDI="G01"/>
<cfdi:Impuestos TotalImpuestosTraslados="160.00">
<cfdi:Traslados>
<cfdi:Traslado
Impuesto="002"
TipoFactor="Tasa"
TasaOCuota="0.160000"
Importe="160.00"/>
</cfdi:Traslados>
</cfdi:Impuestos>
<cfdi:Complemento>
<tfd:TimbreFiscalDigital UUID="550e8400-e29b-41d4-a716-446655440000"/>
</cfdi:Complemento>
</cfdi:Comprobante>

Los campos clave son:

CampoValores posiblesSignificado
TipoDeComprobanteI, E, P, N, TIngreso, Egreso, Pago, Nómina, Traslado
Impuesto001, 002, 003ISR, IVA, IEPS
TasaOCuota0.160000, 0.080000, 0.00000016%, 8%, 0%
TipoFactorTasa, Retencion, ExentoTipo de aplicación del impuesto

RFC multi-empresa

La entidad Empresa puede tener hasta tres RFC distintos. El uso del campo incorrecto en consultas externas produce errores de autenticación o resultados vacíos:

// Correcto
empresa.GetProdigiaRFC() // para consultas a Prodigia, SAT, Hades
empresa.GetServicesRFC() // para consultas al ERP e Importa
empresa.GetDatabaseRFC() // para identificación en BD (= empresa.RFC)
// Incorrecto — puede ser el RFC de prueba en modo TST
empresa.RFC
erp.GetFacturas(empresa.RFC) // MAL
prodigia.GetCFDIs(empresa.RFC) // MAL

En el modo TST la empresa tiene RFC="TST123" en la base de datos pero ProdigiaRFC="RFC_REAL_EMPRESA". Las consultas al SAT deben usar el RFC real; de lo contrario devuelven cero facturas.

Anti-patrones prohibidos

El código de GM Fiscal prohíbe explícitamente los siguientes patrones. Su presencia en un pull request es razón suficiente para rechazarlo:

// PROHIBIDO: comparar montos con ==
if factura.Total == 1160.00 { }
// PROHIBIDO: redondear con math.Round
total := math.Round(monto*100) / 100
// PROHIBIDO: comparar UUIDs con ==
if cfdi.UUID == "550e8400..." { }
// PROHIBIDO: usar RFC base para consultas externas
erp.GetFacturas(empresa.RFC)
// PROHIBIDO: mezclar arrays de impuestos
fc.Impuestos = append(erpImps, prodigiaImps...)
// PROHIBIDO: Unmarshal XML SAT sin limpiar namespaces
xml.Unmarshal(xmlRaw, &cfdi)
// PROHIBIDO: incluir CFDIs cancelados en R21
if cfdi.EstatusERP == "CANCELADO" { calcularIVA(cfdi) }

Invariantes verificables en tests

Las siguientes invariantes se deben verificar en los tests de la capa de dominio. Si alguna falla, indica un error en la lógica de conciliación o procesamiento de impuestos:

// Impuestos nunca mezclados
for _, fc := range conciliacion.FacturasConciliadas {
for _, imp := range fc.ImpuestosERP {
assert.Equal(t, "ERP", imp.Origen)
}
for _, imp := range fc.ImpuestosProdigia {
assert.Equal(t, "PRODIGIA", imp.Origen)
}
}
// Solicitud Conciliada es inmutable
_, err := uc.Execute(CambiarEstatusCommand{UUID: sol.UUID, Estatus: "Listo para conciliar"})
assert.True(t, shared.IsInvalidTransition(err))
// UUID insensible a mayúsculas
uuid := "550E8400-E29B-41D4-A716-446655440000"
uuidMinusculas := strings.ToLower(uuid)
assert.True(t, strings.EqualFold(uuid, uuidMinusculas))

Resumen

  • El redondeo half-up es obligatorio para todo monto fiscal. math.Round y fmt.Sprintf("%.2f") están prohibidos.
  • Los montos se comparan con R21MathUtils.CompararMontos con tolerancia 0.005, nunca con el operador ==.
  • Los UUIDs SAT se comparan siempre con strings.EqualFold por insensibilidad a mayúsculas/minúsculas.
  • RemoveNamespaces() antes de xml.Unmarshal es obligatorio; omitirlo produce parseo silencioso con campos vacíos.
  • Los arrays ImpuestosERP e ImpuestosProdigia nunca se mezclan; la invariante es verificable en tests unitarios.
  • empresa.RFC nunca se usa directamente en consultas externas; siempre se usa GetProdigiaRFC() o GetServicesRFC().