# 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 ``` - [ ] **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 ``` - [ ] **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 ``` - [ ] **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 ``` - [ ] **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 { 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(`/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 `
`, agregar: ```tsx ``` `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`.