Files
HoruxDespachos/docs/plans/2026-04-26-sprints-1-2-3.md
2026-04-27 22:09:36 -06:00

12 KiB
Raw Blame History

Sprints 1, 2, 3 y 6 — cierre del día (2026-04-26)

Cuatro sprints encadenados después del refactor IVA y del wire de notificaciones email:

  • Sprint 1: pre-deploy del refactor IVA. Validar otros tenants, recompute de metricas_mensuales, alinear dashboard.service.ts.
  • Sprint 2: bugs latentes (overage al cambiar de plan, getMyPlan para Mi Empresa, métricas de Despacho, visibilidad auxiliares).
  • Sprint 3: decisiones del owner (D1-D7) — billing dual, re-notif alertas, etc.
  • Sprint 6: investigación SAT — verificar logging codeRequest, reconciliar record stale de Alexa.

Sprint 1 — Pre-deploy refactor IVA

1.1 Validar otros tenants

Inventario por contribuyente con I/07 PUE/PPD/E:

Tenant Contribuyente Lado I PUE/07 con E (cualquier tipoRel) I PPD/07 con E
Patito TOAH (Husberto) Receptor 356 257 (72%) 21/21
Patito TOAH (Husberto) Emisor 6 6 (100%)
Patito TORC (Carlos) Receptor 8 6 (75%) 22/22
Zorro (sin volumen) 0 0 0

Hallazgo crítico ajustado: la primera medida (filtro estricto a E con tipoRel=07 apuntando al anticipo) sugería sobrecausa masiva. La medida correcta (cualquier E PUE que cancele en el mismo mes, incluyendo E/01 sustitución) muestra que 72-100% sí tienen contraparte.

Decisión del owner: para los huérfanos, fidelidad al XML > interpretación:

"Si no existe la tipo E, lo correcto es mostrar los datos tal cual viene la información, ya que si hacemos cualquier modificación, podemos llegar a una discrepancia."

Esto cierra D4: confirmar la fórmula nueva sin compensación I PUE/07 ni clamp en P.

1.2 Recompute bulk de metricas_mensuales

Borrado de cache de años cerrados (< 2026) en los 3 tenants:

DELETE FROM metricas_mensuales WHERE anio < 2026;
Tenant Filas borradas Filas restantes (2026 actual)
Patito 250 0
Zorro 103 6
mo3nhzvl 0 0

Estrategia lazy repopulation: la próxima query de un usuario sobre un mes pasado dispara el path on-the-fly con la fórmula nueva, y el cron metricas-invalidations.job.ts repuebla en background.

1.3 Alinear dashboard.service.ts con la fórmula nueva

calcularIvaBalancePorRegimen tenía la lógica vieja inline (clamp P, compensación I PUE/07, filtro <> '07' en s3/r3). Cambios:

  • IVA_NETO_PAGO: removido clamp LEAST(...), usa campos directos.
  • s1/r1 (I PUE emisor/receptor): removida la rama de compensación con SUM_REL_TRAS. Ahora aportan IVA neto completo.
  • s3/r3 (E PUE): removido filtro <> '07'. Todas las E PUE entran al NEG.
  • Nuevos s4/r4: I PPD/07 hereda IVA neto de E que la cancele en mismo mes. Mirror del SUM_E_REFERENCING_* de impuestos.service.ts.

Suma final actualizada:

causado     = s1 + s2 + s4 - r3
acreditable = r1 + r2 + r4 - s3
balance     = causado - acreditable

Validación: dashboard coincide centavo a centavo con /impuestos para Husberto agosto 2025: T=$111,781.45, A=$182,683.84, balance=$70,902.39.


Sprint 2 — Bugs latentes

2.1 Recomputar overage al cambiar de plan

adjustDespachoOverage no se invocaba desde el flujo de cambios de plan. Si un tenant pasaba de Enterprise → Business Control (o Mi Empresa→Business Control), el addon de overage quedaba huérfano: cobros incorrectos en MP.

Cambios:

  • addon.service.ts:
    • Nuevo helper countActiveContribuyentesForTenant(tenantId) — abre pool tenant y cuenta CONTRIBUYENTEs activos. Reemplaza el helper local countActiveContribuyentes que vivía en contribuyente.controller.ts.
    • Nuevo helper cancelOverageAddonForTenant(tenantId) — cancela el preapproval MP + setea status='cancelled'. Idempotente. No requiere que la subscripción esté activa (útil al cancelarla).
  • subscription.service.ts:
    • Nuevo helper privado reconcileOverageAfterPlanChange(tenantId, fromPlan, toPlan) — fail-soft. Si el plan target permite overage, llama adjustDespachoOverage. Si no, llama cancelOverageAddonForTenant.
    • applyApprovedUpgrade: invoca el reconcile tras el $transaction.
    • applyPendingChanges: invoca el reconcile dentro del loop.
    • cancelSubscription: invoca cancelOverageAddonForTenant antes de marcar status='cancelled' (porque el lookup necesita la sub activa).

2.2 getMyPlan para Mi Empresa

Mapping anterior usaba dbMode como proxy: BYO → business_control, MANAGED → business_cloud. Para Mi Empresa y Mi Empresa+ (también MANAGED) reportaba business_cloud por error.

Fix: leer tenant.plan directamente. Soporta los 4 planes despacho. Trial sigue detectándose por trialEndsAt.

let currentPlan: string;
if (isTrialActive) {
  currentPlan = 'trial';
} else {
  currentPlan = String(tenant.plan);
}

2.3 "Completadas > Pendientes" — NO es bug

Verificación contra datos reales:

Contribuyente Obl. pendientes Obl. completadas
Horux 360 1 2
Husberto 2 3

progresoDelMes = completadas / (pendientes + completadas) está bien. "Completadas > Pendientes" es señal positiva (despacho al día), no error de cálculo. Cerrado.

2.4 Visibilidad auxiliares en carteras — sin reproducción

Datos actuales en Patito:

  • Cartera Demo (top-level, sin auxiliar)
  • Subcartera Demo Auxiliar con auxiliar_user_id apuntando al auxiliar.

El query del controller (WHERE c.auxiliar_user_id = $1) trae la subcartera correctamente. Sin reproducción específica del bug original (quién, qué pantalla, qué se ve vs qué se espera), cerrado como "investigado, pendiente reporte específico".


Sprint 3 — Decisiones del owner (D1-D7)

Resumen de decisiones

# Decisión Estado
D1 Mi Empresa y Mi Empresa+ con billing dual: mensual default, anual = 10 meses (descuento 17%) ✓ Implementado
D2 Mi Empresa(+) sin overage de RFCs ✓ Ya estaba
D3 Enterprise sin timbres incluidos ✓ Ya estaba
D4 Confirmar fidelidad al XML (sin compensación I PUE/07, sin clamp P) ✓ Confirmado tras Sprint 1
D5 Mantener sin clamp en IVA de P ✓ Ya estaba
D6 Sin email de "alerta resuelta" ✓ Ya estaba
D7 Re-notificación tras 30 días de resuelta ✓ Implementado

D1 — Billing dual Mi Empresa(+)

Catálogo nuevo en packages/shared/src/constants/despacho-plans.ts:

export const DESPACHO_PLAN_PRICES = {
  mi_empresa:       { monthly: 580,  firstYear: 5_800,  renewal: 5_800,  permiteMonthly: true  },
  mi_empresa_plus:  { monthly: 900,  firstYear: 9_000,  renewal: 9_000,  permiteMonthly: true  },
  business_control: { monthly: null, firstYear: 25_850, renewal: 25_850, permiteMonthly: false },
  business_cloud:   { monthly: null, firstYear: 43_000, renewal: 43_000, permiteMonthly: false },
};

Helpers nuevos:

  • getPrecioDespacho(plan, frequency, phase) — resuelve precio según frequency. Throws si el plan no permite la frecuencia.
  • permiteFrecuenciaMensual(plan) — flag.

Backend:

  • subscription.service.ts:getPlanPrice usa permiteFrecuenciaMensual
    • getPrecioDespacho.
  • subscription.controller.ts: DESPACHO_ONLY_ANNUAL ahora solo contiene business_control y business_cloud.

UI (/configuracion/planes-despacho):

  • Nuevo FrequencyToggle con dos pestañas (Mensual / Anual 17%) inline en cada Card de Mi Empresa y Mi Empresa+. Toggle per-plan, default mensual.
  • Precio dinámico:
    • Mensual: $580 — "o $5,800/año (ahorras 17%)" como CTA al anual.
    • Anual: $5,800 — "Pagas 10 meses en lugar de 12" en verde.
  • handleContratar y handleCambiar usan frequencyFor(plan) para derivar la frecuencia del toggle.

D7 — Re-notificación tras 30 días

notifications.service.ts:processAlertasContribuyente — antes del INSERT de detección de alertas nuevas, borra registros con resuelta_at < NOW() - 30 days:

DELETE FROM alertas_notificadas
WHERE contribuyente_id = $1::uuid
  AND resuelta_at IS NOT NULL
  AND resuelta_at < NOW() - INTERVAL '30 days'

Si una alerta vuelve a aparecer después de >30 días resuelta, el INSERT posterior la detecta como "nueva" y vuelve a notificar al equipo.


Archivos modificados

packages/shared/src/constants/despacho-plans.ts          [~] D1 catálogo + helpers
apps/api/src/services/dashboard.service.ts               [~] Sprint 1.3 IVA balance
apps/api/src/services/payment/addon.service.ts           [~] Sprint 2.1 helpers
apps/api/src/services/payment/subscription.service.ts    [~] Sprint 2.1 reconcile + D1 getPlanPrice
apps/api/src/controllers/subscription.controller.ts      [~] D1 DESPACHO_ONLY_ANNUAL
apps/api/src/controllers/despacho.controller.ts          [~] Sprint 2.2 getMyPlan
apps/api/src/services/notifications.service.ts           [~] D7 DELETE 30d
apps/web/app/(dashboard)/configuracion/planes-despacho/page.tsx  [~] D1 UI toggle

Migraciones: ninguna nueva (las tablas/columnas necesarias se crearon en sesiones previas).

Cache borrada: metricas_mensuales WHERE anio < 2026 en los 3 tenants (353 filas en total).



Sprint 6 — Investigación SAT

6.1 Verificar logging de codeRequest

apps/api/src/services/sat/sat-client.service.ts líneas 116-184: el código expone codeRequestValue/Entry/Message en cada verify() vía getCodeRequest() de la lib @nodecfdi/sat-ws-descarga-masiva.

Cuando el SAT rechaza una solicitud, los 3 valores se loguean en [SAT Verify Debug] (consola) y se incluyen en error.message del VerifyResult cuando status es rejected o failed. Formato:

SAT request=<entry>(<value>) codeRequest=<entry>(<value>) — <message>
wrapperCode=<status> wrapperMsg="<msg>"

Permite diagnosticar los 5 códigos SAT documentados:

  • 5000 Accepted (happy path)
  • 5002 Exhausted
  • 5003 MaximumLimit
  • 5004 EmptyResult
  • 5005 Duplicated (la hipótesis principal de Manuel pre-2026-04-23)

Logging activo y listo. Solo falta capturar el primer caso real en producción para confirmar la hipótesis 5005 y decidir mitigación (esperar 72h, reducir rangos, cambiar FIEL, etc.).

6.2 Re-sync de Manuel — reformulado

El plan original era re-sync custom de bloques 3-9 de Manuel. Verificación sobre datos reales:

Contribuyente Tenant Job initial CFDIs en BD
Manuel (GADM9107165I0) Zorro ✓ completed 242, distribuidos consistentemente
Alexa (TORA0007099R6) Zorro failed (stale) 415 descargados

Manuel NO necesita re-sync — su initial está completed con 242 CFDIs. Los "bloques 3-9" mencionados en la sesión 2026-04-21 corresponden a sub-fallos internos del job que terminó completed en su totalidad.

El bug real era de Alexa: 415 CFDIs descargados pero record marcado failed por el bug stale del 2026-04-21 (cleanup manual de la sesión MP). Reconciliado con UPDATE directo:

UPDATE sat_sync_jobs
SET status='completed',
    error_message='Reconciliado 2026-04-26: el initial completo pero el
                   status quedo stale por el bug del 2026-04-21.',
    cfdis_inserted=415,
    completed_at=COALESCE(completed_at, NOW())
WHERE id='830bac32-1bfb-44cb-ab47-333eac840f81' AND status='failed';

La pendiente original apuntaba al contribuyente equivocado. Reformularla y cerrarla con fix de datos en lugar de re-sync.


Pendientes derivados

  • Sprint 4: Cloudflare Tunnel + FRONTEND_URL HTTPS dev + typecheck web cleanup. Decisión del owner: dejarlos al final.
  • Sprint 5: Nómina (tipo N) y Carta Porte. Priorizar según demanda real.
  • Sprint 6 abierto:
    • Capturar el primer codeRequest real en producción cuando ocurra una rejection SAT (logging ya activo).
    • Prueba cross-contribuyente end-to-end (manual del owner).
  • Mejora futura: alerta automática "P con IVA > 16% del pago" — follow-up de D5 para detectar XMLs malformados sin reintroducir clamp global.
  • Reportar bug 2.4 con detalle: si reaparece visibilidad de auxiliares en carteras, capturar pantalla + rol del usuario + URL exacta.