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) yPOST /usuarios/:id/accesos(setClienteAccesos, reemplaza todos los accesos) enusuarios.controller.ts:162-194. - Frontend:
apps/web/app/(dashboard)/usuarios/page.tsxtiene botón "Editar RFCs con acceso" por cada usuario tipocliente(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
/contribuyentespor cada RFC → dialog con catálogoADDONS_POR_CONTRIBUYENTE(hoy solo Lolita IA $250/mes) - Cableado automático del overage Business Cloud:
adjustBusinessCloudOverageenaddon.service.ts, llamado desdecontribuyente.controller.ts:createy: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):
completarObligacionesPorDeclaracionendeclaraciones.service.tshace matching por keyword (IVA → 'iva',ISR → 'isr',SUELDOS → 'sueldos'|'salarios'|'nómina', etc.) contraobligaciones_contribuyente.nombrey haceINSERT ... ON CONFLICT DO UPDATEenobligacion_periodosmarcandocompletada=true.createDeclaracionllama esta función tras crear la declaración; recibe elidde 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íapnpm db:migrate-tenants. completarObligacionesPorDeclaracion(..., declaracionId)guarda el FK.getObligacionesPorPeriodohace LEFT JOIN adeclaraciones_provisionalesy devuelve el objetodeclaracion: { id, año, mes, tipo, pdfFilename } | nullpor periodo completado. Nuevo tipo exportadoDeclaracionLink.- 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_periodospara determinar completitud por (obligación, periodo). - Emite eventos con uno de 3 tipos:
obligacion-completada— siobligacion_periodos.completada = truepara el periodo.obligacion-atrasada— si no completada yfechaLimite < now().obligacion-pendiente— si no completada y aún en ventana.
fechaLimiteajustada 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 elselectedContribuyenteIddel store; el controller detecta despacho y usagenerarEventosDesdeObligacionesen 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— usabaseTemplatede la marca. Parametrizado porkind: '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— leeentidades_gestionadas.supervisor_user_iddesde BD tenant, resuelve email, dedupea con owners, EXCLUYE al uploader (no notifica su propia acción). UsaFRONTEND_URL/documentoscomo link al sistema. - Callsites en
controllers/documentos.controller.ts:crearDeclaraciondispara notify tras el INSERT con periodo "Abril 2026", tipo, impuestos, montoPago.crearExtradispara 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/coreloguea 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-*conobligacion_idinexistente (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:
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.inactiveFilterengetAlertasManualesPendientes(alertas-manuales.service.ts:269-273) — defense-in-depth: excluye en query alertas cuyo obligacion_id estéactiva=false.contribuyenteFilterstrict (alertas-manuales.service.ts:223-227) — cuando se pasacontribuyenteId, el WHERE solo incluye alertas cuyo SUBSTRING deltipocoincida con uniddeobligaciones_contribuyentedel RFC. Cross-contribuyente leak imposible.sincronizarDesdeObligacionesContribuyentegenera 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)