7.9 KiB
Refactor IVA — fórmula del owner (2026-04-26)
Cambio mayor en la fórmula del IVA causado/acreditable de /impuestos.
El owner pidió alinear el cálculo a un spec explícito que difiere en tres
puntos clave del código previo. Doc de las fórmulas, los cambios SQL
puntuales y la validación con Husberto.
Relacionado:
docs/plans/2026-04-26-i07-ppd-compensacion.md§8 (rama nueva I PPD/07 que se conserva en este refactor).
1. Fórmula del owner
IVA Trasladado (lado emisor del contribuyente)
| Componente | Filtros | Campo IVA |
|---|---|---|
| (+) I PUE emisor | tipo='I' AND metodo_pago='PUE', régimen emisor en lista, vigente |
iva_traslado_mxn − iva_retencion_mxn |
| (+) P emisor | tipo='P', régimen emisor en lista, vigente |
iva_traslado_pago_mxn − iva_retencion_pago_mxn |
| (+) I PPD/07 emisor — hereda | tipo='I' AND metodo_pago='PPD' AND tipoRel='07', régimen emisor en lista |
suma de IVA neto de E que la referencien en mismo mes |
| (−) E PUE emisor | tipo='E' AND metodo_pago='PUE', régimen emisor en lista |
iva_traslado_mxn − iva_retencion_mxn |
IVA Acreditable (lado receptor)
Simétrico: cambia rfc_emisor → rfc_receptor y regimen_fiscal_emisor → regimen_fiscal_receptor. El componente "I PPD/07 hereda" busca E del lado
receptor que la referencien.
Reglas globales
- Régimenes considerados:
605, 606, 612, 621, 625, 626, 601, 603, 607, 608, 610, 611, 614, 615, 620, 622, 623, 624(excluye 616 público en general, 614 ingresos por intereses, etc. según lista del owner). - Filtro de régimen por lado: el régimen del lado del contribuyente — emisor cuando vende, receptor cuando compra.
- Conceptos excluidos: claves prod/serv
84121603(seguros),93161608(gobierno),85101501(salud),85121800(servicios médicos) se restan del IVA del CFDI. - Tipo P: usa
iva_traslado_pago_mxnyiva_retencion_pago_mxndirectos, sin clamp (vs el código previo que aplicabaLEAST(iva, monto*0.16)como defensa contra XMLs malformados). - E con tipoRel=07: SÍ entran al NEG y restan IVA. El owner asume que el contador emite la E/07 cuando se cancela el anticipo. Si no se emite, el IVA del anticipo se sobrecausa (riesgo aceptado).
- I PUE/07: aporta IVA completo, sin compensación contra los anticipos referenciados (el código previo restaba el IVA del anticipo para evitar doble conteo cuando E/07 ausente).
2. Diferencias vs código previo
| Aspecto | Antes | Ahora |
|---|---|---|
| Clamp IVA en P | LEAST(iva, monto×0.16) |
Campo directo |
| Compensación I PUE/07 | GREATEST(0, IVA − Σ IVA anticipos) |
Sin compensación, IVA completo |
| E con tipoRel=07 | Excluida del NEG (filtro <> '07'), excepto si apuntaba a I PPD/07 |
Todas las E PUE entran al NEG |
bucketCausadoNeg/bucketAcreditableNeg |
Compleja con OR E_REFERENCIA_I_PPD_07 |
Simple: E PUE del lado |
Predicado E_REFERENCIA_I_PPD_07_MISMO_MES |
Existía | Eliminado (ya no necesario) |
IS_I_PUE_07, SUM_REL_TRAS, SUM_REL_RET |
Existían | Eliminados |
IS_I_PPD_07, SUM_E_REFERENCING_TRAS/RET, HAS_E_REFERENCING_MISMO_MES |
Existían | Conservados (rama nueva I PPD/07) |
| Presentación KPI | Trasladado / Acreditable / Retenido separados | Igual: separados, fórmula T − A − R |
3. Cambios concretos en apps/api/src/services/impuestos.service.ts
Eliminados
IS_I_PUE_07SUM_REL_TRAS,SUM_REL_RETE_REFERENCIA_I_PPD_07_MISMO_MES
Modificados
IVA_TRAS_EXPR,IVA_RET_EXPR: rama de tipo P sinLEAST(...).IVA_TRAS_EXPR_ALIAS,IVA_RET_EXPR_ALIAS: idem para subqueries.bucketCausadoNeg,bucketAcreditableNeg: simplificados aE PUE del lado correctosin filtros tipoRel ni rama EXISTS.signedCausadoTras/Ret,signedAcreditableTras/Ret: removida la ramaWHEN bucket POS AND IS_I_PUE_07 THEN GREATEST(0, IVA − SUM_REL). Quedan tres ramas: POS, I PPD/07 hereda, NEG.
Conservados sin cambios
IS_I_PPD_07SUM_E_REFERENCING_TRAS,SUM_E_REFERENCING_RETHAS_E_REFERENCING_MISMO_MESbucketCausadoAny,bucketAcreditableAny(solo usanHAS_E_REFERENCING_MISMO_MES, no el predicado eliminado)- Bloques de presentación KPI en
getResumenIvaygetIvaMensual
4. Validación con caso real
Husberto Ignacio Torres (RFC TOAH680201RA2), agosto 2025:
| KPI | Antes refactor | Después refactor |
|---|---|---|
| Trasladado | $119,093.08 | $111,781.45 |
| Acreditable | $147,023.59 | $182,683.84 |
| Retenido | $0.00 | $0.00 |
| Resultado IVA | −$27,930.51 | −$70,902.39 |
Delta resultado: −$42,971.88 a favor del contribuyente. La diferencia se origina en:
- Compensación I PUE/07 removida: 11 I PUE/07 del mes con $48,197 IVA bruto. Antes aportaban su remanente vs anticipos; ahora aportan completo → +acreditable.
- E/07 que cancelaba anticipos PUE ahora resta: antes excluida del NEG; ahora entra → más NC en el cálculo.
- Sin clamp P: P recibidas con IVA reportado mayor al 16% del pago ya no se truncan → +acreditable.
Validación numérica (breakdown bruto agosto 2025 lado receptor):
- I PUE recibidas: $186,714.60
- P recibidas: $43,659.91
- I PPD/07 hereda IVA de E: $21,793.10
- E PUE recibidas (resta): −$69,483.77
- Total Acreditable: $182,683.84 ✓
Lado emisor:
- I PUE emisor: $111,781.45
- P emisor: $0
- I PPD/07 emisor hereda: $0
- E PUE emisor (resta): $0
- Total Trasladado: $111,781.45 ✓
5. Riesgos y trade-offs aceptados
Sobrecausa cuando E/07 ausente
Sin compensación I PUE/07, el flujo anticipo I PUE + I PUE/07 sin E/07
sobrecausa por el monto del anticipo. En Husberto agosto 2025 hay 11 I
PUE/07 con 0 E/07 emitidas → todo ese volumen actualmente sobrecausa.
El owner aceptó este trade-off bajo la premisa fiscal: "lo correcto es que el contador emita la E/07 cuando aplica el anticipo". Si en producción se detectan tenants donde sistemáticamente faltan las E/07, la decisión deberá revisarse (revertir a compensación o introducir un toggle por tenant).
Sin clamp en P
XMLs de proveedores que reportan el IVA de la factura completa en P
parciales causan un IVA acreditable inflado. El código previo defendía con
LEAST(iva, monto×0.16). Ahora se confía en que el campo del XML sea
correcto.
Divergencia con dashboard
apps/api/src/services/dashboard.service.ts mantiene su lógica de IVA
balance independiente. Después de este cambio, los KPIs del dashboard
podrían diferir de /impuestos. Pendiente: alinear (o documentar la
diferencia intencional).
6. Cache metricas_mensuales
El cambio invalida silenciosamente todas las filas pre-calculadas en
metricas_mensuales (cualquier periodo cerrado por contribuyente). Para
repoblar:
-- Borrar cache de un contribuyente específico:
DELETE FROM metricas_mensuales
WHERE contribuyente_id IN (SELECT entidad_id FROM contribuyentes WHERE rfc = 'XXX');
-- O global del tenant (si se rehace para todos):
DELETE FROM metricas_mensuales;
Después, las consultas a años cerrados caerán al path on-the-fly hasta
que el cron computeMetricaMensual repueble la tabla.
7. Pendientes
- Recompute bulk de
metricas_mensualespara todos los tenants y años pasados con la fórmula nueva (ahora mismo solo limpiamos la cache de Husberto 2025). - Validar otros tenants: el delta esperado depende del volumen de I PUE/07 sin E/07 contraparte. Tenants que no usen el patrón de anticipo no verán cambio significativo; los que sí lo usen verán acreditable subir.
- Alinear dashboard: si los KPIs de
/dashboardy/impuestosdivergen, decidir cuál fórmula es la canónica. - Documentar para usuarios finales: el cambio en el resultado IVA es notable (~−$43K en Husberto agosto). Si se va a desplegar a producción, preparar nota de release explicando por qué cambian los números.