32 KiB
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_mensualescache: 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
ResumenIsrDesglosadoal 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 degetResumenIsr) -
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: 2026total.ingresos === delPeriodo.ingresos + anteriores.ingresos(suma debe cuadrar para ingresos/deducciones/retenciones)total.baseGravablepuede 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:
- Abrir http://localhost:3000/impuestos en el navegador.
- Cambiar a la pestaña ISR.
- Verificar que aparezcan 6 columnas en la tabla "Histórico ISR".
- 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).
- Si hay un mes con BG negativa, verificar que aparezca en rojo (
text-destructive). - 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:
- Abrir
/impuestos→ pestaña ISR. - 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.
- Cambiar el filtro a enero del año en curso: verificar que las dos líneas "anteriores" muestren
$0con la etiqueta(sin meses anteriores). - Cambiar el filtro a febrero: la etiqueta de "anteriores" debe decir
(Ene). - Cambiar el filtro a marzo: etiqueta
(Ene-Feb). - 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. - Aritmética cruzada: la suma
Ing del periodo + Ing anteriores − Ded del periodo − Ded anterioresdebe coincidir con la línea Base gravable acumulada. - 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:
- Cambiar entre IVA y ISR — verificar que IVA siga funcionando igual (no afectado).
- Toggle conciliación on/off — verificar que la sección de cálculo y la tabla actualicen.
- Cambiar contribuyente activo — verificar que los queries refetchean con el contribuyente nuevo.
- 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 statusclean. -
git log -2muestra 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.