298 lines
15 KiB
Markdown
298 lines
15 KiB
Markdown
# Sesión 2026-04-23 — Cierre de features pending + derivados técnicos
|
|
|
|
Sesión extensa que cerró **4 features pending** (#6 #7 #9 + investigación #10)
|
|
y **3 derivados técnicos** (#3 watchdog cron, #4 validación CSD, #5 logging SAT),
|
|
además de 7 bugfixes descubiertos en el proceso.
|
|
|
|
## 1. Feature #6 — Enlazar obligaciones ↔ declaraciones (trazabilidad)
|
|
|
|
El backend del matching impuesto→obligación ya existía (`completarObligacionesPorDeclaracion`
|
|
en `declaraciones.service.ts` hacía el UPSERT en `obligacion_periodos`). Faltaba la
|
|
trazabilidad reversa: poder clickear una obligación completada y ver la declaración
|
|
que la cubrió.
|
|
|
|
**Migration 030** `obligacion_periodos.declaracion_id INT REFERENCES
|
|
declaraciones_provisionales(id) ON DELETE SET NULL` + índice parcial.
|
|
|
|
**Backend:**
|
|
- `completarObligacionesPorDeclaracion` ahora acepta `declaracionId` y lo persiste
|
|
en el INSERT/UPDATE del `ON CONFLICT`.
|
|
- `createDeclaracion` pasa `declaracion.id` tras el INSERT.
|
|
- `getObligacionesPorPeriodo` hace `LEFT JOIN declaraciones_provisionales` y devuelve
|
|
un objeto `declaracion: DeclaracionLink | null` por cada periodo completado
|
|
(nuevo tipo exportado).
|
|
|
|
**Frontend (`/pendientes/page.tsx`):**
|
|
- `ObligacionPeriodo` extendido con `declaracion: DeclaracionLink | null`.
|
|
- Link `↗ Declaración MM/YYYY [Compl.]` junto a obligaciones completadas con FK.
|
|
Click abre PDF en nueva pestaña vía `/documentos/declaraciones/:id/pdf/declaracion`.
|
|
|
|
**Semántica:** `ON DELETE SET NULL` — el periodo sigue marcado completado si la
|
|
declaración se borra, pero pierde la referencia. El usuario decide si reabrirlo.
|
|
|
|
---
|
|
|
|
## 2. Feature #7 — Calendario con colores por status
|
|
|
|
Backend + frontend **ya estaban implementados** end-to-end (tipos
|
|
`obligacion-pendiente|completada|atrasada` generados desde `obligacion_periodos`,
|
|
colores amber/green/red mapeados). Solo agregué 2 mejoras UX:
|
|
|
|
- **`AlertTriangle` icon** para `obligacion-atrasada` (antes usaba el mismo
|
|
`Clock` que pendiente).
|
|
- **Leyenda de colores** visible en el header del grid del calendario:
|
|
amber/green/red + violet (recordatorio custom).
|
|
|
|
---
|
|
|
|
## 3. Feature #10 — Alertas obligaciones per-contribuyente (investigado)
|
|
|
|
El bug reportado "las alertas manuales muestran más obligaciones de las que
|
|
tiene el contribuyente" **no se reproduce** al 2026-04-23.
|
|
|
|
Auditoría SQL sobre los despachos activos:
|
|
- 0 alertas `ob-*` con obligacion_id inexistente (huérfanas)
|
|
- 0 alertas para obligaciones `activa=false`
|
|
- 0 alertas para periodos ya completados
|
|
- Count per-contribuyente: alertas ≤ obligaciones activas en todos los casos
|
|
|
|
Protecciones verificadas en código:
|
|
1. `removeObligacion` hace soft-delete + DELETE alertas + DELETE periodos
|
|
2. `inactiveFilter` en `getAlertasManualesPendientes` excluye por `activa=false`
|
|
3. `contribuyenteFilter` strict previene leak cross-contribuyente
|
|
4. `sincronizarDesdeObligacionesContribuyente` solo genera current + previous month
|
|
|
|
Probablemente fue arreglado implícitamente al agregar el cleanup en
|
|
`removeObligacion` en una sesión anterior. Si reaparece el síntoma, hay que
|
|
capturar el drift específico antes de tocar código.
|
|
|
|
---
|
|
|
|
## 4. Feature #9 — Emails al subir declaración / documento extra
|
|
|
|
**Destinatarios:** owners activos del despacho + supervisor del contribuyente
|
|
(`entidades_gestionadas.supervisor_user_id`), excluyendo al uploader mismo.
|
|
|
|
**Archivos:**
|
|
- `services/email/templates/documento-subido.ts` — **nuevo**, template
|
|
parametrizable por `kind: 'declaracion' | 'extra'`, con bloques condicionales
|
|
para periodo/impuestos/monto (declaración) o nombre/categoría/descripción
|
|
(extra). HTML escapado para evitar XSS.
|
|
- `services/email/email.service.ts:sendDocumentoSubido(recipients, data)` —
|
|
loop por recipient con try/catch individual.
|
|
- `utils/memberships.ts`:
|
|
- `getTenantOwnerEmails(tenantId)` — **nuevo**, lista todos los owners.
|
|
- `getUserEmailById(userId)` — **nuevo**, resolver supervisor.
|
|
- `services/notify-upload.service.ts:notifyDocumentoSubido(...)` — **nuevo**
|
|
orquestador: resuelve recipients, dedupea, excluye uploader, envía.
|
|
- `controllers/documentos.controller.ts`:
|
|
- `crearDeclaracion` y `crearExtra` disparan notify con `.catch()` (fire-and-forget).
|
|
|
|
**Dev mode:** sin SMTP configurado, transport de `@horux/core` loguea a consola.
|
|
|
|
---
|
|
|
|
## 5. Bugfixes descubiertos en el camino
|
|
|
|
### 5.1 `feature-gate.middleware.ts` crash con planes despacho
|
|
`hasFeature(plan)` asumía `PLANS[plan]` siempre existe. Con planes `business_cloud`/
|
|
`business_control` (que viven en `DESPACHO_PLANS`, no `PLANS`) → `undefined.features`
|
|
→ crash del API. Fix en 2 capas:
|
|
- Middleware ahora detecta plan despacho y rutea a `hasDespachoFeature`.
|
|
- `shared/constants/plans.ts:hasFeature` defensive con `?.` + `?? false`.
|
|
|
|
### 5.2 Declaraciones sin filtro por contribuyente
|
|
`declaraciones_provisionales` no tenía columna `contribuyente_id`; todas las
|
|
declaraciones se mezclaban entre RFCs de un despacho.
|
|
|
|
**Migration 031** agrega `contribuyente_id UUID NULL` con `ON DELETE SET NULL`.
|
|
Reemplaza el UNIQUE `(año, mes) WHERE tipo='normal'` por
|
|
`(año, mes, contribuyente_id) WHERE tipo='normal'` para que cada RFC tenga su
|
|
propia normal mensual.
|
|
|
|
Backend + controller + API client + hook + UI actualizados para pasar
|
|
`contribuyenteId` end-to-end. Declaraciones legacy quedan con NULL
|
|
(interpretadas como "tenant-wide / legacy") — invisibles cuando se filtra por
|
|
un RFC específico.
|
|
|
|
### 5.3 `completada_por` UUID/email mismatch
|
|
`obligacion_periodos.completada_por` es UUID, pero `createDeclaracion` pasaba
|
|
el email del usuario. Crash silencioso con `invalid input syntax for type uuid:
|
|
"jd@demo.com"`.
|
|
|
|
Separé en 2 campos:
|
|
- `creadoPor: string` (email) — va a `declaraciones_provisionales.creado_por VARCHAR`.
|
|
- `creadoPorUserId: string` (UUID) — va a `obligacion_periodos.completada_por UUID`.
|
|
|
|
### 5.4 Alertas de pago desaparecían al subir declaración normal con monto>0
|
|
La llamada a `completarObligacionesPorDeclaracion` se hacía SIEMPRE que hubiera
|
|
`contribuyenteId`, incluyendo declaraciones normales cuyo pago aún está pendiente.
|
|
Esto cerraba tanto alertas `decl-*` como `ob-*` (per-obligación), dejando al
|
|
usuario sin visibilidad del pago pendiente.
|
|
|
|
**Fix:** `completarObligacionesPorDeclaracion` solo se llama si la declaración
|
|
cubre el pago (`tipo='complementaria' || montoPago=0`). Además,
|
|
`uploadComprobantePago` ahora también llama `completarObligacionesPorDeclaracion`
|
|
para cerrar las obligaciones cuando se sube el comprobante de pago.
|
|
|
|
### 5.5 Keyword matching IVA → DIOT y mensual → anual
|
|
Subir una declaración con `impuestos: ['IVA']` cerraba también obligaciones
|
|
"Declaración de proveedores de IVA" (DIOT) y "Declaración anual de ISR" porque
|
|
la sustring "iva"/"isr" matchaba demasiado amplio.
|
|
|
|
**Fix doble:**
|
|
1. `IMPUESTO_A_OBLIGACION_KEYWORDS` ahora tiene `include + exclude`:
|
|
- `IVA: include=['iva'], exclude=['diot','proveedores de iva','informativa']`
|
|
- `ISR: include=['isr'], exclude=['retenciones','asimilados a salarios']`
|
|
2. Filtro por `periodicidad` ↔ `obligacion.frecuencia`: una declaración mensual
|
|
no cierra obligaciones anuales.
|
|
|
|
Datos revertidos en BD: 3 obligaciones que se habían marcado completadas por
|
|
error (Husberto DIOT, Husberto anual ISR, Horux 360 anual ISR).
|
|
|
|
### 5.6 Drill-down: falta tipo E + columna Monto Pago
|
|
Los drill-downs desde dashboard/impuestos pasaban `tipoComprobante: 'I,P'` —
|
|
excluían tipo E (notas de crédito), que sí entran en los cálculos de ingresos
|
|
(NC recibida) y gastos (NC emitida).
|
|
|
|
**Fix:**
|
|
- Nuevo param `bucket` en `GET /cfdi/drill-down`:
|
|
- `bucket=ingresos` → `(EMIT I PUE) + (EMIT P) + (RECIB E PUE)`
|
|
- `bucket=gastos` → `(RECIB I PUE) + (RECIB P) + (EMIT E PUE)`
|
|
- Aliases `causado`/`acreditable`
|
|
- 6 links en dashboard/impuestos migrados de `type+tipoComprobante+metodoPago`
|
|
a `bucket`.
|
|
- Nueva columna "Monto Pago" en la tabla drill-down — sortable, solo muestra
|
|
valor para tipo P. Excel export incluye la columna.
|
|
|
|
---
|
|
|
|
## 6. Derivado #3 — Cron watchdog SAT
|
|
|
|
Refactoricé la lógica del script CLI `scripts/sweep-stale-sat-jobs.ts` a
|
|
función exportable en **`services/sat/sweep-stale-jobs.service.ts:sweepStaleSatJobs`**.
|
|
|
|
El CLI ahora es un thin wrapper que reusa la función. El cron
|
|
`WATCHDOG_CRON_SCHEDULE = '0 */2 * * *'` en `sat-sync.job.ts` llama
|
|
`sweepStaleSatJobs({ apply: true })` cada 2 horas. Thresholds vía env:
|
|
`STALE_PENDING_HOURS` (default 12h) y `STALE_RUNNING_HOURS` (default 4h).
|
|
|
|
Gate del cron: usa el mismo `cronsEnabled` del `index.ts` (prod, o `ENABLE_CRONS_IN_DEV=1`).
|
|
|
|
---
|
|
|
|
## 7. Derivado #4 — Validación CSD↔RFC en uploadCsdContribuyente
|
|
|
|
Antes: el contador subía un CSD, Facturapi devolvía "Certificado no válido"
|
|
sin decir por qué. Podría ser RFC mismatch, FIEL subida en lugar de CSD, o
|
|
cert vencido.
|
|
|
|
Ahora `uploadCsdContribuyente` valida **antes** de subir a Facturapi usando
|
|
`@nodecfdi/credentials`:
|
|
1. Parseable + password correcta
|
|
2. `!credential.isFiel()` — debe ser CSD, no FIEL
|
|
3. `certificate.rfc() === contribuyente.rfc` (uppercase, strict)
|
|
4. `validToDateTime() > now` — no vencido
|
|
|
|
Cada fallo tiene un mensaje específico. Facturapi sigue validando también;
|
|
nuestra capa es defense-in-depth con diagnóstico mejor.
|
|
|
|
---
|
|
|
|
## 8. Derivado #5 — Investigación SAT rejections (logging mejorado)
|
|
|
|
La librería `@nodecfdi/sat-ws-descarga-masiva` expone `verifyResult.getCodeRequest()`
|
|
con los 5 códigos SAT específicos del estado de la solicitud:
|
|
- `5000 Accepted` — solicitud recibida con éxito
|
|
- `5002 Exhausted` — agotadas solicitudes de por vida (mismos parámetros)
|
|
- `5003 MaximumLimit` — tope máximo de CFDI/Metadata
|
|
- `5004 EmptyResult` — no hay información en el rango
|
|
- `5005 Duplicated` — solicitud duplicada (existe una vigente con mismos params)
|
|
|
|
Antes `verifySatRequest` solo loggeaba `getStatus().getCode()` que es el
|
|
HTTP-wrapper status (siempre 5000 "Aceptada" si la llamada HTTP funciona).
|
|
Ahora también captura `codeRequest.getValue()` + `getEntryId()` + `getMessage()`
|
|
en el debug log y en el error message cuando status es `rejected`/`failed`.
|
|
|
|
**Patrón observado en Manuel (sin datos del nuevo log):** 9 rejections de
|
|
emitidos en bloques 3-9 + 12-13, pero 10-11 funcionaron. NO es rate-limit
|
|
constante. **Hipótesis más probable:** `5005 Duplicated` — solicitudes previas
|
|
stale del SAT que interfieren con nuevas.
|
|
|
|
Acción pendiente: capturar un caso nuevo con el log mejorado para confirmar
|
|
y decidir estrategia (limpiar solicitudes previas, esperar 72h entre intentos,
|
|
reducir rangos, etc.).
|
|
|
|
---
|
|
|
|
## Archivos tocados
|
|
|
|
### Backend
|
|
- `apps/api/src/migrations/tenant/030_obligacion_periodos_declaracion_id.sql` — **nuevo** (#6)
|
|
- `apps/api/src/migrations/tenant/031_declaraciones_contribuyente_id.sql` — **nuevo** (fix 5.2)
|
|
- `apps/api/src/services/declaraciones.service.ts` — `completarObligacionesPorDeclaracion` con declaracionId + periodicidad filter + include/exclude keywords; `createDeclaracion` con `creadoPorUserId`; `listDeclaraciones` con contribuyenteId filter; `uploadComprobantePago` llama completar obligaciones
|
|
- `apps/api/src/services/obligaciones.service.ts` — `getObligacionesPorPeriodo` con LEFT JOIN declaraciones + tipo `DeclaracionLink`
|
|
- `apps/api/src/controllers/documentos.controller.ts` — `listarDeclaraciones` lee contribuyenteId; `crearDeclaracion`/`crearExtra` disparan notify; `subirComprobantePago` pasa uploadedByUserId
|
|
- `apps/api/src/services/email/templates/documento-subido.ts` — **nuevo** (#9)
|
|
- `apps/api/src/services/email/email.service.ts` — `sendDocumentoSubido`
|
|
- `apps/api/src/utils/memberships.ts` — `getTenantOwnerEmails`, `getUserEmailById`
|
|
- `apps/api/src/services/notify-upload.service.ts` — **nuevo** (#9 orquestador)
|
|
- `apps/api/src/middlewares/feature-gate.middleware.ts` — ruteo catálogo despacho/Horux 360 (fix 5.1)
|
|
- `packages/shared/src/constants/plans.ts` — `hasFeature` defensive
|
|
- `apps/api/src/controllers/cfdi.controller.ts` — nuevo param `bucket` en drillDown (fix 5.6)
|
|
- `apps/api/src/services/sat/sweep-stale-jobs.service.ts` — **nuevo** (#3)
|
|
- `apps/api/src/jobs/sat-sync.job.ts` — cron watchdog cada 2h (#3)
|
|
- `apps/api/scripts/sweep-stale-sat-jobs.ts` — refactorizado a thin CLI wrapper (#3)
|
|
- `apps/api/src/services/contribuyente-facturapi.service.ts` — `uploadCsdContribuyente` con validaciones pre-Facturapi (#4)
|
|
- `apps/api/src/services/sat/sat-client.service.ts` — `verifySatRequest` expone `codeRequest` (#5)
|
|
|
|
### Frontend
|
|
- `apps/web/app/(dashboard)/pendientes/page.tsx` — tipo `DeclaracionLink` + link ↗ (#6)
|
|
- `apps/web/app/(dashboard)/calendario/page.tsx` — ícono AlertTriangle + leyenda (#7)
|
|
- `apps/web/app/(dashboard)/dashboard/page.tsx` — drill links usan `bucket` (fix 5.6)
|
|
- `apps/web/app/(dashboard)/impuestos/page.tsx` — drill links usan `bucket` (fix 5.6)
|
|
- `apps/web/app/(dashboard)/drill-down/page.tsx` — columna "Monto Pago" + sort (fix 5.6)
|
|
- `apps/web/app/(dashboard)/documentos/page.tsx` — DeclaracionesTab pasa `selectedContribuyenteId` (fix 5.2)
|
|
- `apps/web/lib/api/declaraciones.ts` — `listDeclaraciones` con contribuyenteId (fix 5.2)
|
|
- `apps/web/lib/hooks/use-declaraciones.ts` — hook con contribuyenteId (fix 5.2)
|
|
|
|
### Documentación
|
|
- `docs/plans/2026-04-19-pending-features.md` — 4 secciones marcadas ✅ (#6 #7 #9 #10)
|
|
- `docs/plans/2026-04-22-pendientes-y-addons.md` — derivados actualizados con #3 #4 #5
|
|
- `docs/plans/2026-04-23-features-fixes-y-derivados.md` — **este doc**
|
|
|
|
### Data directa
|
|
- Zorro + Patito: `UPDATE obligacion_periodos` para revertir 3 filas completadas
|
|
por error (DIOT + 2 anuales de ISR) — sus alertas `ob-*` reabiertas.
|
|
- Declaraciones legacy con `contribuyente_id=NULL` se dejaron como están
|
|
(test data, no se eliminaron).
|
|
|
|
### Migrations aplicadas a tenants
|
|
- `030_obligacion_periodos_declaracion_id` (Zorro, Patito)
|
|
- `031_declaraciones_contribuyente_id` (Zorro, Patito)
|
|
|
|
---
|
|
|
|
## Pendientes al cierre
|
|
|
|
### Features pending (de 2026-04-19) — solo queda 1 abierto
|
|
|
|
- **#2** Convertir `/pendientes` → "Despacho" con métricas cross-contribuyente.
|
|
|
|
### Derivados abiertos — 10
|
|
|
|
| Prioridad | Derivado |
|
|
|---|---|
|
|
| Alta | Cloudflare Tunnel en prod para `MP_NOTIFICATION_URL` |
|
|
| Alta | Recrear org Facturapi de Carlos (TORC9611214CA) |
|
|
| Media | Prueba cross-contribuyente end-to-end |
|
|
| Media | Re-sync custom de rangos emitidos de Manuel (depende de capturar `codeRequest` nuevo) |
|
|
| Baja | Recomputar overage al cambiar de plan |
|
|
| Baja | `FRONTEND_URL` dev HTTPS permanente |
|
|
| Baja | Typecheck web cleanup (~12 errores) |
|
|
| Baja | Declaraciones legacy con contribuyente_id=NULL |
|
|
| Baja | Flag por despacho para activar/desactivar emails |
|
|
| Baja | Email para declaraciones sin contribuyenteId |
|
|
|
|
**Cerrados hoy:** #3 watchdog cron, #4 validación CSD, #5 logging SAT, 6 bugfixes.
|