Files
HoruxDespachos/docs/plans/2026-04-26-session.md
2026-04-27 22:09:36 -06:00

17 KiB
Raw Permalink Blame History

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
  2. Rebrand de planes despacho
  3. Overage despacho generalizado
  4. Compensación IVA I/07 PPD ↔ E mismo mes
  5. Refactor IVA — fórmula del owner
  6. Notificaciones email automáticas (alertas + recordatorios)
  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):

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 API
business_cloud Enterprise (display) $43,000 100 3M 0 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.tsVALID_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 = 3DESPACHO_INCLUDED_RFCS = 100.
  • Función renombrada adjustBusinessCloudOverageadjustDespachoOverage.
  • 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):
      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.