375 lines
17 KiB
Markdown
375 lines
17 KiB
Markdown
# 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.
|