# 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](#1-v106--isr-base-gravable-acumulada-y-desglose-del-periodo) 2. [V.1.0.7 — Filtros "Considerar activos" y "Considerar NCs" (Fase 1)](#2-v107--filtros-considerar-activos-y-considerar-ncs-fase-1) 3. [V.1.0.8 — Defaults de los toggles a ON (cache-friendly)](#3-v108--defaults-de-los-toggles-a-on-cache-friendly) 4. [V.1.0.9 — Filtro de activos extendido a P y E relacionadas](#4-v109--filtro-de-activos-extendido-a-p-y-e-relacionadas) 5. [Spec en pipeline (no shipped) — Sort por nombre en drill-down](#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](#6-v1011--límite-de-5-rfcs-durante-trial-gratuito) 7. [V.1.0.12 — Fix bug de scope SQL en filtro de activos](#7-v1012--fix-bug-de-scope-sql-en-filtro-de-activos) 8. [V.1.0.14 — Plan Custom (gratis, sin fecha fin, solo admin)](#8-v1014--plan-custom-gratis-sin-fecha-fin-solo-admin) 9. [Pendientes derivados](#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+A−Y−B ← 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 ```ts // 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`: ```ts 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.`: ```sql -- 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: ```sql -- 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.