# Sesión 2026-04-26 — Resumen del día Sesión consolidada con cuatro frentes: (1) limpieza de la columna "Tipo" en CFDI y drill-downs, (2) rebrand de planes despacho con nuevos precios y dos planes nuevos para empresas, (3) overage despacho a 100 RFCs y generalización a Business Control + Enterprise, (4) compensación IVA para el patrón I/07 PPD ↔ E mismo mes. Los frentes fiscales y de planes tienen documentos dedicados; este doc agrega una guía y captura lo que no entró ahí. --- ## Índice 1. [Limpieza columna "Tipo" en CFDI y drill-downs](#1-limpieza-columna-tipo-en-cfdi-y-drill-downs) 2. [Rebrand de planes despacho](#2-rebrand-de-planes-despacho) 3. [Overage despacho generalizado](#3-overage-despacho-generalizado) 4. [Compensación IVA I/07 PPD ↔ E mismo mes](#4-compensación-iva-i07-ppd--e-mismo-mes) 5. [Refactor IVA — fórmula del owner](#5-refactor-iva--fórmula-del-owner) 6. [Notificaciones email automáticas (alertas + recordatorios)](#6-notificaciones-email-automáticas-alertas--recordatorios) 7. [Pendientes](#7-pendientes) Documentos relacionados creados/actualizados hoy: - `docs/plans/2026-04-26-i07-ppd-compensacion.md` (creado en otro turno; §8 agregada hoy con la extensión IVA) - `docs/plans/2026-04-26-rebrand-planes-despacho.md` (creado hoy) - `docs/plans/2026-04-26-iva-refactor.md` (creado hoy — refactor que reemplaza la compensación I PUE/07 y el clamp en P) - `docs/plans/2026-04-26-notifications-email.md` (creado hoy — cron 8:30 AM con emails por alerta nueva y recordatorio próximo a vencer) - `docs/plans/2026-04-26-sprints-1-2-3.md` (creado hoy — pre-deploy IVA, bugs latentes, decisiones del owner D1-D7, sprint 6 SAT) - `docs/plans/2026-04-26-admin-global-setup.md` (creado hoy — bootstrap admin global, gestión clientes, add-ons UI, auto-facturación, redirect login → /clientes) --- ## 1. Limpieza columna "Tipo" en CFDI y drill-downs ### Problema La columna "Tipo" (EMITIDO/RECIBIDO) era ruido: la información ya está implícita en la posición del RFC emisor/receptor relativa al contribuyente activo. Aparecía en `/cfdi`, en los drill-downs de métricas del dashboard y en los drill-downs de alertas, además de duplicarse en cada export Excel. ### Cambios **Frontend `/cfdi`** (`apps/web/app/(dashboard)/cfdi/page.tsx`): - Removida `Tipo` y la celda `` con el badge. - Removida `'Tipo': cfdi.type === 'EMITIDO' ? 'Emitido' : 'Recibido'` de las dos funciones de export. - Filtros "Todos / Emitidos / Recibidos" cambiaron de filtrar por la columna `type` a filtrar por RFC del contribuyente. La razón: con multi-contribuyente por tenant el `type` puede ser inconsistente cuando dos contribuyentes del mismo tenant se facturan entre sí. RFC en posición emisor/receptor es fuente de verdad. **Backend** (`apps/api/src/services/cfdi.service.ts`): ```ts if (filters.tipo === 'EMITIDO') { whereClause += ` AND rfc_emisor = (SELECT rfc FROM contribuyentes WHERE entidad_id = $${paramIndex++})`; params.push(filters.contribuyenteId); } else if (filters.tipo === 'RECIBIDO') { whereClause += ` AND rfc_receptor = (SELECT rfc FROM contribuyentes WHERE entidad_id = $${paramIndex++})`; params.push(filters.contribuyenteId); } ``` **Drill-downs actualizados** (mismo patrón en cada uno: removido `{ header: 'Tipo', key: 'type', width: 10 }` de `EXCEL_COLUMNS`, `Tipo` de thead, `{cfdi.type}` de tbody): - `apps/web/app/(dashboard)/drill-down/page.tsx` — drill-down genérico de métricas del dashboard. - `apps/web/app/(dashboard)/alertas/cancelaciones/page.tsx` - `apps/web/app/(dashboard)/alertas/cancelaciones-periodo-anterior/page.tsx` - `apps/web/app/(dashboard)/alertas/efectivo/page.tsx` - `apps/web/app/(dashboard)/alertas/tipo-relacion-sospechosa/page.tsx` (también removida la columna "Dirección" redundante). --- ## 2. Rebrand de planes despacho Detalle completo: `docs/plans/2026-04-26-rebrand-planes-despacho.md`. ### Resumen de cambios | Plan (codename) | Display | Anual MXN | RFCs | CFDIs/contrib. | Timbres/mes | Backup | Features extra | |---|---|---:|---:|---:|---:|---|---| | `mi_empresa` | Mi Empresa | $6,960 | 1 | 1M | 50 | No | — | | `mi_empresa_plus` | Mi Empresa + | $10,800 | 1 | 1M | 50 | No | API + Lolita IA | | `business_control` | Business Control ★ | $25,850 | 100 | 1M | 0 | Sí | API | | `business_cloud` | Enterprise (display) | $43,000 | 100 | 3M | 0 | Sí | API | ★ = "Más popular". `business_cloud` mantiene su codename interno por compat con suscripciones vigentes; solo cambia el `name` display. ### Archivos tocados hoy - `apps/api/src/controllers/subscription.controller.ts` — `VALID_PLANS` y `DESPACHO_ONLY_ANNUAL` extendidos con `mi_empresa` y `mi_empresa_plus`. - `apps/api/src/services/payment/subscription.service.ts` — type alias `Plan` extendido con los dos literales nuevos para que `subscribe`, `scheduleChange`, `initiateUpgrade` y `applyPendingChanges` los acepten. ### Trabajo de fondo previo (no en esta sesión pero relacionado) - `packages/shared/src/constants/despacho-plans.ts` — catálogo y helpers (`isDespachoPaidPlan`, `permiteOverage`, `despachoPlanTieneDualidad`). - `apps/api/prisma/schema.prisma` — enum `Plan` con `mi_empresa` y `mi_empresa_plus` (migración `20260426073942_add_mi_empresa_plan`). - `apps/web/app/(dashboard)/configuracion/planes-despacho/page.tsx` — UI con grid de 4 cards alineadas verticalmente (`flex flex-col` + `mt-auto`), botón "Contratar". --- ## 3. Overage despacho generalizado ### Antes `addon.service.ts` tenía `BUSINESS_CLOUD_INCLUDED_RFCS = 3` y la función `adjustBusinessCloudOverage` filtraba con `sub.plan !== 'business_cloud'`. Solo Enterprise generaba overage. ### Después - Constante renombrada `BUSINESS_CLOUD_INCLUDED_RFCS = 3` → `DESPACHO_INCLUDED_RFCS = 100`. - Función renombrada `adjustBusinessCloudOverage` → `adjustDespachoOverage`. - Filtro de plan ahora usa `permiteOverage(sub.plan)` (helper en `@horux/shared`) que retorna `true` para `business_control` y `business_cloud`. Mi Empresa / Mi Empresa+ tienen límite duro de 1 RFC y `permiteOverage` retorna false — no entran a overage. - Codename del catálogo `contribuyente_extra_business_cloud` se preserva por compat con `subscription_addons` existentes; solo cambia el `nombre` display a "Contribuyente adicional (RFC extra)". ### Archivos tocados - `apps/api/src/services/payment/addon.service.ts` — constante, función, plan check, comentarios. - `apps/api/src/controllers/contribuyente.controller.ts` — import + dos callsites (`create` y `deactivate`) + comentarios actualizados. - `apps/api/prisma/seed.ts` — nombre del addon catalogo a genérico. --- ## 4. Compensación IVA I/07 PPD ↔ E mismo mes Detalle completo: `docs/plans/2026-04-26-i07-ppd-compensacion.md` §8. ### Asimetría que motivó el fix Para `I PUE/07` la cadena anticipo + aplicación + E/07 cierra algebraicamente con la lógica existente (`SUM_REL_TRAS` + filtro `<> '07'` en NEG). Para `I PPD/07` la aplicación no aporta IVA en su mes (espera al P), pero si en el **mismo mes** existe una E con `tipoRelación ≠ 07` que la referencia, la E sí resta IVA en NEG y la I PPD nunca aportó nada que la neutralice. Resultado previo: IVA acreditable / causado de la E "perdido". ### Solución Mirror del `i07PpdComp` que ya aplicamos en gastos/ingresos G1: la I PPD/07 hereda como aporte el IVA de la E que la cancela (mismo lado, mismo mes/año, `tipoRelación ≠ 07`). Net I PPD + E = 0 dentro del mes. ### Archivos tocados - `apps/api/src/services/impuestos.service.ts`: - **Predicado nuevo** `IS_I_PPD_07`. - **Helpers nuevos** `SUM_E_REFERENCING_TRAS(esLadoE)` / `SUM_E_REFERENCING_RET(esLadoE)`. La I PPD/07 hereda IVA de TODAS las E que la referencien (mismo lado/mes), sin filtrar tipoRelación. - **Helper EXISTS** `HAS_E_REFERENCING_MISMO_MES(esLadoE)` agregado a `bucketCausadoAny` y `bucketAcreditableAny` para que las I PPD/07 relevantes entren al `WHERE` de los queries que usan estos buckets. - **Predicado EXISTS** `E_REFERENCIA_I_PPD_07_MISMO_MES(esLadoIAlias)` que se evalúa **desde la fila E**: detecta si una E referencia una I PPD/07 del mismo lado/mes. Permite distinguir E/07 que apuntan a anticipo I PUE puro (siguen excluidas del NEG, statu quo) de E/07 que apuntan a I PPD/07 (entran al NEG en el caso PPD). - **`bucketCausadoNeg` y `bucketAcreditableNeg` extendidos** con disyuntivo `OR E_REFERENCIA_I_PPD_07_MISMO_MES(...)` — sin esto, la compensación no ocurriría cuando la operación tiene solo una E/07 (lo fiscalmente correcto pero raro en práctica). - **Rama nueva** en los 4 signed exprs (`signedCausadoTras`, `signedCausadoRet`, `signedAcreditableTras`, `signedAcreditableRet`): ```sql WHEN ${esLado} AND ${IS_I_PPD_07} THEN ${SUM_E_REFERENCING_*(esLadoE)} ``` `esLadoE` = `ctx.esEmisor`/`ctx.esReceptor` con rewrite `replace(/\brfc_(emisor|receptor)\b/g, 'e.rfc_$1')`. Análogamente `esLadoIAlias` para alias `i` en `E_REFERENCIA_I_PPD_07_MISMO_MES`. ### Validación con caso real Husberto Ignacio Torres (RFC `TOAH680201RA2`), agosto 2025: - Anticipo `729109fc` I PUE: $148K, IVA $20,413.79. - Aplicación `5c874749` I PPD/07: $454K, IVA $62,620.69. - NC `7163da3b` E PUE/07: $148K, IVA $20,413.79 (cancela anticipo). - NC `7aac715b` E PUE/01: $10K, IVA $1,379.31 (sustitución). | Estado | Acreditable agosto 2025 | |---|---:| | Antes | $19,033.69 | | Después | $20,413.00 | **Delta: +$1,379.31** acreditable recuperado, exactamente la E/01 que restaba sin contraparte. La E/07 sigue sin afectar IVA (correcto). ### Cache `computeMetricaMensual` llama a `getResumenIva` que ya usa los signed exprs nuevos. Periodos cacheados con la lógica vieja quedan stale hasta que se recompute. --- ## 5. Refactor IVA — fórmula del owner Detalle completo: `docs/plans/2026-04-26-iva-refactor.md`. Cambio mayor en `/impuestos`. El owner pidió alinear el cálculo a un spec explícito que difiere del código previo en tres puntos: 1. **Sin clamp del IVA en P**: campos `iva_traslado_pago_mxn` / `iva_retencion_pago_mxn` se usan directos. Antes: `LEAST(iva, monto × 0.16)`. 2. **Sin compensación I PUE/07**: las I PUE/07 aportan IVA completo. La E/07 (si se emite) resta normalmente vía bucket NEG. Antes había `GREATEST(0, IVA − Σ IVA anticipos referenciados)`. 3. **E con tipoRel=07 entra al NEG**: ya no se filtran las E/07 del bucket NEG. Antes el filtro `<> '07'` las excluía (excepto las que apuntaban a I PPD/07 vía un disyuntivo EXISTS). ### Conservados - Rama I PPD/07 con `SUM_E_REFERENCING_TRAS/RET` (hereda IVA de E del mismo mes que la cancelan). - Estructura de tres KPIs separados: Trasladado / Acreditable / Retenido con fórmula `Resultado = T − A − R`. - Exclusiones por clave_prod_serv (`84121603`, `93161608`, `85101501`, `85121800`). - Filtro de régimen por lado del contribuyente (emisor cuando vende, receptor cuando compra). ### Eliminados del código - `IS_I_PUE_07` - `SUM_REL_TRAS`, `SUM_REL_RET` - `E_REFERENCIA_I_PPD_07_MISMO_MES` ### Validación con Husberto agosto 2025 | KPI | Antes | Después | Delta | |---|---:|---:|---:| | Trasladado | $119,093.08 | $111,781.45 | −$7,311.63 | | Acreditable | $147,023.59 | $182,683.84 | +$35,660.25 | | Resultado IVA | −$27,930.51 | −$70,902.39 | **−$42,971.88** | Delta favorable al contribuyente (más acreditable, menos a pagar). ### Riesgos aceptados - Sin compensación I PUE/07 + E/07 ausente → sobrecausa el IVA del anticipo. En Husberto agosto: 11 I PUE/07 con 0 E/07 emitidas. El owner aceptó bajo la premisa "fiscalmente el contador debe emitir la E/07". - Sin clamp P → vulnerables a XMLs de proveedores que reportan IVA total en pagos parciales. --- ## 6. Notificaciones email automáticas (alertas + recordatorios) Detalle completo: `docs/plans/2026-04-26-notifications-email.md`. Cron diario **8:30 AM America/Mexico_City** que cierra el pendiente histórico de "emails automáticos para alertas/recordatorios" (estaba en CLAUDE.md "Problemas conocidos"). Modelo elegido por el owner: **Option B — por evento** (una notificación cuando algo se activa, no digest diario). ### Comportamiento - **Alertas**: por cada contribuyente activo, llama `generarAlertasAutomaticas`. Las que aparecen por primera vez se insertan en `alertas_notificadas` (BD tenant) y disparan email batched al supervisor + auxiliares + clientes del contribuyente. Una alerta solo se notifica **una vez en la vida** (MVP). Las que dejan de aparecer se marcan `resuelta_at` (informativo, no email). - **Recordatorios**: 3 ventanas (3 días antes, 1 día antes, mismo día). Cada ventana se envía a lo más una vez vía columnas `email_3d_at / email_1d_at / email_0d_at` en `recordatorios`. Recipientes: clientes + auxiliares; si no hay auxiliares, también supervisores; si owner es supervisor sin auxiliares, también owner. ### Archivos - Migraciones tenant 039 (alertas_notificadas) y 040 (columnas en recordatorios) - 2 templates email: `alertas-nuevas.ts`, `recordatorio-proximo.ts` - `services/notifications.service.ts`: resolución destinatarios + procesamiento - `jobs/notifications.job.ts`: cron `30 8 * * *` con disparo manual exportado - `email.service.ts`: helpers `sendAlertasNuevas` + `sendRecordatorioProximo` - `index.ts`: wire del cron solo si `NODE_ENV === 'production'` ### Operación - En **dev** el cron NO arranca automáticamente (evita spam con datos de prueba). Disparo manual: `runNotificationsForTenant(tenantId)`. - Migraciones aplicadas a los 3 tenants existentes (Patito, Zorro, mo3nhzvl). - Sin SMTP configurado los emails se loguean a consola (transport detecta `SMTP_USER` vacío). **Pendiente real**: configurar SMTP en `.env` para prod. --- ## 7. Pendientes **Resoluciones de hoy** (cerradas): - ✓ Drill-down dashboard también limpiado de columna "Tipo". - ✓ Compensación IVA aplicada solo al caso PPD (PUE no necesita — evaluado y descartado, ver §8 del doc i07-ppd). - ✓ Versión inicial filtraba `<> '07'`; refinada para distinguir por destino de la E (apunta a I PPD/07 vs apunta a anticipo I PUE puro). Ahora cubre el caso fiscalmente correcto donde solo existe E/07. - ✓ Refactor IVA al spec explícito del owner: removida compensación I PUE/07, removido clamp en P, todas las E PUE entran al NEG. Caso Husberto agosto 2025 valida $111K trasladado / $182K acreditable. - ✓ **Notificaciones email automáticas de alertas/recordatorios** (CLAUDE.md "Problemas conocidos"): cron diario 8:30 AM con detección de alertas nuevas + 3 ventanas de recordatorio (3d/1d/0d). Modelo Option B (por evento). Detalle en `2026-04-26-notifications-email.md`. - ✓ **#2 Convertir `/pendientes` → "Despacho"** (verificado, ya estaba hecho de sesión previa: módulo `/despachos` con sub-nav). - ✓ **Recrear org Facturapi de Carlos** (verificado, ya estaba hecho: TORC9611214CA tiene `facturapi_org_id` y `csd_uploaded=true`). - ✓ **Sprint 1 — pre-deploy IVA**: validados otros tenants (72-100% de I PUE/07 con E contraparte), borrado bulk de `metricas_mensuales` (353 filas en años < 2026), `dashboard.service.ts` alineado con la fórmula nueva (s4/r4 nuevos, sin clamp P, sin compensación I PUE/07, sin filtro `<> '07'`). Detalle en `2026-04-26-sprints-1-2-3.md`. - ✓ **Sprint 2 — bugs latentes**: overage al cambiar de plan (`reconcileOverageAfterPlanChange` en upgrade/scheduled/cancel), `getMyPlan` lee `tenant.plan` directamente. "Completadas > Pendientes" no es bug. Visibilidad auxiliares: investigado, sin reproducción. - ✓ **Sprint 3 — decisiones owner D1-D7**: Mi Empresa(+) con billing dual (mensual default + anual con −17% / 10 meses); re-notificación alertas tras 30 días de resuelta. Resto confirmado o sin cambio. - ✓ **Sprint 6 — investigación SAT**: logging `codeRequest` verificado activo (`sat-client.service.ts:116-184`), listo para diagnosticar rejections futuras. Manuel NO necesitaba re-sync (242 CFDIs completos); el bug real era de Alexa con record stale del 2026-04-21 — reconciliado a `completed`. Detalle en `2026-04-26-sprints-1-2-3.md`. - ✓ **Bootstrap admin global del fork**: ejecutado `pnpm bootstrap:admin-global` con `HORUX_ADMIN_EMAIL=carlos@horuxfin.com` + `HORUX_TI_EMAIL=ivan@horuxfin.com`. Crea tenant `Horux 360` (`HTS240708LJA`) y asigna `platform_admin`/`platform_ti`. Contraseña fijada manualmente a `Admin12345!` (bcrypt cost 12). **Pendientes derivados de hoy:** - Configurar **SMTP en `.env`** de producción para que el cron de notificaciones envíe correos reales (sin esto se loguean a consola). - **Alerta automática "P con IVA > 16% del pago"** (follow-up D5) — detectar XMLs malformados sin reintroducir clamp global. - **Reportar bug 2.4 con detalle** si reaparece visibilidad de auxiliares en carteras: capturar pantalla + rol + URL exacta.