# ISR — Base gravable acumulada y desglose del periodo ## Contexto En `/impuestos` (pestaña ISR) hay dos lugares donde la base gravable se calcula mes a mes en lugar de acumulado, lo cual es fiscalmente incorrecto para pagos provisionales mensuales: 1. **Tabla "Histórico ISR"** (`apps/web/app/(dashboard)/impuestos/page.tsx`, líneas ~503-568): cada fila aplica `Math.max(0, ing_mes − ded_mes)` por mes independiente. Resultado: un mes con pérdida no reduce el acumulado. 2. **Sección "Cálculo de ISR Acumulado"** (mismas líneas ~371-432): muestra los totales del rango filtrado en `resumenIsr`, sin distinguir lo que ya estaba acumulado de meses previos del mismo año vs. el periodo actual. El bug raíz vive en `getIsrMensual` (`apps/api/src/services/impuestos.service.ts`, líneas 409-486): el query corre de `${año}-${mm}-01` a fin de mes, así que el campo nombrado `ingresosAcumulados` en `IsrMensual` realmente trae solo el mes (deuda heredada del refactor previo, el nombre miente). ## Objetivo Mostrar la base gravable y los montos acumulados correctamente: 1. En la tabla, agregar columnas **Ingresos Acum.**, **Deducciones Acum.** y **Base Gravable Acum.** (estas tres son running totals desde enero hasta el mes de cada fila). La **BG mensual desaparece** del display — solo queda la acumulada, que es la única fiscalmente válida. 2. En la sección de cálculo, presentar el desglose como aparece en el formato 14 (declaración provisional mensual del SAT): ``` Ingresos del periodo + Ingresos anteriores − Deducciones del periodo − Deducciones anteriores = Base gravable acumulada ``` Donde **"del periodo" = mes final del filtro** y **"anteriores" = enero hasta el mes anterior al final**. ## Reglas fiscales - **No se aplica `max(0, ...)` al display** de base gravable. Los déficits son reales y se muestran negativos (en rojo). Si filtras febrero y enero tuvo utilidad pero febrero pérdida grande, `BG_acum_feb` puede ser negativa. - **`max(0, ...)` se aplica únicamente al pasar a ISR causado**: si `BG_acum < 0`, ISR causado = 0. SAT hace lo mismo en el formato 14. - **El año fiscal se resetea en enero**. "Anteriores" jamás cruza a años previos. ## Cambios — Backend ### `apps/api/src/services/impuestos.service.ts` **`getIsrMensual` (líneas 409-486):** Después del loop que llena `result[]` con datos mensuales, agregar un segundo pase que computa los running totals: ```ts let ingAcum = 0, dedAcum = 0; for (const row of result) { ingAcum += row.ingresosAcumulados; // (mensual, a pesar del nombre) dedAcum += row.deducciones; row.ingresosAcum = ingAcum; row.deduccionesAcum = dedAcum; row.baseGravableAcum = ingAcum - dedAcum; // sin clamp } ``` Nota sobre naming: el campo existente `ingresosAcumulados` en `IsrMensual` se mantiene por compat (es el mensual). Los nuevos campos son `ingresosAcum`, `deduccionesAcum`, `baseGravableAcum`. En el spec del rename total al final puede ocurrir, pero no es scope de este cambio. **Nueva función exportada** `getResumenIsrDesglosado`: ```ts export async function getResumenIsrDesglosado( pool: Pool, fechaFin: string, tenantId: string, conciliacion?: boolean, contribuyenteId?: string | null, ): Promise<{ delPeriodo: ResumenIsr; anteriores: ResumenIsr; total: ResumenIsr; }> ``` Lógica: 1. Derivar `año = fechaFin.year`, `mesFinal = fechaFin.month`. 2. Tres rangos: - **delPeriodo**: `${año}-${mesFinal}-01` a fin de `mesFinal` (solo mes final) - **anteriores**: `${año}-01-01` a `${año}-${mesFinal-1}-${ultDia}` (Ene a mesFinal-1; vacío si mesFinal=1) - **total**: `${año}-01-01` a fin de `mesFinal` (Ene a mesFinal) 3. Llamar `getResumenIsr` 3 veces con esos rangos, retornar el objeto. Caso `mesFinal=1`: retornar `anteriores` con todos los campos en cero (no se hace query inútil). ### `apps/api/src/controllers/impuestos.controller.ts` Agregar handler `getResumenIsrDesglosado`: ```ts // GET /api/impuestos/resumen-isr-desglosado?fechaFin=...&conciliacion=...&contribuyenteId=... ``` El filtro por régimen no se pasa al endpoint — el frontend hace el lookup contra `resumenIsr.baseGravablePorRegimen[]` igual que hoy con `useResumenIsr`, para que la lógica de filtrado siga centralizada en un solo lugar. ### `apps/api/src/routes/impuestos.routes.ts` Agregar la ruta `/resumen-isr-desglosado` con los mismos middlewares que `/resumen-isr` (auth + tenant + plan limits). ## Cambios — Shared types ### `packages/shared/src/types/reportes.ts` (o donde viva `IsrMensual`) Agregar campos al type: ```ts export interface IsrMensual { // ...campos existentes ingresosAcum: number; deduccionesAcum: number; baseGravableAcum: number; // sin clamp, puede ser negativo } export interface ResumenIsrDesglosado { delPeriodo: ResumenIsr; anteriores: ResumenIsr; total: ResumenIsr; } ``` ## Cambios — Frontend ### `apps/web/lib/api/impuestos.ts` Agregar función `getResumenIsrDesglosado` (cliente HTTP) y hook `useResumenIsrDesglosado` en `apps/web/lib/hooks/use-impuestos.ts`. ### `apps/web/app/(dashboard)/impuestos/page.tsx` **Tabla "Histórico ISR" (líneas ~502-568):** Headers (6 columnas): ``` Mes | Ingresos | Ingresos Acum. | Deducciones | Deducciones Acum. | Base Gravable Acum. ``` Body por fila: ```tsx