# 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!`