Files
HoruxDespachosNuevo/docs/plans/2026-04-26-iva-refactor.md

194 lines
7.9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.