# 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)