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