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

12 KiB

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)