Files
HoruxDespachos/docs/superpowers/plans/2026-04-27-isr-base-gravable-acumulada.md
2026-04-27 22:09:36 -06:00

32 KiB
Raw Blame History

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):

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
/**
 * 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
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:

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:

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:

  // 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
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:

/**
 * 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
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:

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
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:

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

El bloque queda así:

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:

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:

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

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

A:

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:

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
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:

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
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:

{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:

{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:

<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:

<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:

<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
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:

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

Cambiar a:

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:

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:

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

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