12 KiB
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, alineardashboard.service.ts. - Sprint 2: bugs latentes (overage al cambiar de plan,
getMyPlanpara 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 clampLEAST(...), usa campos directos.s1/r1(I PUE emisor/receptor): removida la rama de compensación conSUM_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 delSUM_E_REFERENCING_*deimpuestos.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 localcountActiveContribuyentesque vivía encontribuyente.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).
- Nuevo helper
subscription.service.ts:- Nuevo helper privado
reconcileOverageAfterPlanChange(tenantId, fromPlan, toPlan)— fail-soft. Si el plan target permite overage, llamaadjustDespachoOverage. Si no, llamacancelOverageAddonForTenant. applyApprovedUpgrade: invoca el reconcile tras el$transaction.applyPendingChanges: invoca el reconcile dentro del loop.cancelSubscription: invocacancelOverageAddonForTenantantes de marcar status='cancelled' (porque el lookup necesita la sub activa).
- Nuevo helper privado
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 Auxiliarconauxiliar_user_idapuntando 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:getPlanPriceusapermiteFrecuenciaMensualgetPrecioDespacho.
subscription.controller.ts:DESPACHO_ONLY_ANNUALahora solo contienebusiness_controlybusiness_cloud.
UI (/configuracion/planes-despacho):
- Nuevo
FrequencyTogglecon 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.
handleContrataryhandleCambiarusanfrequencyFor(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 Exhausted5003 MaximumLimit5004 EmptyResult5005 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_URLHTTPS 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
codeRequestreal en producción cuando ocurra una rejection SAT (logging ya activo). - Prueba cross-contribuyente end-to-end (manual del owner).
- Capturar el primer
- 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.