# Sesión 2026-04-18 / 2026-04-19 — Fixes y Features

## Resumen

Sesión intensiva de correcciones del pivot Tenant→Contribuyente, mejoras de UX, nuevas funcionalidades (declaraciones, carteras, ISR mensual), y correcciones fiscales.

---

## 1. Página cancelaciones-periodo-anterior (NUEVA)

**Problema:** La alerta "Facturas de periodos anteriores canceladas" generaba un link a `/alertas/cancelaciones-periodo-anterior` que no existía (404).

**Solución:** Creada la página drilldown similar a `/alertas/cancelaciones`, mostrando CFDIs cuya `fecha_cancelacion` cae en el mes actual pero `fecha_emision` es de meses anteriores.

**Archivos:**
- `apps/web/app/(dashboard)/alertas/cancelaciones-periodo-anterior/page.tsx` (nuevo)

---

## 2. Filtro de regímenes por contribuyente

**Problema:** El dropdown de regímenes en Dashboard/Impuestos/Reportes mostraba regímenes de TODOS los contribuyentes del despacho, no solo del seleccionado.

**Solución:** Propagación de `selectedContribuyenteId` a través de toda la cadena: hook → API client → controller → service (`getRegimenesDelPeriodo` ahora acepta `contribuyenteId` y filtra con `AND contribuyente_id = '...'`).

**Archivos:**
- `apps/api/src/services/dashboard.service.ts` — `getRegimenesDelPeriodo` + `contribuyenteId`
- `apps/api/src/controllers/dashboard.controller.ts` — extrae `contribuyenteId` de query
- `apps/web/lib/api/dashboard.ts` — pasa `contribuyenteId`
- `apps/web/lib/hooks/use-dashboard.ts` — `useRegimenesDelPeriodo` usa `selectedContribuyenteId`

---

## 3. Discrepancia de régimen — regímenes del contribuyente

**Problema:** La alerta y drilldown de discrepancia comparaba CFDIs contra los regímenes del tenant (central), no del contribuyente seleccionado.

**Solución:** Nueva función `getRegimenesActivosClavesEfectivos(tenantId, pool, contribuyenteId?)` que lee de `contribuyentes.regimen_fiscal` cuando hay contribuyenteId, o fallback a `TenantRegimenActivo`.

**Archivos:**
- `apps/api/src/services/regimen.service.ts` — nueva función
- `apps/api/src/services/alertas-auto.service.ts` — usa la nueva función
- `apps/api/src/controllers/alertas.controller.ts` — usa la nueva función en drilldown

---

## 4. Discrepancia de régimen — filtros de fecha y régimen

**Problema:** No había forma de filtrar los CFDIs con discrepancia por fecha o por régimen específico.

**Solución:** Filtros client-side (Desde/Hasta/Régimen) en la página de discrepancia. El dropdown de régimen se construye dinámicamente de los valores únicos presentes.

**Archivos:**
- `apps/web/app/(dashboard)/alertas/discrepancia-regimen/page.tsx` — filtros + `contribuyenteId`

---

## 5. Discrepancias — excluir cancelados

**Problema:** CFDIs cancelados aparecían en la alerta de discrepancia.

**Solución:** Doble filtro: `status NOT IN ('Cancelado', '0') AND fecha_cancelacion IS NULL`.

**Archivos:**
- `apps/api/src/services/alertas-auto.service.ts` — alerta count
- `apps/api/src/controllers/alertas.controller.ts` — drilldown query

---

## 6. Obligaciones — demasiadas y hacia el pasado

**Problema 1:** CSF importaba obligaciones históricas (con `fechaFin`). **Fix:** Filtro `!ob.fechaFin`.

**Problema 2:** Obligaciones aparecían como "atrasadas" para meses anteriores a su creación. **Fix:** `periodo >= obStartPeriodo` (derivado de `created_at`).

**Problema 3:** Re-ejecutar "Generar recomendaciones" duplicaba. **Fix:** `DELETE WHERE es_recomendada = true` antes de insertar.

**Archivos:**
- `apps/api/src/services/obligaciones.service.ts`

---

## 7. Matching de obligaciones CSF → catálogo (MEJORA)

**Problema:** Match por "primeras 3 palabras" fallaba con variantes del SAT (ej: "Pago provisional mensual" vs "Pago provisional de").

**Solución:** Sistema de keyword sets discriminantes. 15 reglas con múltiples variantes, normalización sin acentos, distinción PM/PF por longitud de RFC.

**Análisis base:** 17 CSFs reales analizadas → 22 obligaciones únicas del SAT mapeadas.

**Archivos:**
- `apps/api/src/services/obligaciones.service.ts` — `CATALOG_MATCH_RULES` + `matchCsfToCatalog()`

---

## 8. Calendario — fix crash por tipo desconocido

**Problema:** `tipoIcons[e.tipo]` retornaba `undefined` para eventos con tipo no mapeado, crasheando React.

**Solución:** Fallback `|| Calendar` en el grid del calendario (la lista lateral ya lo tenía).

**Archivos:**
- `apps/web/app/(dashboard)/calendario/page.tsx`

---

## 9. Alertas del dashboard filtradas por contribuyente

**Problema:** Las alertas automáticas del dashboard (`/dashboard/alertas`) no filtraban por contribuyente, mostrando alertas de todos los RFCs. El drilldown sí filtraba, causando "alerta visible pero drilldown vacío".

**Solución:** Propagación de `contribuyenteId` al endpoint `/dashboard/alertas` y al hook `useAlertas`.

**Archivos:**
- `apps/api/src/controllers/dashboard.controller.ts` — `getAlertas` + `contribuyenteId`
- `apps/web/lib/api/dashboard.ts` — `getAlertas` acepta `contribuyenteId`
- `apps/web/lib/hooks/use-dashboard.ts` — `useAlertas` usa `selectedContribuyenteId`

---

## 10. Alertas page — filtro por contribuyente

**Problema:** La página `/alertas` tenía queries inline que no pasaban `contribuyenteId`. Al cambiar contribuyente, los datos no se actualizaban.

**Solución:** Ambas queries (`alertas-automaticas` y `alertas-manuales`) ahora incluyen `selectedContribuyenteId` en query key y lo pasan como parámetro.

**Archivos:**
- `apps/web/app/(dashboard)/alertas/page.tsx`

---

## 11. CFDI — eliminar staleTime

**Problema:** `useCfdis` tenía `staleTime: 30_000` que podía mostrar datos del contribuyente anterior por hasta 30 segundos.

**Solución:** Eliminado `staleTime` y `gcTime`.

**Archivos:**
- `apps/web/lib/hooks/use-cfdi.ts`

---

## 12. Declaraciones — renombrada + periodicidad + monto

**Cambios:**
- Tab renombrada de "Declaraciones Provisionales" a "Declaraciones"
- Selector de periodicidad (mensual/bimestral/trimestral/semestral/anual) con opciones dinámicas de periodo
- Campo "Monto a pagar" — si $0, auto-marca como pagado y resuelve alertas de pago
- Año seleccionable dentro del formulario (para declaración anual de ejercicio anterior)
- Filtro de fecha (Desde/Hasta) basado en `created_at` en lugar de filtro por año fiscal
- Columna "Fecha subida" en la tabla

**Migración:** `021_declaraciones_periodicidad_monto.sql`

**Archivos:**
- `apps/api/src/migrations/tenant/021_declaraciones_periodicidad_monto.sql`
- `apps/api/src/services/declaraciones.service.ts` — nuevos campos + auto-pago $0
- `apps/api/src/controllers/documentos.controller.ts` — Zod schema + filtro por fecha
- `apps/web/lib/api/declaraciones.ts` — tipos actualizados
- `apps/web/lib/hooks/use-declaraciones.ts` — filtro por fecha
- `apps/web/app/(dashboard)/documentos/page.tsx` — UI completa

---

## 13. Carteras y subcarteras (NUEVO)

**Modelo:**
```
Cartera (top-level) → supervisor_user_id
  └── Subcartera → auxiliar_user_id, parent_id
       └── Entidades (RFCs asignados)
```

**Flujo:**
- Owner crea cartera: si hay supervisores → selector; si no → se asigna a sí mismo
- Supervisor (u Owner) crea subcarteras dentro de una cartera, asignando un auxiliar
- Cada subcartera tiene su propio subset de RFCs
- Auxiliar ve solo los RFCs de sus subcarteras (`entidades-visibles.ts`)

**Invite auxiliar:** Requiere seleccionar supervisor. Se almacena en tabla `auxiliar_supervisores`.

**Migración:** `022_carteras_subcarteras.sql` — `parent_id`, `auxiliar_user_id` en carteras + tabla `auxiliar_supervisores`

**Archivos:**
- `apps/api/src/migrations/tenant/022_carteras_subcarteras.sql`
- `apps/api/src/services/cartera.service.ts` — subcarteras + getSupervisores
- `apps/api/src/controllers/cartera.controller.ts` — endpoints subcarteras
- `apps/api/src/routes/cartera.routes.ts` — rutas nuevas
- `apps/api/src/controllers/usuarios.controller.ts` — invite con `supervisorUserId`
- `apps/api/src/utils/entidades-visibles.ts` — auxiliar ve subcarteras
- `apps/web/app/(dashboard)/carteras/page.tsx` — página completa reescrita
- `apps/web/app/(dashboard)/usuarios/page.tsx` — selector supervisor al invitar auxiliar
- `apps/web/lib/api/carteras.ts` — API client actualizado
- `apps/web/lib/hooks/use-carteras.ts` — hooks actualizados
- `packages/shared/src/types/user.ts` — `UserInvite.supervisorUserId`

---

## 14. ISR Mensual — tabla + Excel (NUEVO)

**Backend:** Nuevo endpoint `GET /impuestos/isr/mensual` que calcula ingresos, deducciones y base gravable por cada mes del año (excluyendo régimen 605).

**Frontend:** Tabla "Histórico ISR" con 12 meses + fila de totales + botón Excel. Similar a la tabla IVA existente.

**Excel IVA:** También agregado botón Excel a la tabla de Histórico IVA.

**Archivos:**
- `apps/api/src/services/impuestos.service.ts` — `getIsrMensual()`
- `apps/api/src/controllers/impuestos.controller.ts` — handler
- `apps/api/src/routes/impuestos.routes.ts` — ruta
- `apps/web/lib/api/impuestos.ts` — API client
- `apps/web/lib/hooks/use-impuestos.ts` — hook
- `apps/web/app/(dashboard)/impuestos/page.tsx` — tabla + Excel ambas

---

## 15. ISR — exclusión régimen 605 (Sueldos)

**Problema:** El régimen 605 (Sueldos y Salarios) se incluía en ingresos ISR, pero el patrón ya retuvo el ISR. No debe generar ingreso/deducción para ISR del contribuyente.

**Solución:**
- `getIsrMensual`: SQL filtra `AND regimen_fiscal_emisor != '605'`
- `getResumenIsr`: Filtra 605 de `ingresosPorRegimen`, `deduccionesPorRegimen`, y `regimenesConDatos`

**Dashboard:** 605 sigue mostrándose como ingreso general (correcto).

**Archivos:**
- `apps/api/src/services/impuestos.service.ts`

---

## 16. ISR — cálculo por régimen correcto

**Problema:** El frontend hacía `baseGravable * 0.30` para todos los regímenes. Incorrecto para PF (612 usa tarifa progresiva, no 30%).

**Solución:** `BaseGravableRegimen` ahora incluye `isrCausado` calculado en el backend con la fórmula correcta por régimen:
- 606, 612, 621, 625 → tarifa progresiva Art. 96
- 626 PF → tasa plana RESICO por bracket
- PM (601, etc.) → base × coeficiente × 30%

**KPI "ISR a Pagar":** Ahora usa el `isrCausado` del régimen seleccionado, consistente con la tabla de abajo.

**Coeficiente de Utilidad:** Se oculta cuando se selecciona un régimen PF (no aplica).

**Archivos:**
- `packages/shared/src/types/impuestos.ts` — `BaseGravableRegimen.isrCausado`
- `apps/api/src/services/impuestos.service.ts` — calcula y devuelve `isrCausado` per régimen
- `apps/web/app/(dashboard)/impuestos/page.tsx` — KPI usa per-régimen + oculta coeficiente PF

---

## Migraciones aplicadas

| # | Archivo | Descripción |
|---|---------|-------------|
| 021 | `021_declaraciones_periodicidad_monto.sql` | Periodicidad + monto_pago en declaraciones |
| 022 | `022_carteras_subcarteras.sql` | parent_id + auxiliar_user_id en carteras + auxiliar_supervisores |

## Usuarios creados

| Email | Rol | Despacho |
|---|---|---|
| supervisor@patito.com | Supervisor | Patito |
| auxiliar@patito.com | Auxiliar | Patito |
| cliente@patito.com | Cliente | Patito |

Contraseña: `Admin12345!`
