Files
HoruxDespachos/docs/plans/2026-04-19-pending-features.md
2026-04-27 22:09:36 -06:00

238 lines
12 KiB
Markdown

# Features Pendientes — Horux Despachos
> Documentado 2026-04-19. Actualizado 2026-04-22 con estado real.
## Índice de estado al 2026-04-22
| # | Feature | Estado |
|---|---|---|
| 1 | Editar contribuyentes asignados a Cliente | ✅ **Completado** |
| 2 | Pendientes → Despacho + métricas de seguimiento | ⏸ Abierto |
| 3 | Cobro del plan desde Planes (MP) | ✅ **Completado** (Tanda 1 MP sesión 2026-04-21) |
| 4 | Timbres asignados al despacho | ✅ **Verificado** (ya usa `consumeTimbre(tenantId)`) |
| 5 | Add-ons por contribuyente | ✅ **Completado** (sesión 2026-04-22) |
| 6 | Enlazar obligaciones ↔ declaraciones | ✅ **Completado 2026-04-23** (backend + UI trazabilidad) |
| 7 | Calendario — obligaciones con colores | ✅ **Completado 2026-04-23** (backend + colores + íconos + leyenda) |
| 8 | Sección "Extras" en Documentos | ✅ **Completado** (sesión 2026-04-21) |
| 9 | Avisos por correo al subir declaración / doc extra | ✅ **Completado 2026-04-23** |
| 10 | Alertas obligaciones — filtros per-contribuyente | ✅ **Investigado 2026-04-23** — bug no reproducible; protecciones verificadas |
---
## 1. Editar contribuyentes asignados a usuario tipo Cliente — ✅ COMPLETADO
**Implementación verificada al 2026-04-22:**
- Backend: `GET /usuarios/:id/accesos` (`getClienteAccesos`) y `POST /usuarios/:id/accesos` (`setClienteAccesos`, reemplaza todos los accesos) en `usuarios.controller.ts:162-194`.
- Frontend: `apps/web/app/(dashboard)/usuarios/page.tsx` tiene botón "Editar RFCs con acceso" por cada usuario tipo `cliente` (línea 366), abre modal con checkboxes por contribuyente. Solo visible para owner en despacho.
- Guardrails: endpoint gateado por `req.user.role === 'owner'`.
---
## 2. Convertir "Pendientes" a "Despacho" + métricas de seguimiento
**Estado:** La página `/pendientes` muestra obligaciones por periodo con barras de progreso por contribuyente.
**Cambios necesarios:**
- Renombrar a "Despacho" en sidebar
- Agregar métricas de seguimiento del despacho:
- Total de contribuyentes activos
- Contribuyentes con FIEL vencida o sin FIEL
- Contribuyentes con opinión de cumplimiento negativa
- Declaraciones pendientes del mes
- Progreso de obligaciones del mes (% completado global)
- CFDIs sincronizados vs pendientes
- Resumen de alertas activas por prioridad
- Mantener la vista de pendientes/obligaciones actual como sección inferior
---
## 3. Cobro del plan desde Planes — ✅ COMPLETADO (Tanda 1 MP, sesión 2026-04-21)
Integración MP completa para planes despacho (`business_control` y
`business_cloud`), con dualidad (año 1 $21K / renovación $15K) via Opción B
(updatePreapprovalAmount tras primer pago). Ver
`docs/plans/2026-04-21-session-2-mp-setup-and-bugfixes.md` §5 y §7.
---
## 4. Timbres asignados al despacho — ✅ VERIFICADO
Confirmado al 2026-04-22: `facturacion.controller.ts:60` llama
`consumeTimbre(tenantId)` pasando el tenantId del despacho (no contribuyenteId).
El pool de timbres (`timbre_suscripciones` + `timbre_paquetes`) es compartido
entre todos los contribuyentes del despacho. La UI de timbres ya lo refleja
así. No se necesitan cambios.
---
## 5. Add-ons por contribuyente — ✅ COMPLETADO (sesión 2026-04-22)
**Implementado:**
- Schema: `SubscriptionAddon.contribuyenteId String?` (opcional; NULL = tenant-level)
- Migration `20260422172323_subscription_addons_contribuyente_id`
- Service `addon.service.ts`: `subscribeAddon(contribuyenteId)`, `listActiveAddons(tenantId, contribuyenteId?)` con preapproval MP propio por add-on
- Controller `subscription.controller.ts`: `GET /me/addons?contribuyenteId=...`, `POST /me/addons { addonCodename, contribuyenteId }`
- UI: botón ✨ Sparkles en `/contribuyentes` por cada RFC → dialog con catálogo `ADDONS_POR_CONTRIBUYENTE` (hoy solo Lolita IA $250/mes)
- Cableado automático del overage Business Cloud: `adjustBusinessCloudOverage` en `addon.service.ts`, llamado desde `contribuyente.controller.ts:create` y `:deactivate`
**Modelo descartado:** primer intento fue tabla tenant `contribuyente_addons`
con feature-toggles (facturación/conciliación/documentos/calendario/reportes).
Revertido — los add-ons reales son servicios de cobro recurrente, no
switches de features. Los gates por módulo quedan como feature futura
(requerirían middleware `requireAddon(key)` en rutas existentes).
Ver `docs/plans/2026-04-22-pendientes-y-addons.md` § "Feature: Add-ons por
contribuyente" para detalle completo.
---
## 6. Enlazar obligaciones con Declaraciones — ✅ COMPLETADO 2026-04-23
**Backend (ya existía parcialmente, se completó la trazabilidad):**
- `completarObligacionesPorDeclaracion` en `declaraciones.service.ts` hace
matching por keyword (`IVA → 'iva'`, `ISR → 'isr'`, `SUELDOS → 'sueldos'|'salarios'|'nómina'`, etc.)
contra `obligaciones_contribuyente.nombre` y hace `INSERT ... ON CONFLICT
DO UPDATE` en `obligacion_periodos` marcando `completada=true`.
- `createDeclaracion` llama esta función tras crear la declaración; recibe
el `id` de la declaración y lo propaga.
**Nuevo en esta sesión (trazabilidad):**
- **Migration 030** `obligacion_periodos.declaracion_id INT REFERENCES
declaraciones_provisionales(id) ON DELETE SET NULL` + índice parcial.
Aplicada a Zorro + Patito vía `pnpm db:migrate-tenants`.
- `completarObligacionesPorDeclaracion(..., declaracionId)` guarda el FK.
- `getObligacionesPorPeriodo` hace LEFT JOIN a `declaraciones_provisionales`
y devuelve el objeto `declaracion: { id, año, mes, tipo, pdfFilename } | null`
por periodo completado. Nuevo tipo exportado `DeclaracionLink`.
- UI `/pendientes` (vista single-contribuyente) muestra link
`↗ Declaración MM/YYYY [Compl.]` junto a cada obligación completada
que tenga FK. Click abre el PDF en nueva pestaña via
`/documentos/declaraciones/:id/pdf/declaracion`.
**Qué pasa al borrar la declaración:** `ON DELETE SET NULL` — el periodo
sigue marcado `completada=true` pero pierde la referencia. Decisión
intencional: el usuario puede volver a abrir manualmente si corresponde,
pero el estado se preserva.
**Obligaciones marcadas manualmente** (sin declaración asociada): ya
funcionaban antes, siguen funcionando. El campo `declaracion_id` queda
NULL y la UI no muestra el link.
---
## 7. Calendario — obligaciones con colores — ✅ COMPLETADO 2026-04-23
**Backend** (`calendario-fiscal.service.ts:generarEventosDesdeObligaciones`):
- Lee `obligacion_periodos` para determinar completitud por (obligación, periodo).
- Emite eventos con uno de 3 tipos:
- `obligacion-completada` — si `obligacion_periodos.completada = true` para el periodo.
- `obligacion-atrasada` — si no completada y `fechaLimite < now()`.
- `obligacion-pendiente` — si no completada y aún en ventana.
- `fechaLimite` ajustada a día hábil más próximo (considera inhábiles del año).
**Frontend** (`apps/web/app/(dashboard)/calendario/page.tsx`):
- `tipoColors`: amber / green / red para los 3 estados.
- `tipoIcons`: `Clock` (pendiente), `Check` (completada), `AlertTriangle` (atrasada).
- Leyenda visible en el CardContent del calendario que explica los colores + `custom` (violet).
- El fetch `useEventos(año)` pasa el `selectedContribuyenteId` del store; el controller
detecta despacho y usa `generarEventosDesdeObligaciones` en vez del catálogo estático
(`generarEventosFiscales`) de Horux 360.
---
## 8. Sección "Extras" en Documentos — ✅ COMPLETADO (sesión 2026-04-21)
Implementado: tabla `documentos_extras`, endpoints CRUD, pestaña en UI.
Ver `docs/plans/2026-04-21-session-2-mp-setup-and-bugfixes.md` §12.
---
## 9. Avisos por correo electrónico — ✅ COMPLETADO 2026-04-23
**Implementación:**
- **Template** `apps/api/src/services/email/templates/documento-subido.ts` — usa
`baseTemplate` de la marca. Parametrizado por `kind: 'declaracion' | 'extra'`
y bloques condicionales para periodo/tipo/impuestos/monto (declaración) o
nombre/categoría/descripción (extra). HTML escapado para evitar XSS.
- **`emailService.sendDocumentoSubido(recipients, data)`** —
`apps/api/src/services/email/email.service.ts`. Loop por recipient con
try/catch individual para que un fallo en un destinatario no bloquee los
demás. Subject incluye RFC + periodo/nombre.
- **Helpers de resolución en `utils/memberships.ts`:**
- `getTenantOwnerEmails(tenantId)` — lista todos los owners activos.
- `getUserEmailById(userId)` — resolver supervisor por UUID.
- **Orquestador** `apps/api/src/services/notify-upload.service.ts:notifyDocumentoSubido`
— lee `entidades_gestionadas.supervisor_user_id` desde BD tenant, resuelve
email, dedupea con owners, EXCLUYE al uploader (no notifica su propia
acción). Usa `FRONTEND_URL/documentos` como link al sistema.
- **Callsites** en `controllers/documentos.controller.ts`:
- `crearDeclaracion` dispara notify tras el INSERT con periodo "Abril 2026",
tipo, impuestos, montoPago.
- `crearExtra` dispara notify con nombre + categoría + descripción.
- **Fire-and-forget**: `.catch(err => console.error(...))` en ambos
call-sites — el response HTTP ya retornó cuando el email viaja.
Si SMTP no está configurado, el transport de `@horux/core` loguea a
consola en vez de fallar (dev mode).
**Fuera de alcance:** flag por despacho para activar/desactivar
notificaciones (feature futura cuando haya preferencias de notificación
a nivel tenant/user).
---
## 10. Revisar alertas de obligaciones (posible bug) — ✅ INVESTIGADO 2026-04-23
**Reporte original:** "las alertas manuales de obligaciones muestran más
obligaciones de las que tiene el contribuyente".
**Investigación:** auditoría SQL sobre los despachos activos (Patito, Zorro):
- 0 alertas `ob-*` con `obligacion_id` inexistente (huérfanas)
- 0 alertas para obligaciones con `activa=false`
- 0 alertas para periodos ya completados
- Count per-contribuyente: alertas ≤ obligaciones activas en todos los casos
- Todas las alertas actuales son del periodo actual (`2026-04`), 1 por obligación
**Protecciones verificadas en el código:**
1. **`removeObligacion` (`obligaciones.service.ts:296-311`)** — al
desactivar una obligación hace soft-delete (`activa=false`) +
`DELETE alertas WHERE tipo LIKE 'ob-{id}-%'` + `DELETE obligacion_periodos
WHERE obligacion_id=$1`. Evita alertas huérfanas incluso si queda
residuo por pools/caches.
2. **`inactiveFilter` en `getAlertasManualesPendientes`
(`alertas-manuales.service.ts:269-273`)** — defense-in-depth: excluye
en query alertas cuyo obligacion_id esté `activa=false`.
3. **`contribuyenteFilter` strict
(`alertas-manuales.service.ts:223-227`)** — cuando se pasa
`contribuyenteId`, el WHERE solo incluye alertas cuyo SUBSTRING del
`tipo` coincida con un `id` de `obligaciones_contribuyente` del RFC.
Cross-contribuyente leak imposible.
4. **`sincronizarDesdeObligacionesContribuyente` genera solo current +
previous month** — sin acumulación histórica espontánea.
**Conclusión:** el bug reportado fue probablemente corregido
implícitamente en la sesión 2026-04-18/19 cuando se agregó el cleanup
en `removeObligacion` y los filtros en `getAlertasManualesPendientes`.
El escenario único donde persistiría acumulación es un usuario que
deje periodos sin completar durante meses — pero eso refleja
correctamente la realidad fiscal (cada periodo incumplido es una
obligación pendiente propia).
**Acción:** cerrar #10. Si reaparece el síntoma, correr auditoría
SQL previa al reporte para identificar el drift específico.
---
## Cambios completados en esta sesión (2026-04-18 / 2026-04-19)
Ver `docs/plans/2026-04-18-session-fixes-and-features.md` para el detalle completo de los 16 cambios implementados, incluyendo:
- Filtro contribuyente en regímenes, alertas, CFDIs, CSD, bancos
- Declaraciones con periodicidad + monto + filtro por fecha
- Carteras y subcarteras
- ISR mensual + exclusión 605 + cálculo correcto por régimen
- Matching de obligaciones CSF mejorado
- Descarte persistente de discrepancias
- 4 migraciones (021-024)
- 3 usuarios creados (supervisor, auxiliar, cliente)