# 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 `<th>Tipo</th>` y la celda `<td>` 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`,
`<th>Tipo</th>` de thead, `<td>{cfdi.type}</td>` 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.
