Files
HoruxDespachosNuevo/docs/plans/2026-04-27-session.md

27 KiB
Raw Permalink Blame History

Sesión 2026-04-27 — Resumen del día

Sesión enfocada en el módulo de Impuestos y la gestión de planes: refactor del cálculo de ISR acumulado al estilo formato 14 del SAT, dos toggles nuevos para excluir activos fijos y NCs, extensión del filtro de activos para cubrir las cadenas P → I y E → {I, P → I}, límite de 5 RFCs durante el trial gratuito, fix de un bug crítico de scope SQL, y un plan "Custom" gratis sin fecha fin asignable solo por Admin Global.

Ocho releases shippeadas en OneDrive durante la sesión: V.1.0.6, V.1.0.7, V.1.0.8, V.1.0.9, V.1.0.11, V.1.0.12, V.1.0.14 (V.1.0.10 y V.1.0.13 fueron solo docs).


Índice

  1. V.1.0.6 — ISR base gravable acumulada y desglose del periodo
  2. V.1.0.7 — Filtros "Considerar activos" y "Considerar NCs" (Fase 1)
  3. V.1.0.8 — Defaults de los toggles a ON (cache-friendly)
  4. V.1.0.9 — Filtro de activos extendido a P y E relacionadas
  5. Spec en pipeline (no shipped) — Sort por nombre en drill-down
  6. V.1.0.11 — Límite de 5 RFCs durante trial gratuito
  7. V.1.0.12 — Fix bug de scope SQL en filtro de activos
  8. V.1.0.14 — Plan Custom (gratis, sin fecha fin, solo admin)
  9. Pendientes derivados

Documentos relacionados creados hoy:

  • docs/superpowers/specs/2026-04-27-isr-base-gravable-acumulada-design.md
  • docs/superpowers/plans/2026-04-27-isr-base-gravable-acumulada.md
  • 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
  • docs/superpowers/specs/2026-04-27-drill-down-sort-by-name-design.md (no implementado)
  • docs/superpowers/specs/2026-04-27-trial-rfc-limit-design.md
  • docs/superpowers/specs/2026-04-27-custom-plan-design.md

1. V.1.0.6 — ISR base gravable acumulada y desglose del periodo

Problema

La pestaña ISR de /impuestos calculaba la base gravable mes a mes con Math.max(0, ing ded). Esto perdía déficits acumulados: un mes con pérdida no reducía el acumulado de meses siguientes. La lógica fiscal correcta es acumular ingresos y deducciones desde enero, restar al final, y solo aplicar max(0, …) al pasar a ISR causado.

Cambios — Tabla "Histórico ISR"

Antes: 4 columnas (Mes, Ingresos, Deducciones, Base Gravable). Base Gravable era el max(0, ing_mes ded_mes) mensual independiente.

Después: 6 columnas — Mes, Ingresos, Ingresos Acum., Deducciones, Deducciones Acum., Base Gravable Acum. La columna BG mensual desaparece. La BG Acum. se calcula como ingAcum dedAcum sin clamp: si el acumulado es negativo, se renderiza en rojo (text-destructive). Fila "Total" eliminada (la última fila con datos ya es el YTD).

Cambios — Sección "Cálculo de ISR Acumulado" → "Cálculo de ISR del Periodo"

Rename del título y reescritura del card al estilo del formato 14 SAT:

Ingresos del periodo (Mar 2026)               $X
(+) Ingresos acumulados anteriores (Ene-Feb)  $A
() Deducciones del periodo (Mar 2026)        $Y
() Deducciones acumuladas anteriores         $B
─────────────────────────────────────────────
(=) Base gravable acumulada                   $X+AYB  ← rojo si negativa
ISR causado (acumulado)                       tarifa(max(0, BG))
() ISR retenido (acumulado)                  $R
─────────────────────────────────────────────
ISR a pagar                                   max(0, causado  retenido)

"Del periodo" = único el mes final del filtro (no el rango entero). "Anteriores" = enero hasta el mes previo al mes final, mismo año. Etiquetas de mes derivadas dinámicamente: mesFinal=1 muestra "(sin meses anteriores)", mesFinal=2 muestra "(Ene)", etc.

Backend — endpoint nuevo

GET /api/impuestos/isr/resumen-desglosado?fechaFin=...&conciliacion=...&contribuyenteId=...

Internamente llama 3 veces a getResumenIsr con rangos distintos:

  • delPeriodo: solo el mes final (1 mes)
  • anteriores: Ene-1 a (mesFinal-1)-último-día (vacío si mesFinal=1)
  • total: Ene-1 a último-día-del-mes-final

Promise.all para los 2 que son independientes (delPeriodo + total). Cuando mesFinal === 1, evita query inútil retornando emptyResumenIsr() para anteriores.

Archivos modificados (V.1.0.6)

packages/shared/src/types/impuestos.ts
apps/api/src/services/impuestos.service.ts
apps/api/src/controllers/impuestos.controller.ts
apps/api/src/routes/impuestos.routes.ts
apps/web/lib/api/impuestos.ts
apps/web/lib/hooks/use-impuestos.ts
apps/web/app/(dashboard)/impuestos/page.tsx

IsrMensual extendido con ingresosAcum, deduccionesAcum, baseGravableAcum (running totals desde enero). BaseGravableRegimen ganó isrCausado para alimentar la sección por régimen.


2. V.1.0.7 — Filtros "Considerar activos" y "Considerar NCs" (Fase 1)

Problema

La pestaña Impuestos no permitía excluir compras de activos fijos (que se deprecian, no se deducen mensualmente) ni notas de crédito (NCs tipoRel=01 que ajustan facturas previas). El contador necesita poder ver/ocultar estas categorías para análisis.

UI

Dos toggles nuevos junto a "Conciliación":

[Régimen ▾]  [☐ Conciliación]  [☐ Considerar activos]  [☐ Considerar NCs]

Mismo styling que Conciliación, tooltips descriptivos via title. En esta versión los defaults eran OFF (excluir por default). El default se invirtió a ON en V.1.0.8 — ver §3.

Toggle ON = considerar/incluir. Toggle OFF = no considerar/excluir.

Backend

Helper neutral en módulo nuevo apps/api/src/services/_shared/cfdi-filters.ts:

  • buildExtraFilters(considerarActivos, considerarNCs) → fragmento WHERE para queries con FROM cfdis directo.
  • buildExtraFiltersAlias(alias, considerarActivos, considerarNCs) → versión alias-aware para subqueries (FROM cfdis e).

Cuando ambos flags son true retorna string vacío (no afecta el WHERE).

Se extendieron 7 funciones de servicio con 2 nuevos parámetros opcionales, default true para preservar el comportamiento de los callers que no los pasan (dashboard, reportes, alertas):

Función Archivo
calcularIngresosPorRegimen dashboard.service.ts
calcularEgresosPorRegimen dashboard.service.ts
getResumenIva impuestos.service.ts
getIvaMensual impuestos.service.ts
getResumenIsr impuestos.service.ts
getIsrMensual impuestos.service.ts
getResumenIsrDesglosado impuestos.service.ts

Templates de subqueries de la rama I PPD/07 (SUM_E_REFERENCING_TRAS, SUM_E_REFERENCING_RET, HAS_E_REFERENCING_MISMO_MES) y sus helpers intermedios (bucketCausadoAny, bucketAcreditableAny, signed* exprs, readResumenIvaFromCache) propagaron los flags a través de 3 niveles.

Cache gate de IVA extendido: metricas_mensuales solo se consulta cuando !conciliacion && considerarActivos && considerarNCs. Cualquier toggle distinto del default backend → live query.

Frontend

  • API client: 5 funciones HTTP serializan los flags como query params, incluyendo cuando son false (if (flag !== undefined) params.set(..., String(flag))).
  • Hooks: 5 hooks incluyen los flags en queryKey para refetch al togglear.
  • UI: state + 2 toggle buttons + propagación a las 5 llamadas.

Decisión de diseño — Default backend true, default UI inicial false

El backend default true (= include todo) preserva dashboard, reportes, etc. La UI inicialmente arrancó con default false (= excluir por default) por lógica fiscal. La asimetría se resolvió en V.1.0.8.

Pruebas

  • pnpm typecheck shared + api: PASS.
  • Web typecheck para los archivos del plan: clean (otros errores web son pre-existentes, fuera de scope).
  • Smoke deferido a verificación manual del owner.

Fase 2 (futura)

Extender metricas_mensuales con columnas base + 2 deltas (*_activos, *_ncs_01) por métrica IVA. Hace los toggles instantáneos vía suma/resta sin live query.


3. V.1.0.8 — Defaults de los toggles a ON (cache-friendly)

Motivación

Tras shippear V.1.0.7, el final code review levantó una observación importante: con UI default OFF (ambos toggles excluyendo), el cache metricas_mensuales queda siempre bypass-eado en /impuestos. Cada carga inicial era live query (~1-3s). El cache solo servía cuando el contador activaba manualmente ambos toggles.

Decisión

Invertir defaults UI de false a true. Trade-off:

  • Antes (V.1.0.7): default OFF → carga inicial siempre lenta. Default fiscalmente "más correcto" (excluir por automático).
  • Después (V.1.0.8): default ON → carga inicial rápida (cache hit, comportamiento idéntico al de versiones previas). El contador activa el filtro cuando lo necesita.

La consciencia del filtro queda como acción del contador, no como default silencioso. Fase 2 elimina el dilema: con cache base+deltas, ambos defaults serán igual de rápidos.

Cambios

// Antes:
const [considerarActivos, setConsiderarActivos] = useState(false);
const [considerarNCs, setConsiderarNCs] = useState(false);

// Después:
const [considerarActivos, setConsiderarActivos] = useState(true);
const [considerarNCs, setConsiderarNCs] = useState(true);

Plus actualización del spec doc para reflejar la decisión.

Archivos modificados (V.1.0.8)

apps/web/app/(dashboard)/impuestos/page.tsx
docs/superpowers/specs/2026-04-27-filtros-activos-ncs-impuestos-fase1-design.md

4. V.1.0.9 — Filtro de activos extendido a P y E relacionadas

Problema

El filtro de activos en V.1.0.7 solo excluía facturas tipo I con uso I01-I08. Pero en realidad un activo fijo se materializa en varias facturas relacionadas:

  1. La I de la compra (uso I01-I08).
  2. La P (complemento de pago) que paga esa compra.
  3. Eventualmente una E (NC) que cancela la compra o el pago.

Si el contador desactiva "Considerar activos", esperaría ver excluidas todas las facturas asociadas a la operación de activo fijo, no solo la I original. De lo contrario los pagos y NCs quedan visibles sin la factura que los origina, generando inconsistencia.

Solución

Extender el predicado del filtro de activos para cubrir 3 capas:

Capa Predicado SQL
1. I directa tipo_comprobante = 'I' AND uso_cfdi IN (I01-I08)
2. P → I-activo tipo='P' AND EXISTS(SELECT 1 FROM cfdis i_act WHERE i_act.uuid=uuid_relacionado AND i_act.tipo='I' AND i_act.uso_cfdi IN (I01-I08))
3. E → {I-activo, P-de-activo} tipo='E' AND EXISTS(SELECT 1 FROM cfdis r_act WHERE r_act.uuid IN cfdis_relacionados pipe-split AND (r_act es I-activo OR r_act es P-de-activo))

La capa 3 cubre los dos casos del owner:

  • E tipoRel=01 que cancela una P que pagó un activo (caso 1 del prompt).
  • E tipoRel=03 que devuelve directamente una I-activo (caso 3 del prompt).
  • Cualquier otro tipoRel — el predicado es genérico, no filtra por cfdi_tipo_relacion en la rama de activos.

Independencia con el filtro de NCs

Los dos filtros operan en AND:

  • "Considerar NCs" OFF: excluye todas las E tipoRel=01, sin importar a qué se relacionen.
  • "Considerar activos" OFF: excluye E (cualquier tipoRel) que se relacione con activos.

Una E tipoRel=01 sobre I regular: solo el filtro de NCs la afecta. Una E tipoRel=03 sobre I-activo: solo el filtro de activos la afecta. Una E tipoRel=01 sobre I-activo: ambos filtros la excluirían.

Comportamiento por tipo de CFDI

CFDI Excluido si activos OFF?
I uso I01-I08 predicado 1
I uso G03 (gasto regular)
P pagando I-activo predicado 2
P pagando I regular
E tipoRel=01 → I-activo predicado 3
E tipoRel=03 → I-activo predicado 3
E tipoRel=01 → P-de-activo predicado 3
E tipoRel=07 → I PPD/07 (anticipo) no es activo
E tipoRel=01 → I regular (lo cubre el filtro NCs si está OFF)

Implementación

Cambio único en apps/api/src/services/_shared/cfdi-filters.ts (~70 líneas netas, el archivo creció de ~50 a ~110 líneas). Helpers internos activosExclusionNoAlias() y activosExclusionAlias(alias) encapsulan los 3 predicados. buildExtraFilters y buildExtraFiltersAlias los invocan cuando !considerarActivos.

Cero cambios downstream — todos los callsites del helper (16+ en service/dashboard) heredan automáticamente el comportamiento extendido.

Performance

  • Default UI ON (V.1.0.8): el helper retorna empty string. Cero impacto, cache hit normal.
  • Filtro activos OFF: cada query con FROM cfdis ejecuta los 3 predicados con EXISTS anidados. Sin índice en uuid_relacionado ni cfdis_relacionados, las queries grandes pueden ser ~10-20% más lentas. Aceptable para Fase 1.
  • Si el perf hit se vuelve notorio, evaluar índice B-tree en uuid_relacionado y GIN sobre array para cfdis_relacionados en Fase 2 (parte del cache extension).

Archivos modificados (V.1.0.9)

apps/api/src/services/_shared/cfdi-filters.ts

5. Spec en pipeline (no shipped) — Sort por nombre en drill-down

Se diseñó y especificó el cambio para agregar sort por nombre emisor / receptor en la página /drill-down genérica (los KPIs del dashboard abren ahí). Spec en docs/superpowers/specs/2026-04-27-drill-down-sort-by-name-design.md, shippeada en V.1.0.5.

Estado: spec aprobada, implementación pendiente. Cambio trivial (~6 líneas en un archivo). Las páginas de alertas con tablas similares quedaron fuera de scope para evitar plan grande — se evaluarán en otra sesión.


6. V.1.0.11 — Límite de 5 RFCs durante trial gratuito

Problema

Despachos en periodo de prueba (30 días) podían agregar RFCs sin restricción. El owner pidió un límite duro de 5 RFCs para forzar al contador a contratar un plan si necesita gestionar más.

Reglas

Estado Límite RFCs
Trial activo (tenant.trialEndsAt > now) 5 contribuyentes activos (5 OK, 6 bloqueado)
Trial expirado Aplica el límite del plan vigente; este spec no agrega nada nuevo
Plan pagado (sin trial activo) Sin nuevo límite

Backend (apps/api/src/controllers/contribuyente.controller.ts)

Constante local TRIAL_MAX_CONTRIBUYENTES = 5. En el handler create, antes del createContribuyente:

const tenant = await prisma.tenant.findUnique({
  where: { id: req.user!.tenantId },
  select: { trialEndsAt: true },
});
const isTrialActive = tenant?.trialEndsAt ? tenant.trialEndsAt > new Date() : false;
if (isTrialActive) {
  const activeCount = await countActiveContribuyentes(req.tenantPool!);
  if (activeCount >= TRIAL_MAX_CONTRIBUYENTES) {
    return next(new AppError(
      403,
      `Durante el periodo de prueba puedes gestionar hasta ${TRIAL_MAX_CONTRIBUYENTES} contribuyentes. Contrata un plan para agregar más.`,
    ));
  }
}

Frontend (apps/web/app/(dashboard)/contribuyentes/page.tsx)

  • useQuery(['my-plan-info'], ...) para fetch /despachos/me/plan (endpoint existente).

  • Cómputo trialAtLimit = isTrialActive && activeCount >= 5.

  • Los 2 botones "Agregar RFC" / "Agregar primer RFC" reciben disabled={trialAtLimit} con title mostrando el tooltip exacto literal del owner:

    "Límite de contribuyentes para la prueba gratuita, para continuar agregando contribuyentes, selecciona un plan."

No-cambios

  • No nueva tabla, no migration.
  • Mi Empresa hard limit a 1 RFC sigue siendo billing-only (out of scope).
  • tenant.cfdiLimit, tenant.usersLimit no se tocan.

Archivos modificados (V.1.0.11)

apps/api/src/controllers/contribuyente.controller.ts
apps/web/app/(dashboard)/contribuyentes/page.tsx
docs/superpowers/specs/2026-04-27-trial-rfc-limit-design.md

7. V.1.0.12 — Fix bug de scope SQL en filtro de activos

Problema reportado por owner

Tras shippear V.1.0.9 (filtro de activos extendido a P y E relacionadas), el owner reportó que la factura 8ec2eaf3-7879-11f0-81a8-8daae9822b10 (tipo P, monto $295,100) seguía apareciendo en cálculos cuando desactivaba el toggle "Considerar activos". La P pagaba la I 5C874749-748F-11F0-96B1-2B9310891836, que tenía uso_cfdi = I03 (Equipo de transporte) — un activo fijo per la regla.

Causa raíz — scope ambiguity SQL

En activosExclusionNoAlias() (helper en _shared/cfdi-filters.ts), el subquery del predicado P referenciaba LOWER(uuid_relacionado) sin qualifying. Como el subquery usa FROM cfdis i_act y i_act también tiene la columna uuid_relacionado, PostgreSQL resolvía la referencia no-qualificada al scope interno (i_act.uuid_relacionado) en vez del outer (cfdis.uuid_relacionado).

Resultado: el predicado evaluaba "¿existe un i_act donde i_act.uuid = i_act.uuid_relacionado AND uso I01-I08?" — eso es prácticamente siempre false (un CFDI no se referencia a sí mismo). El AND NOT (FALSE) = AND TRUE, así que la P nunca se excluía.

Mismo bug en el predicado E (cfdis_relacionados no qualificado dentro del subquery con r_act que también tiene esa columna).

La versión Alias (activosExclusionAlias) NO tenía el bug porque ahí escribí ${alias}.uuid_relacionado qualificado explícitamente.

Fix

Qualifying las 2 referencias outer dentro de los subqueries con cfdis.:

-- Antes (bug):
WHERE LOWER(i_act.uuid) = LOWER(uuid_relacionado)

-- Después (fix):
WHERE LOWER(i_act.uuid) = LOWER(cfdis.uuid_relacionado)

Y para el predicado E:

-- Antes (bug):
WHERE LOWER(r_act.uuid) = ANY(string_to_array(LOWER(cfdis_relacionados), '|'))

-- Después (fix):
WHERE LOWER(r_act.uuid) = ANY(string_to_array(LOWER(cfdis.cfdis_relacionados), '|'))

Comentario JSDoc agregado a la función explicando por qué necesita qualifying explícito (evitar futuras regresiones).

Validación

Test directo con SQL antes/después del fix:

BUGGY version (sin qualifying outer):
  bug_excludes = false   ← P nunca se excluía
FIXED version (cfdis.uuid_relacionado):
  fixed_excludes = true  ← P correctamente excluida

Referenced I: { uuid: '5c874749...', uso_cfdi: 'I03', tipo_comprobante: 'I' }

Lecciones

  • Qualifying explícito es defensivo siempre que un subquery comparta nombre de tabla con el outer. Postgres prioriza scope interno sin warning, así que el bug es silencioso.
  • El test del owner en producción es indispensable: el typecheck y los tests de tipo no detectan errores semánticos de SQL. El fix solo apareció porque el owner reportó un caso específico que esperaba excluir.

Archivos modificados (V.1.0.12)

apps/api/src/services/_shared/cfdi-filters.ts

8. V.1.0.14 — Plan Custom (gratis, sin fecha fin, solo admin)

Problema

El owner pidió un plan especial para casos de cortesía / beta tester / caso especial: acceso al sistema sin cobro, sin fecha de finalización, asignable solo por el Admin Global y oculto del catálogo user-facing.

Decisión clave — Reusar enum custom

El Plan enum de Prisma ya tenía custom (legacy: "precio variable por tenant") con 0 tenants en dev. La lógica antigua en subscription.service.ts ya rechazaba custom del flujo self-serve — patrón que coincide con la nueva semántica. Reusar el enum evita migration y mantiene compatibilidad con código existente.

Comportamiento

  • Limits: idéntico a Mi Empresa (1 RFC, 50 timbres/mes, MANAGED, sin API ni Lolita).
  • Costo: $0. NO se incluye en DESPACHO_PLAN_PRICES → no genera Subscription, no usa MercadoPago.
  • Vigencia: indefinida. tenant.trialEndsAt = null. Sin currentPeriodEnd. Ningún cron lo expira (expireTrials, applyPendingChanges no le afectan).
  • Visibilidad: oculto del catálogo en /configuracion/planes-despacho (página user). Solo aparece como opción en /clientes (admin global).

Catálogo (packages/shared/src/constants/despacho-plans.ts)

Nueva entrada custom en DESPACHO_PLANS con limits idénticos a Mi Empresa y mismo array de features. NO se agrega a DESPACHO_PLAN_PRICES — helpers permiteOverage('custom') y isDespachoPaidPlan('custom') ya retornan false por exclusión.

Admin /clientes — extensión del dropdown

La página admin tenía un dropdown limitado a starter | business | enterprise. Cambios:

  • PlanType extendido a starter | business | business_ia | enterprise | custom.
  • Tipo CreateTenantData.plan y UpdateTenantData.plan en apps/web/lib/api/tenants.ts extendidos al mismo union.
  • Dropdown ahora lista los 4 legacy con etiqueta "(legacy)" + Custom con descripción "Sin cobro, sin fecha fin (despacho)".
  • Cuando se selecciona Custom, el input "Monto Mensual" se oculta y aparece nota: "Plan Custom no genera cobro ni suscripción. Vigencia indefinida."
  • Lista de tenants ahora usa el PLAN_LABELS global (cubre todos los planes incluyendo despacho) en vez del planLabels local que solo cubría legacy. planColors extendido con entradas para todos los planes despacho + custom.

Out of scope: asignar planes despacho pagables (mi_empresa, mi_empresa_plus, business_control, business_cloud) desde /clientes. Esos van por self-serve del owner para evitar el escenario de un tenant en plan paid sin Subscription. Si se necesita en el futuro, requiere manejar la creación/cancelación de preapproval MP en el endpoint admin.

User /configuracion/planes-despacho — banner Custom

  • Despachoplan type extendido con custom.
  • Si currentPlan === 'custom': banner rosa al top con "Plan Custom — sin cobro, vigencia indefinida" + descripción "Tu cuenta está bajo un plan especial asignado por tu administrador. Contacta a soporte si necesitas cambiar de plan." Las cards de planes pagables se ocultan (no hay opción de auto-cambio).
  • Otros planes: comportamiento idéntico al de antes.

Backend — sin cambios

PUT /api/tenants/:id ya acepta cualquier valor del enum Prisma (no hay Zod gate restrictivo en el endpoint admin). Solo el endpoint self-serve addMyTenant tiene Zod limitado a legacy — no se toca, sigue siendo correcto que self-serve no permita Custom.

Limitaciones aceptadas

  1. Transición paid → custom: si el admin cambia un tenant que tenía suscripción MP activa a Custom, el preapproval MP sigue cobrando hasta que se cancele manualmente. Mitigación: el admin debe cancelar la suscripción primero desde /configuracion/suscripcion del tenant impersonado.
  2. Hard limit 1 RFC en Custom: igual que Mi Empresa, el límite de 1 RFC para Custom es solo billing-only hoy (no enforced en contribuyente.controller.ts:create). Si se quiere enforce duro, replicar el patrón del trial limit V.1.0.11.

Archivos modificados (V.1.0.14)

packages/shared/src/constants/despacho-plans.ts
apps/web/lib/api/tenants.ts
apps/web/app/(dashboard)/clientes/page.tsx
apps/web/app/(dashboard)/configuracion/planes-despacho/page.tsx
docs/superpowers/specs/2026-04-27-custom-plan-design.md

9. Pendientes derivados

Verificación manual del owner (smoke)

Aún no se ha ejecutado smoke en navegador para los cambios de la sesión. Lista mínima:

  1. Dashboard regression: KPIs (Ingresos, Gastos, Utilidad) deben ser idénticos a los de V.1.0.5 (= sin filtros aplicados).
  2. Histórico ISR (V.1.0.6): 6 columnas, BG_acum negativa en rojo, running totals correctos, sin fila Total.
  3. Cálculo de ISR del Periodo (V.1.0.6): 8 renglones, etiquetas dinámicas según mesFinal, BG en rojo si negativa.
  4. Toggle "Considerar activos" OFF (V.1.0.7+V.1.0.9):
    • Excluye I uso I01-I08.
    • Excluye P pagando esos I.
    • Excluye E (cualquier tipoRel) referenciando esos I o P.
  5. Toggle "Considerar NCs" OFF (V.1.0.7): excluye E tipoRel=01.
  6. Combinaciones de los 3 toggles (Conciliación + Activos + NCs): 8 combinaciones, números deben ser consistentes.
  7. Activos Fijos tab: la tabla sigue mostrando todos los I con uso I01-I08, no afectada por los toggles de impuestos.
  8. Filtro de régimen: sigue distribuyendo correctamente cuando se selecciona un régimen y se togglea cualquier filtro.

Performance follow-ups

  • Si toggle activos OFF en /impuestos siente lento (>3-4s), considerar:
    • Índice B-tree en uuid_relacionado (cheap).
    • Migración para indexar cfdis_relacionados (caro: requiere GIN sobre array, o normalizar a tabla M:N).
  • Fase 2 del feature de filtros: extender metricas_mensuales con columnas base + deltas. Toggles instantáneos sin importar el estado.

Push manual

Las versiones están commiteadas en OneDrive pero no pusheadas a origin/main. Owner las push cuando quiera.

72ffdca V.1.0.14  ← Custom plan (gratis, sin fecha fin, solo admin)
6686e70 V.1.0.13  ← session doc updated (V.1.0.11 + V.1.0.12)
59d71ae V.1.0.12  ← fix scope SQL helper (bug crítico V.1.0.9)
f4e0d6f V.1.0.11  ← trial RFC limit (5 max)
b8c9df1 V.1.0.10  ← session summary doc original
297ffdb V.1.0.9   ← filtro activos extendido a P y E (con bug que V.1.0.12 arregla)
4b7566e V.1.0.8   ← defaults flipped a ON
2970ccf V.1.0.7   ← filtros activos/NCs Fase 1
cc34c39 V.1.0.6   ← ISR base gravable acumulada

Fuera de scope (a evaluar después)

  • Implementar drill-down sort por nombre (spec ya aprobada).
  • Replicar los toggles de impuestos a /dashboard (si se pide): ya está habilitado por las signatures de calcular*PorRegimen — solo falta UI + propagación.
  • Persistencia de los toggles (hoy son useState, se pierden al recargar): considerar localStorage o tenant-view-store.
  • Hard limit Mi Empresa (1 RFC): hoy es solo billing-only. Replicar el patrón del trial limit V.1.0.11 (Mi Empresa también haría check duro al crear). Aplica también para Custom (1 RFC). Considerar al implementar Fase 2 del feature de filtros.
  • Bug class similar al V.1.0.12 en otras subqueries: revisar otros helpers SQL del repo que tengan subqueries con tablas del mismo nombre (cfdis, conciliaciones, etc.) y qualifying débil. Sería un audit pasivo, no urgente.
  • Asignar planes despacho pagados desde /clientes admin: hoy Custom es el único plan despacho asignable desde admin. Si se quiere agregar también mi_empresa, business_control, etc., requiere manejar creación/cancelación de preapproval MP en el endpoint admin. Out of scope para V.1.0.14, pendiente futuro.