# 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: ```sql -- 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.