# 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.
