# ISR — Base gravable acumulada y desglose del periodo — Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Mostrar la base gravable y los acumulados de ISR correctamente en la pestaña ISR de `/impuestos`. La tabla histórica gana 3 columnas acumuladas (Ingresos, Deducciones, Base Gravable Acum.) y pierde la BG mensual incorrecta. La sección "Cálculo de ISR del Periodo" muestra el desglose `del periodo + anteriores = total acumulado` como en el formato 14 del SAT.

**Architecture:** Cambio puramente de cómputo + UI. Backend agrega running totals a `getIsrMensual` y un nuevo endpoint `/impuestos/resumen-isr-desglosado` que llama 3 veces a `getResumenIsr` (mes final, anteriores, total) y los devuelve juntos. Frontend modifica la tabla y reescribe el card de cálculo. Sin migraciones, sin cambio en la BD.

**Tech Stack:** Express + TypeScript en el API, Next.js 14 + React Query en el web, types compartidos en `@horux/shared`. Verificación vía `pnpm typecheck` (este proyecto no tiene unit tests para esta área — la disciplina es typecheck + smoke manual, ver `feedback_horux360_tscheck.md`).

**Spec:** `docs/superpowers/specs/2026-04-27-isr-base-gravable-acumulada-design.md`

---

## File Structure

### Files to modify

```
packages/shared/src/types/impuestos.ts
  └── Extender IsrMensual con ingresosAcum, deduccionesAcum, baseGravableAcum
  └── Agregar interface ResumenIsrDesglosado

apps/api/src/services/impuestos.service.ts
  └── Modificar getIsrMensual (líneas 409-486): pase de running totals
  └── Agregar getResumenIsrDesglosado (función nueva exportada)

apps/api/src/controllers/impuestos.controller.ts
  └── Agregar handler getResumenIsrDesglosado

apps/api/src/routes/impuestos.routes.ts
  └── Agregar GET /isr/resumen-desglosado

apps/web/lib/api/impuestos.ts
  └── Agregar función getResumenIsrDesglosado (cliente HTTP)

apps/web/lib/hooks/use-impuestos.ts
  └── Agregar hook useResumenIsrDesglosado

apps/web/app/(dashboard)/impuestos/page.tsx
  └── Tabla Histórico ISR: 6 columnas, BG mensual fuera, BG_acum en rojo si negativa
  └── Sección "Cálculo de ISR del Periodo": rename + layout nuevo con desglose
```

### Files NOT touched

- BD: ningún cambio de schema.
- `metricas_mensuales` cache: sigue guardando mensuales puros.
- KPIs de la parte alta de `/impuestos`: siguen mostrando rango filtrado completo.
- IVA mensual: fuera de scope.

---

## Task 1: Extender shared types

**Files:**
- Modify: `packages/shared/src/types/impuestos.ts`

- [ ] **Step 1: Agregar campos acumulados a `IsrMensual`**

Editar el interface existente (líneas 16-28):

```ts
export interface IsrMensual {
  id: number;
  año: number;
  mes: number;
  ingresosAcumulados: number; // mensual — naming legacy, no se renombra en este spec
  deducciones: number;        // mensual
  baseGravable: number;       // mensual — sigue retornándose para no romper consumidores externos, pero ya no se muestra en la UI
  // Nuevos: running totals desde enero hasta el mes de esta fila
  ingresosAcum: number;
  deduccionesAcum: number;
  baseGravableAcum: number;   // sin clamp; puede ser negativo
  isrCausado: number;
  isrRetenido: number;
  isrAPagar: number;
  estado: EstadoDeclaracion;
  fechaDeclaracion: string | null;
}
```

- [ ] **Step 2: Agregar `ResumenIsrDesglosado` al final del archivo**

```ts
/**
 * Desglose del cálculo provisional ISR del mes final del filtro:
 *   delPeriodo  = solo el mes final del filtro (1 mes)
 *   anteriores  = enero hasta el mes anterior al final (puede estar vacío)
 *   total       = enero hasta el mes final inclusive
 *
 * Reglas:
 * - delPeriodo + anteriores = total para campos aditivos (ingresos, deducciones, retenciones).
 * - Para baseGravable e isrCausado el total se calcula sobre el rango entero
 *   (no es la suma algebraica de delPeriodo + anteriores).
 * - baseGravable puede ser negativa en cualquiera de los tres rangos.
 * - isrCausado se clampa a 0 cuando la baseGravable acumulada es negativa.
 */
export interface ResumenIsrDesglosado {
  delPeriodo: ResumenIsr;
  anteriores: ResumenIsr;
  total: ResumenIsr;
  /** Mes final del filtro (1-12) */
  mesFinal: number;
  /** Año fiscal del filtro */
  anio: number;
}
```

- [ ] **Step 3: Verificar que el package compile**

Run: `cd C:/Users/chtr1/Downloads/Horux_despacho && pnpm --filter @horux/shared typecheck`
Expected: PASS sin errores.

- [ ] **Step 4: Commit**

```bash
cd C:/Users/chtr1/Downloads/Horux_despacho
git add packages/shared/src/types/impuestos.ts
git commit -m "feat(shared): types para acumulados ISR mensual + desglose del periodo"
```

---

## Task 2: Backend — running totals en `getIsrMensual`

**Files:**
- Modify: `apps/api/src/services/impuestos.service.ts:409-486`

- [ ] **Step 1: Modificar el push del result en el loop interno**

Encontrar el `result.push({ ... })` actual (alrededor de líneas 470-482) y agregar campos placeholder. Cambiar:

```ts
result.push({
  id: 0,
  año,
  mes: m,
  ingresosAcumulados: ing,
  deducciones: ded,
  baseGravable: base,
  isrCausado: 0,
  isrRetenido: 0,
  isrAPagar: 0,
  estado: 'pendiente',
  fechaDeclaracion: null,
});
```

A:

```ts
result.push({
  id: 0,
  año,
  mes: m,
  ingresosAcumulados: ing,
  deducciones: ded,
  baseGravable: base,
  ingresosAcum: 0,    // se llena en el segundo pase abajo
  deduccionesAcum: 0,
  baseGravableAcum: 0,
  isrCausado: 0,
  isrRetenido: 0,
  isrAPagar: 0,
  estado: 'pendiente',
  fechaDeclaracion: null,
});
```

- [ ] **Step 2: Agregar segundo pase de running totals justo antes del `return result`**

Reemplazar `return result;` por:

```ts
  // Running totals: para cada mes, acumular ingresos y deducciones desde enero
  // hasta ese mes inclusive. baseGravableAcum NO se clampa — los déficits se
  // muestran negativos en la UI y solo se clampan al pasar a ISR causado.
  let ingAcum = 0;
  let dedAcum = 0;
  for (const row of result) {
    ingAcum += row.ingresosAcumulados; // (campo mensual, naming heredado)
    dedAcum += row.deducciones;
    row.ingresosAcum = ingAcum;
    row.deduccionesAcum = dedAcum;
    row.baseGravableAcum = ingAcum - dedAcum;
  }

  return result;
}
```

- [ ] **Step 3: Verificar typecheck del API**

Run: `cd C:/Users/chtr1/Downloads/Horux_despacho && pnpm --filter @horux/api typecheck`
Expected: PASS sin errores. Si falla por que `IsrMensual` requiere los campos nuevos, asegurar que Task 1 ya esté aplicada.

- [ ] **Step 4: Commit**

```bash
cd C:/Users/chtr1/Downloads/Horux_despacho
git add apps/api/src/services/impuestos.service.ts
git commit -m "feat(api): getIsrMensual computa running totals (ingresos/deducciones/base gravable acumulada)"
```

---

## Task 3: Backend — nueva función `getResumenIsrDesglosado`

**Files:**
- Modify: `apps/api/src/services/impuestos.service.ts` (agregar al final, después de `getResumenIsr`)

- [ ] **Step 1: Agregar la función exportada**

Buscar el final de `getResumenIsr` (alrededor de línea 887) y después del `}` agregar:

```ts
/**
 * Desglose del cálculo provisional ISR para el mes final del filtro.
 *
 * Tres llamadas a getResumenIsr con rangos distintos:
 *   - delPeriodo: solo el mes final del filtro (1 mes calendario)
 *   - anteriores: enero hasta el mes anterior al final (vacío si mesFinal=1)
 *   - total:      enero hasta el mes final inclusive
 *
 * Si mesFinal === 1, la rama "anteriores" no llama al backend — retorna ceros
 * para evitar un query inútil.
 */
export async function getResumenIsrDesglosado(
  pool: Pool,
  fechaFin: string,
  tenantId: string,
  conciliacion?: boolean,
  contribuyenteId?: string | null,
): Promise<import('@horux/shared').ResumenIsrDesglosado> {
  const fechaFinDate = new Date(fechaFin + 'T00:00:00');
  const anio = fechaFinDate.getFullYear();
  const mesFinal = fechaFinDate.getMonth() + 1; // 1-12

  // Helper para construir rango fin de mes
  const mmFinal = String(mesFinal).padStart(2, '0');
  const ultDiaFinal = new Date(anio, mesFinal, 0).getDate();
  const ultDiaFinalStr = String(ultDiaFinal).padStart(2, '0');

  // delPeriodo: 1er a último día del mes final
  const fiPeriodo = `${anio}-${mmFinal}-01`;
  const ffPeriodo = `${anio}-${mmFinal}-${ultDiaFinalStr}`;

  // anteriores: enero 1 al último día del (mesFinal - 1). Vacío si mesFinal=1.
  let anteriores: import('@horux/shared').ResumenIsr;
  if (mesFinal === 1) {
    anteriores = emptyResumenIsr();
  } else {
    const mesAntes = mesFinal - 1;
    const mmAntes = String(mesAntes).padStart(2, '0');
    const ultDiaAntes = new Date(anio, mesAntes, 0).getDate();
    const ultDiaAntesStr = String(ultDiaAntes).padStart(2, '0');
    const fiAnt = `${anio}-01-01`;
    const ffAnt = `${anio}-${mmAntes}-${ultDiaAntesStr}`;
    anteriores = await getResumenIsr(pool, fiAnt, ffAnt, tenantId, conciliacion, contribuyenteId);
  }

  // total: enero 1 al último día del mes final
  const fiTotal = `${anio}-01-01`;
  const ffTotal = `${anio}-${mmFinal}-${ultDiaFinalStr}`;

  const [delPeriodo, total] = await Promise.all([
    getResumenIsr(pool, fiPeriodo, ffPeriodo, tenantId, conciliacion, contribuyenteId),
    getResumenIsr(pool, fiTotal, ffTotal, tenantId, conciliacion, contribuyenteId),
  ]);

  return { delPeriodo, anteriores, total, mesFinal, anio };
}

function emptyResumenIsr(): import('@horux/shared').ResumenIsr {
  return {
    ingresosAcumulados: 0,
    ingresosPorRegimen: [],
    deducciones: 0,
    deduccionesPorRegimen: [],
    baseGravable: 0,
    baseGravablePorRegimen: [],
    isrCausado: 0,
    isrRetenido: 0,
    isrAPagar: 0,
  };
}
```

- [ ] **Step 2: Sin import top-level necesario**

El archivo ya usa el patrón `import('@horux/shared').XYZ` inline (ver línea 793 con `BaseGravableRegimen`). El código del Step 1 sigue ese patrón para `ResumenIsr` y `ResumenIsrDesglosado`, así que no hace falta agregar un import top-level. Continuar al Step 3.

- [ ] **Step 3: Verificar typecheck del API**

Run: `cd C:/Users/chtr1/Downloads/Horux_despacho && pnpm --filter @horux/api typecheck`
Expected: PASS.

- [ ] **Step 4: Commit**

```bash
cd C:/Users/chtr1/Downloads/Horux_despacho
git add apps/api/src/services/impuestos.service.ts
git commit -m "feat(api): getResumenIsrDesglosado retorna {delPeriodo, anteriores, total} para desglose ISR provisional"
```

---

## Task 4: Backend — controller handler

**Files:**
- Modify: `apps/api/src/controllers/impuestos.controller.ts`

- [ ] **Step 1: Agregar handler después de `getResumenIsr` (línea 88)**

Insertar entre `getResumenIsr` y `getCoeficiente`:

```ts
export async function getResumenIsrDesglosado(req: Request, res: Response, next: NextFunction) {
  try {
    if (!req.tenantPool) {
      return next(new AppError(400, 'Tenant no configurado'));
    }

    // fechaFin define mes_final + año. Default: último día del mes corriente.
    const now = new Date();
    const y = now.getFullYear();
    const m = now.getMonth() + 1;
    const lastDay = new Date(y, m, 0).getDate();
    const fechaFin = (req.query.fechaFin as string) || `${y}-${String(m).padStart(2, '0')}-${String(lastDay).padStart(2, '0')}`;
    const conciliacion = parseConciliacion(req);
    const contribuyenteId = (req.query.contribuyenteId as string) || null;

    const desglose = await impuestosService.getResumenIsrDesglosado(
      req.tenantPool,
      fechaFin,
      effectiveTenantId(req),
      conciliacion,
      contribuyenteId,
    );
    res.json(desglose);
  } catch (error) {
    next(error);
  }
}
```

- [ ] **Step 2: Verificar typecheck**

Run: `cd C:/Users/chtr1/Downloads/Horux_despacho && pnpm --filter @horux/api typecheck`
Expected: PASS.

- [ ] **Step 3: Commit**

```bash
cd C:/Users/chtr1/Downloads/Horux_despacho
git add apps/api/src/controllers/impuestos.controller.ts
git commit -m "feat(api): controller handler para resumen-isr-desglosado"
```

---

## Task 5: Backend — wire up route

**Files:**
- Modify: `apps/api/src/routes/impuestos.routes.ts`

- [ ] **Step 1: Agregar la ruta**

Encontrar la línea 17 (`router.get('/isr/resumen', impuestosController.getResumenIsr);`) y agregar inmediatamente después:

```ts
router.get('/isr/resumen-desglosado', impuestosController.getResumenIsrDesglosado);
```

El bloque queda así:

```ts
router.get('/iva/mensual', impuestosController.getIvaMensual);
router.get('/iva/resumen', impuestosController.getResumenIva);
router.get('/isr/mensual', impuestosController.getIsrMensual);
router.get('/isr/resumen', impuestosController.getResumenIsr);
router.get('/isr/resumen-desglosado', impuestosController.getResumenIsrDesglosado);
router.get('/isr/coeficiente', impuestosController.getCoeficiente);
router.put('/isr/coeficiente', impuestosController.setCoeficiente);
```

- [ ] **Step 2: Verificar typecheck**

Run: `cd C:/Users/chtr1/Downloads/Horux_despacho && pnpm --filter @horux/api typecheck`
Expected: PASS.

- [ ] **Step 3: Smoke test del endpoint con un tenant existente**

Necesitas el dev API corriendo. En otra terminal:

```bash
cd C:/Users/chtr1/Downloads/Horux_despacho
pnpm dev
```

Login con un usuario que tenga datos (p.ej. del tenant Patito) y obtener el JWT. Luego:

```bash
# Reemplazar TOKEN por el JWT real
curl -s "http://localhost:4000/api/impuestos/isr/resumen-desglosado?fechaFin=2026-03-31&conciliacion=false" \
  -H "Authorization: Bearer $TOKEN" | jq '. | {mesFinal, anio, "delPeriodo.ingresos": .delPeriodo.ingresosAcumulados, "anteriores.ingresos": .anteriores.ingresosAcumulados, "total.ingresos": .total.ingresosAcumulados}'
```

Expected:
- `mesFinal: 3, anio: 2026`
- `total.ingresos === delPeriodo.ingresos + anteriores.ingresos` (suma debe cuadrar para ingresos/deducciones/retenciones)
- `total.baseGravable` puede diferir de la suma (BG no es aditiva si hay meses de pérdida).

Probar también `fechaFin=2026-01-31` y verificar `anteriores.ingresosAcumulados === 0`.

- [ ] **Step 4: Commit**

```bash
cd C:/Users/chtr1/Downloads/Horux_despacho
git add apps/api/src/routes/impuestos.routes.ts
git commit -m "feat(api): ruta GET /impuestos/isr/resumen-desglosado"
```

---

## Task 6: Frontend — API client

**Files:**
- Modify: `apps/web/lib/api/impuestos.ts`

- [ ] **Step 1: Actualizar import de types**

En la línea 2 cambiar:

```ts
import type { IvaMensual, IsrMensual, ResumenIva, ResumenIsr } from '@horux/shared';
```

A:

```ts
import type { IvaMensual, IsrMensual, ResumenIva, ResumenIsr, ResumenIsrDesglosado } from '@horux/shared';
```

- [ ] **Step 2: Agregar la función al final del archivo**

Después de `getResumenIsr` (línea 51), agregar:

```ts
export async function getResumenIsrDesglosado(
  fechaFin: string,
  conciliacion?: boolean,
  contribuyenteId?: string | null,
): Promise<ResumenIsrDesglosado> {
  const params = new URLSearchParams();
  params.set('fechaFin', fechaFin);
  if (conciliacion) params.set('conciliacion', 'true');
  if (contribuyenteId) params.set('contribuyenteId', contribuyenteId);
  const response = await apiClient.get<ResumenIsrDesglosado>(`/impuestos/isr/resumen-desglosado?${params}`);
  return response.data;
}
```

- [ ] **Step 3: Verificar typecheck del web**

Run: `cd C:/Users/chtr1/Downloads/Horux_despacho && pnpm --filter @horux/web typecheck`
Expected: PASS. Si la app no tiene script `typecheck`, correr `pnpm --filter @horux/web exec tsc --noEmit`.

- [ ] **Step 4: Commit**

```bash
cd C:/Users/chtr1/Downloads/Horux_despacho
git add apps/web/lib/api/impuestos.ts
git commit -m "feat(web): cliente API getResumenIsrDesglosado"
```

---

## Task 7: Frontend — hook `useResumenIsrDesglosado`

**Files:**
- Modify: `apps/web/lib/hooks/use-impuestos.ts`

- [ ] **Step 1: Agregar hook al final del archivo**

Después de `useResumenIsr` (línea 55), agregar:

```ts
export function useResumenIsrDesglosado(fechaFin: string, conciliacion?: boolean) {
  const tk = useTenantKey();
  const { selectedContribuyenteId } = useContribuyenteStore();
  return useQuery({
    queryKey: ['isr-resumen-desglosado', tk, fechaFin, conciliacion, selectedContribuyenteId],
    queryFn: () => impuestosApi.getResumenIsrDesglosado(fechaFin, conciliacion, selectedContribuyenteId),
    enabled: !!fechaFin,
  });
}
```

- [ ] **Step 2: Verificar typecheck**

Run: `cd C:/Users/chtr1/Downloads/Horux_despacho && pnpm --filter @horux/web exec tsc --noEmit`
Expected: PASS.

- [ ] **Step 3: Commit**

```bash
cd C:/Users/chtr1/Downloads/Horux_despacho
git add apps/web/lib/hooks/use-impuestos.ts
git commit -m "feat(web): hook useResumenIsrDesglosado"
```

---

## Task 8: Frontend — Tabla "Histórico ISR" con columnas acumuladas

**Files:**
- Modify: `apps/web/app/(dashboard)/impuestos/page.tsx:502-568`

- [ ] **Step 1: Reemplazar el bloque del export Excel (líneas 506-524)**

Cambiar:

```tsx
{isrMensual && isrMensual.length > 0 && (
  <Button variant="outline" size="sm" onClick={() => exportToExcel(
    isrMensual.map(r => ({
      Mes: meses[r.mes - 1],
      Ingresos: r.ingresosAcumulados,
      Deducciones: r.deducciones,
      'Base Gravable': r.baseGravable,
    })),
    [
      { header: 'Mes', key: 'Mes', width: 12 },
      { header: 'Ingresos', key: 'Ingresos', width: 18 },
      { header: 'Deducciones', key: 'Deducciones', width: 18 },
      { header: 'Base Gravable', key: 'Base Gravable', width: 18 },
    ],
    `isr-mensual-${año}`,
  )}>
    <Download className="h-4 w-4 mr-1" /> Excel
  </Button>
)}
```

A:

```tsx
{isrMensual && isrMensual.length > 0 && (
  <Button variant="outline" size="sm" onClick={() => exportToExcel(
    isrMensual.map(r => ({
      Mes: meses[r.mes - 1],
      Ingresos: r.ingresosAcumulados,
      'Ingresos Acumulados': r.ingresosAcum,
      Deducciones: r.deducciones,
      'Deducciones Acumuladas': r.deduccionesAcum,
      'Base Gravable Acumulada': r.baseGravableAcum,
    })),
    [
      { header: 'Mes', key: 'Mes', width: 12 },
      { header: 'Ingresos', key: 'Ingresos', width: 18 },
      { header: 'Ingresos Acumulados', key: 'Ingresos Acumulados', width: 22 },
      { header: 'Deducciones', key: 'Deducciones', width: 18 },
      { header: 'Deducciones Acumuladas', key: 'Deducciones Acumuladas', width: 22 },
      { header: 'Base Gravable Acumulada', key: 'Base Gravable Acumulada', width: 22 },
    ],
    `isr-mensual-${año}`,
  )}>
    <Download className="h-4 w-4 mr-1" /> Excel
  </Button>
)}
```

- [ ] **Step 2: Reemplazar el `<thead>` (líneas 532-538)**

Cambiar:

```tsx
<thead>
  <tr className="border-b text-left text-sm text-muted-foreground">
    <th className="pb-3 font-medium">Mes</th>
    <th className="pb-3 font-medium text-right">Ingresos</th>
    <th className="pb-3 font-medium text-right">Deducciones</th>
    <th className="pb-3 font-medium text-right">Base Gravable</th>
  </tr>
</thead>
```

A:

```tsx
<thead>
  <tr className="border-b text-left text-sm text-muted-foreground">
    <th className="pb-3 font-medium">Mes</th>
    <th className="pb-3 font-medium text-right">Ingresos</th>
    <th className="pb-3 font-medium text-right">Ingresos Acum.</th>
    <th className="pb-3 font-medium text-right">Deducciones</th>
    <th className="pb-3 font-medium text-right">Deducciones Acum.</th>
    <th className="pb-3 font-medium text-right">Base Gravable Acum.</th>
  </tr>
</thead>
```

- [ ] **Step 3: Reemplazar el `<tbody>` filas y la fila Total (líneas 540-566)**

Cambiar el bloque entero de `<tbody>...</tbody>` por:

```tsx
<tbody className="text-sm">
  {isrMensual?.map((row) => (
    <tr key={row.mes} className="border-b hover:bg-muted/50">
      <td className="py-3 font-medium">{meses[row.mes - 1]}</td>
      <td className="py-3 text-right">{formatCurrency(row.ingresosAcumulados)}</td>
      <td className="py-3 text-right">{formatCurrency(row.ingresosAcum)}</td>
      <td className="py-3 text-right">{formatCurrency(row.deducciones)}</td>
      <td className="py-3 text-right">{formatCurrency(row.deduccionesAcum)}</td>
      <td className={cn(
        'py-3 text-right font-medium',
        row.baseGravableAcum < 0 ? 'text-destructive' : ''
      )}>
        {formatCurrency(row.baseGravableAcum)}
      </td>
    </tr>
  ))}
  {(!isrMensual || isrMensual.length === 0) && (
    <tr>
      <td colSpan={6} className="py-8 text-center text-muted-foreground">
        No hay registros de ISR para este año
      </td>
    </tr>
  )}
</tbody>
```

Notas:
- Removida la fila Total. La última fila (con datos) ya es el YTD al cierre de ese mes.
- `colSpan={6}` actualizado de 4.

- [ ] **Step 4: Verificar typecheck del web**

Run: `cd C:/Users/chtr1/Downloads/Horux_despacho && pnpm --filter @horux/web exec tsc --noEmit`
Expected: PASS.

- [ ] **Step 5: Smoke manual de la tabla**

Si el dev no está corriendo: `pnpm dev`. Luego:

1. Abrir http://localhost:3000/impuestos en el navegador.
2. Cambiar a la pestaña ISR.
3. Verificar que aparezcan **6 columnas** en la tabla "Histórico ISR".
4. Verificar que las columnas Ingresos Acum., Deducciones Acum. y Base Gravable Acum. muestren running totals correctos (la fila de febrero debe tener acumulado = enero + febrero).
5. Si hay un mes con BG negativa, verificar que aparezca **en rojo** (`text-destructive`).
6. Hacer click en "Excel" y verificar que el archivo descargado tenga las 6 columnas alineadas con el orden de la UI.

- [ ] **Step 6: Commit**

```bash
cd C:/Users/chtr1/Downloads/Horux_despacho
git add apps/web/app/\(dashboard\)/impuestos/page.tsx
git commit -m "feat(web): tabla Histórico ISR con columnas acumuladas; BG mensual deja de mostrarse"
```

---

## Task 9: Frontend — Sección "Cálculo de ISR del Periodo"

**Files:**
- Modify: `apps/web/app/(dashboard)/impuestos/page.tsx:371-432`

- [ ] **Step 1: Importar el nuevo hook**

Buscar la línea 7:

```ts
import { useIvaMensual, useIsrMensual, useResumenIva, useResumenIsr, useCoeficiente } from '@/lib/hooks/use-impuestos';
```

Cambiar a:

```ts
import { useIvaMensual, useIsrMensual, useResumenIva, useResumenIsr, useResumenIsrDesglosado, useCoeficiente } from '@/lib/hooks/use-impuestos';
```

- [ ] **Step 2: Llamar al nuevo hook después de `useResumenIsr`**

Buscar la línea 46 (`const { data: resumenIsr } = useResumenIsr(fechaInicio, fechaFin, conciliacion);`) y agregar inmediatamente después:

```ts
const { data: resumenIsrDesglose } = useResumenIsrDesglosado(fechaFin, conciliacion);
```

- [ ] **Step 3: Reescribir la sección del Card "Cálculo de ISR Acumulado"**

Reemplazar el bloque desde `<CardTitle className="text-base">Calculo de ISR Acumulado</CardTitle>` hasta el cierre `</CardContent>` correspondiente (aproximadamente líneas 381-432) por:

```tsx
<CardTitle className="text-base">Cálculo de ISR del Periodo</CardTitle>
</CardHeader>
<CardContent>
  {(() => {
    // Etiquetas dinámicas a partir del mesFinal del filtro
    const desglose = resumenIsrDesglose;
    if (!desglose) {
      return <div className="text-sm text-muted-foreground">Cargando…</div>;
    }
    const { delPeriodo, anteriores, total, mesFinal, anio } = desglose;
    const labelMesFinal = `${meses[mesFinal - 1]} ${anio}`;
    const labelAnteriores =
      mesFinal === 1
        ? '(sin meses anteriores)'
        : mesFinal === 2
          ? `(${meses[0]})`
          : `(${meses[0]}-${meses[mesFinal - 2]})`;

    // Resolver per-régimen si hay régimen seleccionado, igual patrón que antes.
    const ingPer = regimenSeleccionado
      ? delPeriodo.ingresosPorRegimen?.find(r => r.regimenClave === regimenSeleccionado)?.monto || 0
      : delPeriodo.ingresosAcumulados || 0;
    const ingAnt = regimenSeleccionado
      ? anteriores.ingresosPorRegimen?.find(r => r.regimenClave === regimenSeleccionado)?.monto || 0
      : anteriores.ingresosAcumulados || 0;
    const dedPer = regimenSeleccionado
      ? delPeriodo.deduccionesPorRegimen?.find(r => r.regimenClave === regimenSeleccionado)?.monto || 0
      : delPeriodo.deducciones || 0;
    const dedAnt = regimenSeleccionado
      ? anteriores.deduccionesPorRegimen?.find(r => r.regimenClave === regimenSeleccionado)?.monto || 0
      : anteriores.deducciones || 0;
    const bgTotal = regimenSeleccionado
      ? total.baseGravablePorRegimen?.find(r => r.regimenClave === regimenSeleccionado)?.baseGravable || 0
      : total.baseGravable || 0;
    const causadoTotal = regimenSeleccionado
      ? total.baseGravablePorRegimen?.find(r => r.regimenClave === regimenSeleccionado)?.isrCausado || 0
      : total.isrCausado || 0;
    const retenido = total.isrRetenido || 0;
    const aPagar = Math.max(0, causadoTotal - (regimenSeleccionado ? 0 : retenido));

    return (
      <div className="space-y-1">
        <div className="flex justify-between py-2 border-b">
          <span className="text-muted-foreground">Ingresos del periodo ({labelMesFinal})</span>
          <span className="font-medium">{formatCurrency(ingPer)}</span>
        </div>
        <div className="flex justify-between py-2 border-b">
          <span className="text-muted-foreground">(+) Ingresos acumulados anteriores {labelAnteriores}</span>
          <span className="font-medium">{formatCurrency(ingAnt)}</span>
        </div>
        <div className="flex justify-between py-2 border-b">
          <span className="text-muted-foreground">(−) Deducciones del periodo ({labelMesFinal})</span>
          <span className="font-medium">{formatCurrency(dedPer)}</span>
        </div>
        <div className="flex justify-between py-2 border-b">
          <span className="text-muted-foreground">(−) Deducciones acumuladas anteriores {labelAnteriores}</span>
          <span className="font-medium">{formatCurrency(dedAnt)}</span>
        </div>
        <div className="flex justify-between py-2 border-b">
          <span className="font-medium">(=) Base gravable acumulada</span>
          <span className={cn('font-medium', bgTotal < 0 ? 'text-destructive' : '')}>
            {formatCurrency(bgTotal)}
          </span>
        </div>
        <div className="flex justify-between py-2 border-b">
          <span className="text-muted-foreground">ISR causado (acumulado)</span>
          <span className="font-medium">{formatCurrency(causadoTotal)}</span>
        </div>
        <div className="flex justify-between py-2 border-b">
          <span className="text-muted-foreground">(−) ISR retenido (acumulado)</span>
          <span className="font-medium">{formatCurrency(regimenSeleccionado ? 0 : retenido)}</span>
        </div>
        <div className="flex justify-between py-2 bg-muted/50 px-4 rounded-lg mt-2">
          <span className="font-medium">ISR a pagar</span>
          <span className="font-bold text-lg">{formatCurrency(aPagar)}</span>
        </div>
      </div>
    );
  })()}
</CardContent>
```

Nota: `cn` ya está importado al inicio del archivo (línea 12). Si por alguna razón no lo está, agregar `cn` al import de `@horux/shared-ui`.

- [ ] **Step 4: Verificar typecheck**

Run: `cd C:/Users/chtr1/Downloads/Horux_despacho && pnpm --filter @horux/web exec tsc --noEmit`
Expected: PASS.

- [ ] **Step 5: Smoke manual de la sección**

Con el dev corriendo y un tenant con datos:

1. Abrir `/impuestos` → pestaña ISR.
2. Filtro de periodo en el mes corriente: verificar que aparezcan los 4 renglones de descomposición + base gravable + ISR causado + ISR retenido + ISR a pagar.
3. Cambiar el filtro a **enero del año en curso**: verificar que las dos líneas "anteriores" muestren `$0` con la etiqueta `(sin meses anteriores)`.
4. Cambiar el filtro a **febrero**: la etiqueta de "anteriores" debe decir `(Ene)`.
5. Cambiar el filtro a **marzo**: etiqueta `(Ene-Feb)`.
6. Si hay un tenant con pérdidas YTD: verificar que la línea "Base gravable acumulada" aparezca **en rojo** y que ISR a pagar sea `$0`.
7. Aritmética cruzada: la suma `Ing del periodo + Ing anteriores − Ded del periodo − Ded anteriores` debe coincidir con la línea Base gravable acumulada.
8. Probar también con **régimen seleccionado** en el dropdown — los números deben filtrar correctamente.

- [ ] **Step 6: Commit**

```bash
cd C:/Users/chtr1/Downloads/Horux_despacho
git add apps/web/app/\(dashboard\)/impuestos/page.tsx
git commit -m "feat(web): sección 'Cálculo de ISR del Periodo' con desglose periodo+anteriores=total"
```

---

## Task 10: Verificación final + sync OneDrive + commit release

**Files:**
- Verify: typecheck completo del repo
- Copy: 6 archivos modificados/nuevos a OneDrive
- Commit: bump de versión en OneDrive (mantener pattern V.1.0.x)

- [ ] **Step 1: Typecheck completo**

```bash
cd C:/Users/chtr1/Downloads/Horux_despacho
pnpm --filter @horux/shared typecheck
pnpm --filter @horux/api typecheck
pnpm --filter @horux/web exec tsc --noEmit
```

Expected: los tres en PASS sin errores. Si hay errores, regresar al task correspondiente.

- [ ] **Step 2: Smoke test cross-feature**

Con dev corriendo, en el browser:

1. Cambiar entre IVA y ISR — verificar que IVA siga funcionando igual (no afectado).
2. Toggle conciliación on/off — verificar que la sección de cálculo y la tabla actualicen.
3. Cambiar contribuyente activo — verificar que los queries refetchean con el contribuyente nuevo.
4. Validar que los KPIs de la parte alta (Ingresos, Base Gravable, etc.) sigan mostrando los valores del rango filtrado completo (estos NO deben cambiar — solo afectamos la tabla y la sección de cálculo).

- [ ] **Step 3: Copiar archivos a OneDrive**

```bash
SRC="C:/Users/chtr1/Downloads/Horux_despacho"
DST="C:/Users/chtr1/OneDrive/Documentos/GitHub/Horux_despachos"

cp -p "$SRC/packages/shared/src/types/impuestos.ts"           "$DST/packages/shared/src/types/impuestos.ts"
cp -p "$SRC/apps/api/src/services/impuestos.service.ts"       "$DST/apps/api/src/services/impuestos.service.ts"
cp -p "$SRC/apps/api/src/controllers/impuestos.controller.ts" "$DST/apps/api/src/controllers/impuestos.controller.ts"
cp -p "$SRC/apps/api/src/routes/impuestos.routes.ts"          "$DST/apps/api/src/routes/impuestos.routes.ts"
cp -p "$SRC/apps/web/lib/api/impuestos.ts"                    "$DST/apps/web/lib/api/impuestos.ts"
cp -p "$SRC/apps/web/lib/hooks/use-impuestos.ts"              "$DST/apps/web/lib/hooks/use-impuestos.ts"
cp -p "$SRC/apps/web/app/(dashboard)/impuestos/page.tsx"      "$DST/apps/web/app/(dashboard)/impuestos/page.tsx"
cp -p "$SRC/docs/superpowers/specs/2026-04-27-isr-base-gravable-acumulada-design.md" "$DST/docs/superpowers/specs/2026-04-27-isr-base-gravable-acumulada-design.md"
cp -p "$SRC/docs/superpowers/plans/2026-04-27-isr-base-gravable-acumulada.md" "$DST/docs/superpowers/plans/2026-04-27-isr-base-gravable-acumulada.md"
```

- [ ] **Step 4: Verificar diff OneDrive vs Downloads**

```bash
diff -rq \
  --exclude=node_modules --exclude=.git --exclude=.turbo --exclude=.next \
  --exclude=dist --exclude=tsconfig.tsbuildinfo --exclude=email-previews \
  --exclude=pnpm-lock.yaml --exclude=.env --exclude=.env.local \
  "C:/Users/chtr1/Downloads/Horux_despacho" \
  "C:/Users/chtr1/OneDrive/Documentos/GitHub/Horux_despachos"
```

Expected: única diferencia esperada es `Only in C:/Users/chtr1/Downloads/Horux_despacho/apps/api: data` (XMLs runtime). Si aparece otra diferencia inesperada, investigar.

- [ ] **Step 5: Commit en OneDrive**

```bash
cd "C:/Users/chtr1/OneDrive/Documentos/GitHub/Horux_despachos"
git add \
  packages/shared/src/types/impuestos.ts \
  apps/api/src/services/impuestos.service.ts \
  apps/api/src/controllers/impuestos.controller.ts \
  apps/api/src/routes/impuestos.routes.ts \
  apps/web/lib/api/impuestos.ts \
  apps/web/lib/hooks/use-impuestos.ts \
  "apps/web/app/(dashboard)/impuestos/page.tsx" \
  docs/superpowers/specs/2026-04-27-isr-base-gravable-acumulada-design.md \
  docs/superpowers/plans/2026-04-27-isr-base-gravable-acumulada.md

git commit -m "V.1.0.6"
git status --short
git log -2 --oneline
```

Expected:
- Commit creado con hash nuevo, mensaje `V.1.0.6` (mantiene el pattern de OneDrive).
- `git status` clean.
- `git log -2` muestra V.1.0.6 sobre V.1.0.5.

- [ ] **Step 6: NO push automático**

Per workflow del owner: el push a `origin/main` lo dispara él manualmente cuando quiera. Confirmar que NO se ejecutó `git push`.
