947 lines
34 KiB
Markdown
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`.
|