17 KiB
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
- Limpieza columna "Tipo" en CFDI y drill-downs
- Rebrand de planes despacho
- Overage despacho generalizado
- Compensación IVA I/07 PPD ↔ E mismo mes
- Refactor IVA — fórmula del owner
- Notificaciones email automáticas (alertas + recordatorios)
- 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
typea filtrar por RFC del contribuyente. La razón: con multi-contribuyente por tenant eltypepuede 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):
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.tsxapps/web/app/(dashboard)/alertas/cancelaciones-periodo-anterior/page.tsxapps/web/app/(dashboard)/alertas/efectivo/page.tsxapps/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_PLANSyDESPACHO_ONLY_ANNUALextendidos conmi_empresaymi_empresa_plus.apps/api/src/services/payment/subscription.service.ts— type aliasPlanextendido con los dos literales nuevos para quesubscribe,scheduleChange,initiateUpgradeyapplyPendingChangeslos 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— enumPlanconmi_empresaymi_empresa_plus(migración20260426073942_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 retornatrueparabusiness_controlybusiness_cloud. Mi Empresa / Mi Empresa+ tienen límite duro de 1 RFC ypermiteOverageretorna false — no entran a overage. - Codename del catálogo
contribuyente_extra_business_cloudse preserva por compat consubscription_addonsexistentes; solo cambia elnombredisplay 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 (createydeactivate) + 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 abucketCausadoAnyybucketAcreditableAnypara que las I PPD/07 relevantes entren alWHEREde 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). bucketCausadoNegybucketAcreditableNegextendidos con disyuntivoOR 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):WHEN ${esLado} AND ${IS_I_PPD_07} THEN ${SUM_E_REFERENCING_*(esLadoE)}esLadoE=ctx.esEmisor/ctx.esReceptorcon rewritereplace(/\brfc_(emisor|receptor)\b/g, 'e.rfc_$1'). AnálogamenteesLadoIAliaspara aliasienE_REFERENCIA_I_PPD_07_MISMO_MES.
- Predicado nuevo
Validación con caso real
Husberto Ignacio Torres (RFC TOAH680201RA2), agosto 2025:
- Anticipo
729109fcI PUE: $148K, IVA $20,413.79. - Aplicación
5c874749I PPD/07: $454K, IVA $62,620.69. - NC
7163da3bE PUE/07: $148K, IVA $20,413.79 (cancela anticipo). - NC
7aac715bE 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:
- Sin clamp del IVA en P: campos
iva_traslado_pago_mxn/iva_retencion_pago_mxnse usan directos. Antes:LEAST(iva, monto × 0.16). - 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). - 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_07SUM_REL_TRAS,SUM_REL_RETE_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 enalertas_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 marcanresuelta_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_atenrecordatorios. 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 + procesamientojobs/notifications.job.ts: cron30 8 * * *con disparo manual exportadoemail.service.ts: helperssendAlertasNuevas+sendRecordatorioProximoindex.ts: wire del cron solo siNODE_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_USERvacío). Pendiente real: configurar SMTP en.envpara 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/despachoscon sub-nav). - ✓ Recrear org Facturapi de Carlos (verificado, ya estaba hecho:
TORC9611214CA tiene
facturapi_org_idycsd_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.tsalineado con la fórmula nueva (s4/r4 nuevos, sin clamp P, sin compensación I PUE/07, sin filtro<> '07'). Detalle en2026-04-26-sprints-1-2-3.md. - ✓ Sprint 2 — bugs latentes: overage al cambiar de plan
(
reconcileOverageAfterPlanChangeen upgrade/scheduled/cancel),getMyPlanleetenant.plandirectamente. "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
codeRequestverificado 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 acompleted. Detalle en2026-04-26-sprints-1-2-3.md. - ✓ Bootstrap admin global del fork: ejecutado
pnpm bootstrap:admin-globalconHORUX_ADMIN_EMAIL=carlos@horuxfin.comHORUX_TI_EMAIL=ivan@horuxfin.com. Crea tenantHorux 360(HTS240708LJA) y asignaplatform_admin/platform_ti. Contraseña fijada manualmente aAdmin12345!(bcrypt cost 12).
Pendientes derivados de hoy:
- Configurar SMTP en
.envde 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.