321 lines
12 KiB
Markdown
321 lines
12 KiB
Markdown
# 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=<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:
|
||
|
||
```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.
|