# 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: ```sql 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: ```ts 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`. ```ts 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`: ```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`: ```sql 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=() codeRequest=() — wrapperCode= wrapperMsg="" ``` 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: ```sql 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.