Files
HoruxDespachos/docs/plans/2026-04-26-iva-refactor.md
2026-04-27 22:09:36 -06:00

7.9 KiB
Raw Permalink Blame History

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_mxn y iva_retencion_pago_mxn directos, sin clamp (vs el código previo que aplicaba LEAST(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_07
  • SUM_REL_TRAS, SUM_REL_RET
  • E_REFERENCIA_I_PPD_07_MISMO_MES

Modificados

  • IVA_TRAS_EXPR, IVA_RET_EXPR: rama de tipo P sin LEAST(...).
  • IVA_TRAS_EXPR_ALIAS, IVA_RET_EXPR_ALIAS: idem para subqueries.
  • bucketCausadoNeg, bucketAcreditableNeg: simplificados a E PUE del lado correcto sin filtros tipoRel ni rama EXISTS.
  • signedCausadoTras/Ret, signedAcreditableTras/Ret: removida la rama WHEN 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_07
  • SUM_E_REFERENCING_TRAS, SUM_E_REFERENCING_RET
  • HAS_E_REFERENCING_MISMO_MES
  • bucketCausadoAny, bucketAcreditableAny (solo usan HAS_E_REFERENCING_MISMO_MES, no el predicado eliminado)
  • Bloques de presentación KPI en getResumenIva y getIvaMensual

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:

  1. 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.
  2. E/07 que cancelaba anticipos PUE ahora resta: antes excluida del NEG; ahora entra → más NC en el cálculo.
  3. 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_mensuales para 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 /dashboard y /impuestos divergen, 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.