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

375 lines
17 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.