Update: nueva version Horux Despachos
This commit is contained in:
894
docs/superpowers/plans/2026-04-27-isr-base-gravable-acumulada.md
Normal file
894
docs/superpowers/plans/2026-04-27-isr-base-gravable-acumulada.md
Normal file
@@ -0,0 +1,894 @@
|
||||
# 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`.
|
||||
Reference in New Issue
Block a user