14 KiB
Filtros "Considerar activos" y "Considerar NCs" en /impuestos — Fase 1
Contexto
La pestaña ISR e IVA de /impuestos actualmente solo tiene un toggle de
"Conciliación" que cambia la semántica de fechas. El owner pidió dos toggles
adicionales:
- Considerar activos — cuando ACTIVADO, incluye facturas tipo I con
uso_cfdi∈ {I01, I02, I03, I04, I05, I06, I07, I08} (compras de activos fijos / inversiones). Cuando DESACTIVADO, excluye esas facturas. - Considerar NCs — cuando ACTIVADO, incluye facturas tipo E con
cfdi_tipo_relacion = '01'(notas de crédito). Cuando DESACTIVADO, las excluye.
Decisión de defaults
Default ambos toggles ON (incluir) — revertido del default original OFF
por concerns de performance: con default OFF, el cache metricas_mensuales
quedaría siempre bypass-eado en /impuestos hasta Fase 2. Con default ON,
las cargas iniciales aprovechan el cache (comportamiento idéntico al de
versiones previas), y el contador opt-in al view filtrado cuando lo necesita.
Trade-off aceptado: el contador debe desactivar manualmente los toggles cuando quiere ver números sin activos / sin NCs. La lógica fiscal de "depreciación de activos" requiere consciencia del contador, no se aplica silenciosamente.
Los filtros aplican solo en la pestaña Impuestos (IVA + ISR). Dashboard, reportes, drill-downs, alertas y demás permanecen intactos.
Justificación fiscal
- Los activos fijos (uso I01-I08) deben depreciarse, no deducirse en su mes
de adquisición. Excluirlos del cálculo provisional mensual evita inflar las
deducciones. La pestaña dedicada "Activos Fijos" (en
/impuestos) es donde se muestra y gestiona esa información. - Las NCs tipoRel=01 son ajustes a documentos previos. El owner quiere ver los números brutos sin ajustes por default y opt-in con el toggle. Asume el riesgo de over-reporting si el contador olvida activarlo.
Fases
- Fase 1 (este spec): UI + backend con live query. Sin cambios al cache
metricas_mensuales. Cuando los toggles están en su default (OFF), el cache queda bypass-eado y todo es live query. - Fase 2 (spec posterior): extender
metricas_mensualescon columnas base + 2 deltas para hacer el toggle instantáneo (computado por suma/resta).
Cambios — Frontend
apps/web/app/(dashboard)/impuestos/page.tsx
State nuevo (defaults true = filter active = incluir, cache-friendly):
const [considerarActivos, setConsiderarActivos] = useState(true);
const [considerarNCs, setConsiderarNCs] = useState(true);
Con defaults true, las cargas iniciales aprovechan el cache de
metricas_mensuales. El gate !conciliacion && considerarActivos && considerarNCs
queda en true por default y permite cache hit. El contador opt-in al view
filtrado desactivando los toggles cuando lo necesita.
UI: 2 toggle buttons en la misma fila que "Conciliación", mismo styling.
Orden recomendado: Régimen | Conciliación | Considerar activos | Considerar NCs.
<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>
(Análogo para Considerar NCs con tooltip "...facturas tipo E con tipo de relación 01 (notas de crédito).")
Pasar a todos los hooks consumidos en la pestaña ISR e IVA.
apps/web/lib/hooks/use-impuestos.ts
Extender 5 hooks. Ejemplo:
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 el mismo patrón a useResumenIsr, useResumenIva, useIsrMensual,
useIvaMensual.
apps/web/lib/api/impuestos.ts
Extender funciones HTTP. Ejemplo:
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;
}
(Análogo para getResumenIsr, getResumenIva, getIsrMensual, getIvaMensual.)
Cambios — Backend
Helper compartido en apps/api/src/services/impuestos.service.ts
/**
* Construye fragmentos AND adicionales para WHERE clauses según los toggles
* "Considerar activos" y "Considerar NCs" en 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), retorna string vacío. Esto preserva
* el comportamiento histórico para callers que no pasan los flags (ej. dashboard).
*/
function buildExtraFilters(considerarActivos: boolean, considerarNCs: boolean): string {
const parts: string[] = [];
if (!considerarActivos) {
parts.push(`AND NOT (tipo_comprobante = 'I' AND uso_cfdi IN ('I01','I02','I03','I04','I05','I06','I07','I08'))`);
}
if (!considerarNCs) {
parts.push(`AND NOT (tipo_comprobante = 'E' AND COALESCE(cfdi_tipo_relacion, '') = '01')`);
}
return parts.length > 0 ? ' ' + parts.join(' ') : '';
}
Funciones modificadas
Agregar 2 parámetros booleanos opcionales con default true (= include
todo, comportamiento histórico). Forman el último par de la signature.
| Función | Archivo | Cambio |
|---|---|---|
calcularIngresosPorRegimen |
dashboard.service.ts |
+considerarActivos=true, considerarNCs=true, concatenar buildExtraFilters(...) al WHERE |
calcularEgresosPorRegimen |
dashboard.service.ts |
Idem |
getResumenIva |
impuestos.service.ts |
Idem + propagar al cache gate (ver abajo) |
getIvaMensual |
impuestos.service.ts |
Idem |
getResumenIsr |
impuestos.service.ts |
Idem + propagar a calcular*PorRegimen |
getIsrMensual |
impuestos.service.ts |
Idem + propagar a calcular*PorRegimen |
getResumenIsrDesglosado |
impuestos.service.ts |
Idem + propagar a las 3 llamadas a getResumenIsr |
Importante: como buildExtraFilters está en impuestos.service.ts y
calcular*PorRegimen viven en dashboard.service.ts, hay que mover el
helper a un módulo compartido o duplicarlo. Recomendación: mover a un
nuevo apps/api/src/services/_shared/cfdi-filters.ts (módulo neutral
reutilizable). Ambos services lo importan.
Aplicación del fragmento
Concatenar al WHERE de TODA query que escanee cfdis dentro de las funciones
afectadas. Buscar patrón WHERE ${VIGENTE} AND ${FR} y agregar
${buildExtraFilters(...)} al final del WHERE.
Ejemplo en una query existente:
const FR = getFR(conciliacion);
const extra = buildExtraFilters(considerarActivos, considerarNCs);
const { rows } = await pool.query(`
SELECT regimen_fiscal_emisor as regimen, ...
FROM cfdis
WHERE ${VIGENTE} AND ${FR}${extra}
AND ${ctx.esEmisor}
GROUP BY regimen_fiscal_emisor
`, [fechaInicio, fechaFin]);
buildExtraFilters ya retorna con leading space, así que se concatena directo.
Subqueries con alias (SUM_E_REFERENCING_*): el alias e para la tabla
externa requiere referenciar columnas como e.tipo_comprobante,
e.uso_cfdi, e.cfdi_tipo_relacion. Necesitamos una variante del helper que
acepte alias:
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 ('I01','I02','I03','I04','I05','I06','I07','I08'))`);
}
if (!considerarNCs) {
parts.push(`AND NOT (${alias}.tipo_comprobante = 'E' AND COALESCE(${alias}.cfdi_tipo_relacion, '') = '01')`);
}
return parts.length > 0 ? ' ' + parts.join(' ') : '';
}
Y se usa donde aparezcan subqueries con alias e (ej. SUM_E_REFERENCING_*,
HAS_E_REFERENCING_MISMO_MES, E_REFERENCIA_I_PPD_07_MISMO_MES si existe).
Controllers — apps/api/src/controllers/impuestos.controller.ts
Helper para parsear (junto a parseConciliacion):
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';
}
Cada handler relevante (getResumenIva, getIvaMensual, getResumenIsr,
getIsrMensual, getResumenIsrDesglosado) parsea los 2 nuevos flags con
default true y los pasa al service.
const considerarActivos = parseFlag(req, 'considerarActivos', true);
const considerarNCs = parseFlag(req, 'considerarNCs', true);
Razón del default true en el controller: si por algún motivo el query
param no llega (cliente legacy, prueba manual, otro consumer), comportamiento
es como antes (todo incluido). El frontend siempre manda el flag explícito,
así que en la práctica el default solo aplica al testing externo.
Cache gate en getResumenIva (línea ~322)
Extender la condición:
if (
!conciliacion &&
considerarActivos && // new — cache solo aplica con backend default
considerarNCs && // new
contribuyenteId &&
...
) {
const cached = await readResumenIvaFromCache(...);
if (cached) return cached;
}
Con UI default (ambos toggles ON), considerarActivos=true && considerarNCs=true
→ cache hit (comportamiento idéntico a versiones previas). Cuando el contador
desactiva alguno → cache bypass → live query (~1-3s). Aceptable porque el
desactivado es action consciente, no la carga inicial. Fase 2 hará los toggles
instantáneos vía cache base+deltas.
No-cambios
- Schema BD: ninguno. SQL puro.
- Cache
metricas_mensuales: estructura intacta. Solo se actualiza el gate. - Dashboard, reportes, drill-downs, alertas: comportamiento idéntico
(gracias a defaults
trueencalcular*PorRegimen). - Activos Fijos tab: usa su propio
activos-fijos.service.ts, no pasa por las funciones filtradas. Verificar en el smoke. getRegimenesDelPeriodoy otros que NO calculan ingresos/deducciones no se modifican. Los regímenes disponibles en el dropdown siguen siendo los mismos (basados en presencia de CFDIs, no filtrados por estos toggles).
Riesgos
- Tocar funciones compartidas con dashboard:
calcular*PorRegimenviven endashboard.service.ts. Defaulttruedebería preservar el dashboard, pero hay que verificar manualmente post-deploy. - Performance Fase 1: con UI default ON (cache-friendly), las cargas iniciales son rápidas. Solo cuando el contador desactiva un toggle hay live query. Fase 2 elimina ese delay también.
- Subqueries con alias: hay 5+ subqueries con alias
eenimpuestos.service.ts(rama I PPD/07). Cada una necesita el helper alias. Riesgo de olvidar una → resultados inconsistentes. - NCs default OFF puede sobre-reportar ingresos: el contador puede no notar que las NCs están excluidas si no lee el tooltip. Mitigación: tooltip claro y label "Considerar NCs" (lectura obvia).
Plan de pruebas (smoke)
- Typecheck:
pnpm --filter @horux/shared typecheck,pnpm --filter @horux/api typecheck. Ambos PASS sin errores. - Dashboard regression: abrir
/dashboard→ KPIs (ingresos, gastos, utilidad) deben tener los mismos valores que antes del deploy. - Activos Fijos tab: abrir
/impuestos→ pestaña "Activos Fijos" → la tabla debe seguir mostrando todas las facturas I con uso I01-I08. - UI default (ambos toggles OFF): cargar
/impuestosISR. Verificar que ingresos del periodo y deducciones son menores que antes (excluyen activos + NCs tipoRel=01). - Toggle "Considerar activos" ON: deducciones suben con la suma de los activos del periodo.
- Toggle "Considerar NCs" ON: comportamiento depende del lado:
- Como receptor (NC recibida que cancela una factura PUE): deducciones bajan (la NC resta).
- Como emisor (NC emitida que cancela una factura PUE propia): ingresos bajan.
- Combinaciones de los 3 toggles (Conciliación + Activos + NCs): ocho combinaciones, números deben ser consistentes.
- IVA tab: mismas pruebas (toggle on/off, comparar números).
- Tabla "Histórico ISR": debe respetar los 2 nuevos toggles también (cada fila refleja los acumulados con los filtros activos).
- Sección "Cálculo de ISR del Periodo": las 3 ramas (
delPeriodo,anteriores,total) deben respetar los toggles consistentemente.
Pendientes derivados
- Fase 2: extender
metricas_mensualescon columnas*_activos,*_ncs_01(×3 métricas IVA = 6 columnas nuevas). Migration + recompute del cache + actualizar lectura del cache para hacer suma/resta según toggles. Fase 2 entrega toggles instantáneos. - Tooltip + iconos: si el owner quiere distinguir visualmente los 3 toggles (Conciliación con un check, Activos con un asset icon, NCs con un document icon), aplicar después.
- Persistencia de los toggles: hoy el state vive en
useState, se pierde al recargar. Si se quiere persistir, considerarlocalStorageo agregar atenant-view-store. Out-of-scope para Fase 1. - Dashboard parity: si en el futuro el owner quiere los mismos toggles en
/dashboard, ya está habilitado por la signature decalcular*PorRegimen— solo falta UI + propagación. Out-of-scope.