Files
HoruxDespachosNuevo/docs/superpowers/plans/2026-04-27-filtros-activos-ncs-impuestos-fase1.md

947 lines
34 KiB
Markdown

# Filtros "Considerar activos" y "Considerar NCs" — Fase 1 — 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:** Agregar 2 toggles en `/impuestos` ("Considerar activos" y "Considerar NCs") que cuando están OFF (default) excluyen del cálculo de IVA/ISR las facturas tipo I con uso I01-I08 y las facturas tipo E con cfdi_tipo_relacion=01 respectivamente.
**Architecture:** Frontend agrega 2 booleanos al state de la página de impuestos y los propaga como query params hasta el backend. Backend aplica un fragmento WHERE adicional (helper en módulo neutral `_shared/cfdi-filters.ts`) a todas las queries que escanean `cfdis` dentro del path de impuestos. Funciones compartidas con dashboard (`calcular*PorRegimen`) reciben los flags como params opcionales con default `true` (= include todo) para preservar el comportamiento del dashboard. Cache `metricas_mensuales` queda intacto pero su gate se extiende para fall-through cuando los toggles están OFF; el cache se actualizará en Fase 2 con un schema base+deltas.
**Tech Stack:** Express + TypeScript en API, Next.js 14 + React Query en web, types compartidos en `@horux/shared`. Verificación vía `pnpm typecheck` (no unit tests para esta área per el patrón del repo).
**Spec:** `docs/superpowers/specs/2026-04-27-filtros-activos-ncs-impuestos-fase1-design.md`
---
## File Structure
### Files to create
```
apps/api/src/services/_shared/cfdi-filters.ts
└── Helper buildExtraFilters + buildExtraFiltersAlias (módulo neutral)
```
### Files to modify
```
apps/api/src/services/dashboard.service.ts
└── calcularIngresosPorRegimen + calcularEgresosPorRegimen: agregar 2 params booleanos default true, aplicar buildExtraFilters al WHERE de TODAS las queries internas
apps/api/src/services/impuestos.service.ts
└── getResumenIva + getIvaMensual: nuevos params + aplicar filtro al WHERE
└── getResumenIsr + getIsrMensual + getResumenIsrDesglosado: nuevos params + propagar a calcular*PorRegimen
└── Cache gate de getResumenIva: extender condición para bypass cuando flags ≠ default backend
└── Subqueries con alias `e` (rama I PPD/07): aplicar buildExtraFiltersAlias
apps/api/src/controllers/impuestos.controller.ts
└── Helper parseFlag + 5 handlers parsean los 2 query params nuevos
apps/web/lib/api/impuestos.ts
└── 5 funciones HTTP extendidas con 2 params nuevos
apps/web/lib/hooks/use-impuestos.ts
└── 5 hooks extendidos con 2 params nuevos (incluir en queryKey)
apps/web/app/(dashboard)/impuestos/page.tsx
└── 2 useState nuevos + 2 toggle buttons + propagación a hooks
```
---
## Task 1: Crear módulo helper compartido
**Files:**
- Create: `apps/api/src/services/_shared/cfdi-filters.ts`
- [ ] **Step 1: Crear directorio si no existe**
```bash
mkdir -p "C:/Users/chtr1/Downloads/Horux_despacho/apps/api/src/services/_shared"
```
- [ ] **Step 2: Escribir el módulo**
Crear `apps/api/src/services/_shared/cfdi-filters.ts` con el contenido completo:
```ts
/**
* Helpers para construir fragmentos AND adicionales en WHERE clauses según
* los toggles "Considerar activos" y "Considerar NCs" de la UI de impuestos.
*
* - considerarActivos === false → excluir facturas tipo I con uso de CFDI I01-I08.
* - considerarNCs === false → excluir facturas tipo E con cfdi_tipo_relacion = '01'.
*
* Cuando ambos son true (default backend = "include todo"), retorna string
* vacío. Esto preserva el comportamiento histórico para callers que no pasan
* los flags (ej. dashboard, reportes).
*
* Las versiones `Alias` se usan en subqueries con alias de tabla
* (ej. `cfdis e` en SUM_E_REFERENCING_*). Para activos el filtro es no-op
* en esos subqueries (porque escanean type E), pero el filtro de NCs sí
* aplica.
*/
const ACTIVOS_USOS = "('I01','I02','I03','I04','I05','I06','I07','I08')";
export function buildExtraFilters(
considerarActivos: boolean,
considerarNCs: boolean,
): string {
const parts: string[] = [];
if (!considerarActivos) {
parts.push(`AND NOT (tipo_comprobante = 'I' AND uso_cfdi IN ${ACTIVOS_USOS})`);
}
if (!considerarNCs) {
parts.push(`AND NOT (tipo_comprobante = 'E' AND COALESCE(cfdi_tipo_relacion, '') = '01')`);
}
return parts.length > 0 ? ' ' + parts.join(' ') : '';
}
export function buildExtraFiltersAlias(
alias: string,
considerarActivos: boolean,
considerarNCs: boolean,
): string {
const parts: string[] = [];
if (!considerarActivos) {
parts.push(`AND NOT (${alias}.tipo_comprobante = 'I' AND ${alias}.uso_cfdi IN ${ACTIVOS_USOS})`);
}
if (!considerarNCs) {
parts.push(`AND NOT (${alias}.tipo_comprobante = 'E' AND COALESCE(${alias}.cfdi_tipo_relacion, '') = '01')`);
}
return parts.length > 0 ? ' ' + parts.join(' ') : '';
}
```
- [ ] **Step 3: Verificar typecheck del API**
Run: `cd C:/Users/chtr1/Downloads/Horux_despacho && pnpm --filter @horux/api typecheck`
Expected: PASS sin errores.
- [ ] **Step 4: Commit**
```bash
cd C:/Users/chtr1/Downloads/Horux_despacho
git add apps/api/src/services/_shared/cfdi-filters.ts
git commit -m "feat(api): helper buildExtraFilters para toggles activos/NCs"
```
---
## Task 2: Extender `calcularIngresosPorRegimen` y `calcularEgresosPorRegimen` en dashboard.service.ts
**Files:**
- Modify: `apps/api/src/services/dashboard.service.ts`
**Heads up:** Dashboard también consume estas funciones. Default `true` en los nuevos params preserva su comportamiento.
- [ ] **Step 1: Agregar import del helper al inicio del archivo**
Encontrar la sección de imports al inicio de `dashboard.service.ts` y agregar:
```ts
import { buildExtraFilters } from './_shared/cfdi-filters.js';
```
(Las imports en este proyecto usan extensión `.js` aunque el archivo sea `.ts` — patrón ESM con tsx. Revisa imports existentes para confirmar el estilo.)
- [ ] **Step 2: Extender la signature de `calcularIngresosPorRegimen`**
Buscar la función exportada `calcularIngresosPorRegimen`. Agregar 2 parámetros opcionales con default `true` al final de la lista, antes del cierre de `)`:
Cambiar la signature para incluir:
```ts
export async function calcularIngresosPorRegimen(
pool: Pool,
tenantId: string,
fechaInicio: string,
fechaFin: string,
// ...parámetros existentes preservados...
conciliacion?: boolean,
contribuyenteId?: string | null,
considerarActivos: boolean = true, // nuevo
considerarNCs: boolean = true, // nuevo
): Promise<...>
```
(Mantener los nombres y orden de los parámetros existentes. Solo agregar los 2 nuevos al final.)
- [ ] **Step 3: Aplicar el filtro a TODAS las queries internas de calcularIngresosPorRegimen**
Dentro del cuerpo de la función, antes de las queries SQL, computar el fragmento:
```ts
const extra = buildExtraFilters(considerarActivos, considerarNCs);
```
Luego, en cada query SQL que escanee `cfdis`, agregar `${extra}` al final del WHERE clause. Buscar todos los `FROM cfdis` dentro del cuerpo de la función — deben ser ~3-5 queries — y a cada uno agregarle el fragmento.
Ejemplo de transformación:
```ts
// Antes:
const { rows } = await pool.query(`
SELECT ...
FROM cfdis
WHERE ${VIGENTE} AND ${FR}
AND ${ctx.esEmisor}
GROUP BY ...
`, [fechaInicio, fechaFin]);
// Después:
const { rows } = await pool.query(`
SELECT ...
FROM cfdis
WHERE ${VIGENTE} AND ${FR}${extra}
AND ${ctx.esEmisor}
GROUP BY ...
`, [fechaInicio, fechaFin]);
```
`extra` retorna con leading space cuando agrega contenido. Si ambos flags son `true` retorna string vacío y la query queda idéntica.
- [ ] **Step 4: Repetir para `calcularEgresosPorRegimen`**
Misma extensión de signature (2 params al final con default `true`), mismo helper `extra = buildExtraFilters(...)`, misma aplicación a todos los `FROM cfdis` del cuerpo.
- [ ] **Step 5: Verificar typecheck del API**
Run: `cd C:/Users/chtr1/Downloads/Horux_despacho && pnpm --filter @horux/api typecheck`
Expected: PASS sin errores. Cualquier callsite existente de estas funciones que no pase los nuevos params usa los defaults `true`, comportamiento idéntico a antes.
- [ ] **Step 6: Commit**
```bash
cd C:/Users/chtr1/Downloads/Horux_despacho
git add apps/api/src/services/dashboard.service.ts
git commit -m "feat(api): calcular*PorRegimen aceptan flags considerarActivos/considerarNCs"
```
---
## Task 3: Extender `getResumenIva` y `getIvaMensual` en impuestos.service.ts
**Files:**
- Modify: `apps/api/src/services/impuestos.service.ts`
- [ ] **Step 1: Agregar import del helper al inicio del archivo**
Buscar la sección de imports del archivo. Agregar:
```ts
import { buildExtraFilters } from './_shared/cfdi-filters.js';
```
- [ ] **Step 2: Extender signature de `getResumenIva`**
Encontrar `export async function getResumenIva(...)`. Agregar 2 params al final con default `true`:
```ts
export async function getResumenIva(
pool: Pool,
fechaInicio: string,
fechaFin: string,
tenantId: string,
conciliacion?: boolean,
contribuyenteId?: string | null,
considerarActivos: boolean = true,
considerarNCs: boolean = true,
): Promise<ResumenIva>
```
- [ ] **Step 3: Computar `extra` y aplicar a todas las queries internas**
Dentro del body, después de `const FR = getFR(conciliacion);` agregar:
```ts
const extra = buildExtraFilters(considerarActivos, considerarNCs);
```
Y aplicar `${extra}` al final de cada WHERE en queries con `FROM cfdis` (las que NO usan alias `e` — esas son Task 5). Aplica el mismo patrón del Task 2 Step 3.
- [ ] **Step 4: Extender el cache gate de getResumenIva**
Buscar la condición que protege el path de cache (alrededor de línea 322 según la versión actual del archivo, puede haber cambiado por WIP). El patrón es:
```ts
if (
!conciliacion &&
contribuyenteId &&
...condiciones existentes...
) {
const cached = await readResumenIvaFromCache(...);
if (cached) return cached;
}
```
Extender:
```ts
if (
!conciliacion &&
considerarActivos && // nuevo: cache solo aplica con backend default (todo incluido)
considerarNCs && // nuevo
contribuyenteId &&
...condiciones existentes...
) {
const cached = await readResumenIvaFromCache(...);
if (cached) return cached;
}
```
Cuando UI tiene los toggles OFF (default), `considerarActivos===false || considerarNCs===false` → cache bypass → live query. Aceptado para Fase 1.
- [ ] **Step 5: Extender signature de `getIvaMensual`**
Misma extensión: agregar 2 params al final con default `true`. Agregar `const extra = buildExtraFilters(...)` y aplicar a todas las queries con `FROM cfdis` dentro del loop mensual.
- [ ] **Step 6: Verificar typecheck del API**
Run: `cd C:/Users/chtr1/Downloads/Horux_despacho && pnpm --filter @horux/api typecheck`
Expected: PASS.
- [ ] **Step 7: Commit**
```bash
cd C:/Users/chtr1/Downloads/Horux_despacho
git add apps/api/src/services/impuestos.service.ts
git commit -m "feat(api): getResumenIva y getIvaMensual aceptan flags considerarActivos/considerarNCs + cache gate"
```
---
## Task 4: Extender `getResumenIsr`, `getIsrMensual`, `getResumenIsrDesglosado`
**Files:**
- Modify: `apps/api/src/services/impuestos.service.ts`
- [ ] **Step 1: Extender signature de `getResumenIsr`**
Agregar 2 params al final con default `true`:
```ts
export async function getResumenIsr(
pool: Pool,
fechaInicio: string,
fechaFin: string,
tenantId: string,
conciliacion?: boolean,
contribuyenteId?: string | null,
considerarActivos: boolean = true,
considerarNCs: boolean = true,
): Promise<ResumenIsr>
```
- [ ] **Step 2: Propagar a llamadas a `calcular*PorRegimen` y a queries internas**
Dentro de `getResumenIsr`:
- Agregar `const extra = buildExtraFilters(considerarActivos, considerarNCs);` al inicio del cuerpo (después del `getFR`).
- Aplicar `${extra}` a TODOS los `FROM cfdis` internos de la función (sin alias).
- En las llamadas existentes `calcularIngresosPorRegimen(pool, tenantId, fechaInicio, fechaFin, undefined, undefined, conciliacion, contribuyenteId)` agregar al final los 2 nuevos args:
```ts
const ingresosData = await calcularIngresosPorRegimen(
pool, tenantId, fechaInicio, fechaFin,
undefined, undefined, conciliacion, contribuyenteId,
considerarActivos, considerarNCs, // nuevos
);
```
Idem para `calcularEgresosPorRegimen`.
- [ ] **Step 3: Extender signature de `getIsrMensual`**
Agregar 2 params al final con default `true`:
```ts
export async function getIsrMensual(
pool: Pool,
año: number,
tenantId: string,
conciliacion?: boolean,
contribuyenteId?: string | null,
regimenClave?: string | null,
considerarActivos: boolean = true,
considerarNCs: boolean = true,
): Promise<IsrMensual[]>
```
- [ ] **Step 4: Propagar dentro de `getIsrMensual`**
Dentro del loop mensual de `getIsrMensual`, las llamadas existentes a `calcularIngresosPorRegimen` y `calcularEgresosPorRegimen` deben recibir los 2 nuevos args al final. Patrón:
```ts
const [ingresosData, egresosData] = await Promise.all([
calcularIngresosPorRegimen(
pool, tenantId, fi, ff,
undefined, undefined, conciliacion, contribuyenteId,
considerarActivos, considerarNCs, // nuevos
),
calcularEgresosPorRegimen(
pool, tenantId, fi, ff,
undefined, undefined, conciliacion, contribuyenteId,
considerarActivos, considerarNCs, // nuevos
),
]);
```
- [ ] **Step 5: Extender signature de `getResumenIsrDesglosado`**
Agregar 2 params al final con default `true`:
```ts
export async function getResumenIsrDesglosado(
pool: Pool,
fechaFin: string,
tenantId: string,
conciliacion?: boolean,
contribuyenteId?: string | null,
considerarActivos: boolean = true,
considerarNCs: boolean = true,
): Promise<import('@horux/shared').ResumenIsrDesglosado>
```
- [ ] **Step 6: Propagar dentro de `getResumenIsrDesglosado`**
Las 3 llamadas a `getResumenIsr` (una secuencial para `anteriores` cuando mesFinal !== 1, dos en `Promise.all` para `delPeriodo` y `total`) deben pasar los 2 nuevos args al final:
```ts
anteriores = await getResumenIsr(
pool, fiAnt, ffAnt, tenantId, conciliacion, contribuyenteId,
considerarActivos, considerarNCs, // nuevos
);
const [delPeriodo, total] = await Promise.all([
getResumenIsr(pool, fiPeriodo, ffPeriodo, tenantId, conciliacion, contribuyenteId, considerarActivos, considerarNCs),
getResumenIsr(pool, fiTotal, ffTotal, tenantId, conciliacion, contribuyenteId, considerarActivos, considerarNCs),
]);
```
- [ ] **Step 7: Verificar typecheck del API**
Run: `cd C:/Users/chtr1/Downloads/Horux_despacho && pnpm --filter @horux/api typecheck`
Expected: PASS.
- [ ] **Step 8: Commit**
```bash
cd C:/Users/chtr1/Downloads/Horux_despacho
git add apps/api/src/services/impuestos.service.ts
git commit -m "feat(api): getResumenIsr/getIsrMensual/getResumenIsrDesglosado aceptan flags considerarActivos/considerarNCs"
```
---
## Task 5: Aplicar filtros a subqueries con alias `e` (rama I PPD/07)
**Files:**
- Modify: `apps/api/src/services/impuestos.service.ts`
**Context:** En la rama I PPD/07 hay subqueries que iteran sobre `cfdis e` (alias) para detectar E que referencian I PPD/07. Estos subqueries pueden ser constants templates (`SUM_E_REFERENCING_TRAS`, `SUM_E_REFERENCING_RET`, `HAS_E_REFERENCING_MISMO_MES`) o expresiones inline. Necesitan el filtro `buildExtraFiltersAlias('e', ...)`.
- [ ] **Step 1: Importar `buildExtraFiltersAlias`**
Verificar que el import al inicio del archivo incluya ambas:
```ts
import { buildExtraFilters, buildExtraFiltersAlias } from './_shared/cfdi-filters.js';
```
- [ ] **Step 2: Identificar y modificar las constantes/templates de subqueries con alias `e`**
Buscar `cfdis e` en `impuestos.service.ts`. Deberían aparecer en constantes como `SUM_E_REFERENCING_TRAS = (esLadoE: string) => \`...\`` y similares.
**Decisión arquitectónica**: estas constantes son templates funcionales. La forma más limpia es **convertirlas a funciones que reciben los flags** y los aplican.
Buscar las constantes existentes (típicamente templates string functions) y convertirlas. Ejemplo (la firma exacta existente puede variar; la idea es agregar los 2 params al final):
Si encuentras (formato actual aproximado):
```ts
const SUM_E_REFERENCING_TRAS = (esLadoE: string) => `COALESCE((
SELECT SUM(${IVA_TRAS_EXPR_ALIAS('e')})
FROM cfdis e
WHERE e.status NOT IN ('Cancelado', '0')
AND ${esLadoE}
AND ...resto del where...
), 0)`;
```
Cambiar a:
```ts
const SUM_E_REFERENCING_TRAS = (esLadoE: string, considerarActivos: boolean, considerarNCs: boolean) => `COALESCE((
SELECT SUM(${IVA_TRAS_EXPR_ALIAS('e')})
FROM cfdis e
WHERE e.status NOT IN ('Cancelado', '0')
AND ${esLadoE}
AND ...resto del where...${buildExtraFiltersAlias('e', considerarActivos, considerarNCs)}
), 0)`;
```
Aplicar el mismo patrón a las demás subqueries con alias `e`:
- `SUM_E_REFERENCING_TRAS`
- `SUM_E_REFERENCING_RET`
- `HAS_E_REFERENCING_MISMO_MES`
- Cualquier otra que use `cfdis e`
- [ ] **Step 3: Actualizar callsites de las subqueries**
Buscar dónde se usan estas funciones (ej. dentro de `getResumenIva`, `getResumenIsr`, sus helpers `bucketCausadoNeg`, `bucketAcreditableNeg`, etc.) y agregar los nuevos params:
```ts
// Antes:
SUM_E_REFERENCING_TRAS(esLado)
// Después:
SUM_E_REFERENCING_TRAS(esLado, considerarActivos, considerarNCs)
```
Los callsites están dentro de funciones que ya recibieron los flags en Tasks 3 y 4. Solo es propagación local.
- [ ] **Step 4: Verificar typecheck del API**
Run: `cd C:/Users/chtr1/Downloads/Horux_despacho && pnpm --filter @horux/api typecheck`
Expected: PASS.
- [ ] **Step 5: Commit**
```bash
cd C:/Users/chtr1/Downloads/Horux_despacho
git add apps/api/src/services/impuestos.service.ts
git commit -m "feat(api): subqueries con alias 'e' (I PPD/07) respetan flags considerarActivos/considerarNCs"
```
---
## Task 6: Controllers — `parseFlag` helper + propagación
**Files:**
- Modify: `apps/api/src/controllers/impuestos.controller.ts`
- [ ] **Step 1: Agregar helper `parseFlag` cerca del top del archivo**
Después del helper `parseConciliacion(req)` existente, agregar:
```ts
function parseFlag(req: Request, key: string, defaultValue = true): boolean {
const v = req.query[key];
if (v === undefined || v === null) return defaultValue;
return v === 'true' || v === '1';
}
```
- [ ] **Step 2: Extender los 5 handlers**
Para cada uno de los 5 handlers (`getIvaMensual`, `getIsrMensual`, `getResumenIva`, `getResumenIsr`, `getResumenIsrDesglosado`):
1. Agregar las 2 lecturas de query params después de las existentes:
```ts
const considerarActivos = parseFlag(req, 'considerarActivos', true);
const considerarNCs = parseFlag(req, 'considerarNCs', true);
```
2. Pasar al service como los 2 últimos args.
Ejemplo para `getResumenIsrDesglosado`:
```ts
export async function getResumenIsrDesglosado(req: Request, res: Response, next: NextFunction) {
try {
if (!req.tenantPool) {
return next(new AppError(400, 'Tenant no configurado'));
}
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 considerarActivos = parseFlag(req, 'considerarActivos', true); // nuevo
const considerarNCs = parseFlag(req, 'considerarNCs', true); // nuevo
const desglose = await impuestosService.getResumenIsrDesglosado(
req.tenantPool,
fechaFin,
effectiveTenantId(req),
conciliacion,
contribuyenteId,
considerarActivos, // nuevo
considerarNCs, // nuevo
);
res.json(desglose);
} catch (error) {
next(error);
}
}
```
Aplicar el mismo patrón a los otros 4 handlers (`getIvaMensual`, `getIsrMensual`, `getResumenIva`, `getResumenIsr`).
- [ ] **Step 3: Verificar typecheck**
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/controllers/impuestos.controller.ts
git commit -m "feat(api): controllers parsean flags considerarActivos/considerarNCs y los propagan al service"
```
---
## Task 7: Frontend API client
**Files:**
- Modify: `apps/web/lib/api/impuestos.ts`
- [ ] **Step 1: Extender las 5 funciones HTTP**
Para cada función, agregar 2 params booleanos opcionales y serializarlos en `URLSearchParams`. Patrón:
```ts
export async function getResumenIsrDesglosado(
fechaFin: string,
conciliacion?: boolean,
considerarActivos?: boolean,
considerarNCs?: boolean,
contribuyenteId?: string | null,
): Promise<ResumenIsrDesglosado> {
const params = new URLSearchParams();
params.set('fechaFin', fechaFin);
if (conciliacion) params.set('conciliacion', 'true');
if (considerarActivos) params.set('considerarActivos', 'true');
if (considerarNCs) params.set('considerarNCs', 'true');
if (contribuyenteId) params.set('contribuyenteId', contribuyenteId);
const response = await apiClient.get<ResumenIsrDesglosado>(`/impuestos/isr/resumen-desglosado?${params}`);
return response.data;
}
```
Aplicar el mismo patrón a:
- `getIsrMensual(año, conciliacion, contribuyenteId, regimenClave, considerarActivos, considerarNCs)` — orden: insertar los 2 nuevos AL FINAL para no romper callers existentes que pasan posicionalmente.
- `getIvaMensual(año, conciliacion, contribuyenteId, considerarActivos, considerarNCs)`
- `getResumenIva(fechaInicio, fechaFin, conciliacion, contribuyenteId, considerarActivos, considerarNCs)`
- `getResumenIsr(fechaInicio, fechaFin, conciliacion, contribuyenteId, considerarActivos, considerarNCs)`
- `getResumenIsrDesglosado(fechaFin, conciliacion, considerarActivos, considerarNCs, contribuyenteId)` — la signature actual ya tiene `contribuyenteId` al final; mantenerlo allí.
**Importante**: solo set en URLSearchParams cuando el valor es `true`. Si el frontend pasa `undefined` o `false`, NO se manda el param (el backend default `true` aplica). Esto evita ambigüedad con la convención `'false'` string.
Espera — esta regla es la INVERSA de lo que queremos. Nuestro UI default es `false` (toggle OFF) y queremos QUE EL BACKEND EXCLUYA. Si el frontend NO manda el param cuando el toggle está OFF, el backend default `true` (include) aplica → no se excluye → COMPORTAMIENTO INCORRECTO.
Corrección: serializar el booleano explícitamente (siempre).
```ts
if (considerarActivos !== undefined) params.set('considerarActivos', String(considerarActivos));
if (considerarNCs !== undefined) params.set('considerarNCs', String(considerarNCs));
```
Y en el controller (ya implementado en Task 6) `parseFlag` retorna `false` cuando `req.query.considerarActivos === 'false'`.
Verificar que el `parseFlag` del Task 6 maneja el string `'false'`:
```ts
function parseFlag(req: Request, key: string, defaultValue = true): boolean {
const v = req.query[key];
if (v === undefined || v === null) return defaultValue;
return v === 'true' || v === '1'; // cualquier otra cosa (ej. 'false', '0') → false
}
```
`v === 'true' || v === '1'` retorna `false` cuando `v === 'false'`. Correcto.
Aplicar a los 5 funciones:
```ts
if (considerarActivos !== undefined) params.set('considerarActivos', String(considerarActivos));
if (considerarNCs !== undefined) params.set('considerarNCs', String(considerarNCs));
```
- [ ] **Step 2: Verificar typecheck del web**
Run: `cd C:/Users/chtr1/Downloads/Horux_despacho && pnpm --filter @horux/web exec tsc --noEmit 2>&1 | grep "lib/api/impuestos"`
Expected: NO output (clean — los errores pre-existentes en otros archivos del web no nos importan).
- [ ] **Step 3: Commit**
```bash
cd C:/Users/chtr1/Downloads/Horux_despacho
git add apps/web/lib/api/impuestos.ts
git commit -m "feat(web): API client funciones aceptan considerarActivos/considerarNCs"
```
---
## Task 8: Frontend hooks
**Files:**
- Modify: `apps/web/lib/hooks/use-impuestos.ts`
- [ ] **Step 1: Extender los 5 hooks con 2 params nuevos**
Para cada hook, agregar 2 params booleanos opcionales al final, incluirlos en `queryKey`, y pasarlos al API call. Patrón:
```ts
export function useResumenIsrDesglosado(
fechaFin: string,
conciliacion?: boolean,
considerarActivos?: boolean,
considerarNCs?: boolean,
) {
const tk = useTenantKey();
const { selectedContribuyenteId } = useContribuyenteStore();
return useQuery({
queryKey: ['isr-resumen-desglosado', tk, fechaFin, conciliacion, considerarActivos, considerarNCs, selectedContribuyenteId],
queryFn: () => impuestosApi.getResumenIsrDesglosado(fechaFin, conciliacion, considerarActivos, considerarNCs, selectedContribuyenteId),
enabled: !!fechaFin,
});
}
```
Aplicar a los 5 hooks: `useResumenIsrDesglosado`, `useResumenIsr`, `useResumenIva`, `useIsrMensual`, `useIvaMensual`.
Para `useIsrMensual` que ya tiene `regimenClave` opcional, mantener ese param y agregar los 2 nuevos al final:
```ts
export function useIsrMensual(
año?: number,
conciliacion?: boolean,
regimenClave?: string | null,
considerarActivos?: boolean,
considerarNCs?: boolean,
)
```
(Verificar el orden actual de params del hook — los nuevos van AL FINAL.)
- [ ] **Step 2: Verificar typecheck del web**
Run: `cd C:/Users/chtr1/Downloads/Horux_despacho && pnpm --filter @horux/web exec tsc --noEmit 2>&1 | grep "lib/hooks/use-impuestos"`
Expected: NO output.
- [ ] **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): hooks de impuestos aceptan considerarActivos/considerarNCs en queryKey"
```
---
## Task 9: Frontend UI — toggles + propagación
**Files:**
- Modify: `apps/web/app/(dashboard)/impuestos/page.tsx`
- [ ] **Step 1: Agregar 2 useState al inicio del componente**
Buscar la sección de useState existente (cerca de líneas 30-40, donde está `useState(false)` para `conciliacion`). Agregar:
```ts
const [considerarActivos, setConsiderarActivos] = useState(false);
const [considerarNCs, setConsiderarNCs] = useState(false);
```
- [ ] **Step 2: Pasar los 2 nuevos states a TODOS los hooks de impuestos**
Buscar cada llamada a hook y agregar los 2 args al final. Patrón:
```ts
// Antes:
const { data: resumenIsr } = useResumenIsr(fechaInicio, fechaFin, conciliacion);
const { data: resumenIsrDesglose } = useResumenIsrDesglosado(fechaFin, conciliacion);
// Después:
const { data: resumenIsr } = useResumenIsr(fechaInicio, fechaFin, conciliacion, considerarActivos, considerarNCs);
const { data: resumenIsrDesglose } = useResumenIsrDesglosado(fechaFin, conciliacion, considerarActivos, considerarNCs);
```
Aplicar a:
- `useIvaMensual(año, conciliacion, considerarActivos, considerarNCs)`
- `useIsrMensual(año, conciliacion, regimenSeleccionado, considerarActivos, considerarNCs)`
- `useResumenIva(fechaInicio, fechaFin, conciliacion, considerarActivos, considerarNCs)`
- `useResumenIsr(fechaInicio, fechaFin, conciliacion, considerarActivos, considerarNCs)`
- `useResumenIsrDesglosado(fechaFin, conciliacion, considerarActivos, considerarNCs)`
- [ ] **Step 3: Agregar 2 toggle buttons al row de filtros**
Buscar el bloque del toggle de Conciliación (alrededor de líneas 92-103). Después del button de Conciliación y antes del cierre del `<div className="flex items-center gap-3">`, agregar:
```tsx
<button
onClick={() => setConsiderarActivos(!considerarActivos)}
className={cn(
'flex items-center gap-2 rounded-lg px-3 py-2 text-sm font-medium transition-colors',
considerarActivos
? 'bg-primary/10 text-primary border border-primary/30'
: 'hover:bg-accent'
)}
title="Si está inactivo, no se consideran facturas tipo I con uso de CFDI I01-I08 (compras de activos fijos)."
>
<CheckSquare className="h-4 w-4" />
Considerar activos
</button>
<button
onClick={() => setConsiderarNCs(!considerarNCs)}
className={cn(
'flex items-center gap-2 rounded-lg px-3 py-2 text-sm font-medium transition-colors',
considerarNCs
? 'bg-primary/10 text-primary border border-primary/30'
: 'hover:bg-accent'
)}
title="Si está inactivo, no se consideran facturas tipo E con tipo de relación 01 (notas de crédito)."
>
<CheckSquare className="h-4 w-4" />
Considerar NCs
</button>
```
`CheckSquare` y `cn` ya están importados al inicio del archivo. NO agregues imports nuevos.
- [ ] **Step 4: Verificar typecheck del web**
Run: `cd C:/Users/chtr1/Downloads/Horux_despacho && pnpm --filter @horux/web exec tsc --noEmit 2>&1 | grep "impuestos/page"`
Expected: NO output.
- [ ] **Step 5: Smoke (opcional, defer si dev no corre)**
Si dev corre (`curl -s -o /dev/null -w "%{http_code}" http://localhost:3000 2>/dev/null` retorna algo distinto de 000):
1. Abrir `/impuestos`, pestaña ISR. Confirmar que aparecen 3 toggles: Conciliación, Considerar activos, Considerar NCs (todos OFF inicialmente).
2. Tooltip al hover en cada toggle nuevo describe el filtro.
3. Click "Considerar activos" → cambia a estilo activo (azul).
4. Verificar que los números de la tabla y la sección "Cálculo de ISR del Periodo" recalculan al togglear.
5. Smoke completo cross-feature en Task 10.
Si dev NO corre, **NO lo inicies**. Skip.
- [ ] **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): toggles 'Considerar activos' y 'Considerar NCs' en /impuestos"
```
---
## Task 10: Verificación final + sync OneDrive + commit V.1.0.7
**Files:**
- Verify: typecheck completo
- Smoke: cross-feature en browser
- Copy: 8 archivos a OneDrive (1 nuevo + 7 modificados)
- Commit: V.1.0.7
- [ ] **Step 1: Typecheck completo de shared + api**
```bash
cd C:/Users/chtr1/Downloads/Horux_despacho
pnpm --filter @horux/shared typecheck
pnpm --filter @horux/api typecheck
```
Expected: ambos PASS sin errores. Si falla, **STOP y reporta**.
- [ ] **Step 2: Verificar archivos web del plan limpios**
```bash
cd C:/Users/chtr1/Downloads/Horux_despacho
pnpm --filter @horux/web exec tsc --noEmit 2>&1 | grep -E "(lib/api/impuestos|lib/hooks/use-impuestos|impuestos/page)"
```
Expected: NO output (los 3 archivos web del plan están limpios; otros errores web son pre-existentes y fuera de scope).
- [ ] **Step 3: Smoke cross-feature**
Si dev corre y tienes acceso al browser:
1. **Default UI** (`/impuestos`, ambos toggles OFF):
- ISR/IVA cargan números menores que antes (excluyen activos + NCs).
- Tabla "Histórico ISR" usa los acumulados filtrados.
- Sección "Cálculo de ISR del Periodo" refleja los filtros consistentemente en `delPeriodo`, `anteriores`, `total`.
2. **Toggle "Considerar activos" ON**: ingresos/deducciones/base gravable suben con la suma de activos del periodo.
3. **Toggle "Considerar NCs" ON**: cambia el bucket — NCs aparecen restando.
4. **Combinaciones**: probar las 4 combinaciones de los 2 toggles + Conciliación on/off (8 total).
5. **Cross-check `/dashboard`**: KPIs (ingresos, gastos, utilidad) **NO cambian** vs antes del deploy. Esto valida que el default `true` en `calcular*PorRegimen` preserva el dashboard.
6. **Activos Fijos tab**: la tabla sigue mostrando todos los CFDIs I con uso I01-I08 (no afectada por el toggle "Considerar activos" en ISR/IVA).
7. **Cambiar contribuyente**: el state de los toggles persiste en sesión (no se resetea al cambiar contribuyente).
Si no puedes hacer smoke completo, reporta qué se verificó y qué quedó pendiente para el owner.
- [ ] **Step 4: Copiar archivos a OneDrive (8 archivos: 1 nuevo + 7 modificados)**
```bash
SRC="C:/Users/chtr1/Downloads/Horux_despacho"
DST="C:/Users/chtr1/OneDrive/Documentos/GitHub/Horux_despachos"
# Crear carpeta _shared si no existe en OneDrive
mkdir -p "$DST/apps/api/src/services/_shared"
cp -p "$SRC/apps/api/src/services/_shared/cfdi-filters.ts" "$DST/apps/api/src/services/_shared/cfdi-filters.ts"
cp -p "$SRC/apps/api/src/services/dashboard.service.ts" "$DST/apps/api/src/services/dashboard.service.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/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-filtros-activos-ncs-impuestos-fase1-design.md" "$DST/docs/superpowers/specs/2026-04-27-filtros-activos-ncs-impuestos-fase1-design.md"
cp -p "$SRC/docs/superpowers/plans/2026-04-27-filtros-activos-ncs-impuestos-fase1.md" "$DST/docs/superpowers/plans/2026-04-27-filtros-activos-ncs-impuestos-fase1.md"
```
- [ ] **Step 5: Verificar diff Downloads vs OneDrive**
```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 cosa, **STOP y reporta**.
- [ ] **Step 6: Commit en OneDrive**
```bash
cd "C:/Users/chtr1/OneDrive/Documentos/GitHub/Horux_despachos"
git status --short
```
Confirma que aparezcan exactamente los archivos copiados como M (modified) o ?? (untracked). Si hay algo más, reporta.
```bash
git add \
apps/api/src/services/_shared/cfdi-filters.ts \
apps/api/src/services/dashboard.service.ts \
apps/api/src/services/impuestos.service.ts \
apps/api/src/controllers/impuestos.controller.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-filtros-activos-ncs-impuestos-fase1-design.md \
docs/superpowers/plans/2026-04-27-filtros-activos-ncs-impuestos-fase1.md
git commit -m "V.1.0.7"
git status --short
git log -2 --oneline
```
Expected:
- Commit creado con hash nuevo, mensaje `V.1.0.7`.
- Working tree clean.
- `git log -2` muestra V.1.0.7 sobre V.1.0.6.
- [ ] **Step 7: NO push**
Push lo hace el owner manualmente. Confirmar explícitamente que NO se ejecutó `git push`.