# Resumen de cambios - 4 de mayo de 2026

---

## 1. Catálogo de obligaciones fiscales: nuevas obligaciones predefinidas

**Fecha:** 2026-05-04

Se agregaron 3 obligaciones fiscales predefinidas al catálogo maestro.

### Obligaciones agregadas

| ID | Nombre | Frecuencia | Fecha límite | Aplica a | Categoría | Condición | Recomendada por defecto |
|---|---|---|---|---|---|---|---|
| `isrtp` | Impuesto sobre remuneración al trabajo | mensual | Día 10 del mes siguiente | PM y PF | Estatal | Ninguna | No |
| `ish` | ISH - Impuesto Sobre Hospedaje | mensual | Día 15 del mes siguiente | PM y PF | Estatal | Ninguna | No |
| `sipare` | SIPARE - Cuotas obrero-patronales | mensual | Día 15 del mes siguiente | PM y PF | Seguridad social | Con empleados | No |

### Archivo modificado

| Archivo | Cambio |
|---|---|
| `apps/api/src/constants/obligaciones-fiscales.ts` | Se agregaron las 3 entradas al array `OBLIGACIONES_CATALOGO` |

---

## 2. Fix: Suscripciones `pending` se mostraban como activas en /configuracion/planes-despacho

**Fecha:** 2026-06-18

### Problema
En la página **Configuración › Planes**, las suscripciones con estado `pending` (primer pago aún no completado) mostraban el banner verde **"Suscripción activa"** y el badge **"Plan actual"** en verde, dando la impresión de que el plan estaba pagado y vigente.

### Causa
El frontend evaluaba `subStatus === 'authorized' || subStatus === 'pending'` para mostrar el banner de activa, y consideraba `pending` como "plan actual pagado" (`isCurrentPlanPaid`).

### Solución
- Se derivó el estado real de la suscripción con `getSubscriptionState()` de `@horux/shared`.
- El banner **"Suscripción activa"** ahora solo aparece cuando la suscripción está realmente `authorized` y dentro de su período.
- Se agregó un banner amarillo **"Suscripción pendiente de pago"** para estados `pending`.
- El badge del plan actual cambia a amarillo y muestra **"Plan actual — pendiente"** cuando la suscripción está pendiente.
- El botón **"Cancelar suscripción"** ya no se muestra para suscripciones `pending`.

### Archivos modificados
| Archivo | Cambio |
|---|---|
| `apps/web/app/(dashboard)/configuracion/planes-despacho/page.tsx` | Lógica de estado de suscripción, banners y badges |

---

## 3. Fix: Botón "Pagar este plan" fallaba para suscripciones `pending`

**Fecha:** 2026-06-18

### Problema
Al hacer clic en **"Pagar este plan"** en una suscripción con estado `pending`, se mostraba el error:  
**"No hay suscripción activa para cambiar"** en lugar de abrir MercadoPago.

### Causa
El flujo `handleContratar` intentaba crear una nueva suscripción (`subscribeMe`), pero el backend rechazaba porque ya existía una `pending`. El frontend entonces caía en `upgradeMe` y luego `changeMyPlan`, ambos validan que haya una suscripción `authorized` o `trial` — `pending` no califica, por eso el error.

### Solución
En `handleContratar`:
- Si el usuario selecciona el plan actual y la suscripción está `pending`, se llama directamente a `generatePaymentLink` para regenerar el link de pago de MercadoPago.
- Si el usuario intenta cambiar a otro plan estando `pending`, se muestra:  
  *"Completa el pago del plan actual antes de cambiar de plan."*

### Archivos modificados
| Archivo | Cambio |
|---|---|
| `apps/web/app/(dashboard)/configuracion/planes-despacho/page.tsx` | Lógica de `handleContratar` para estados `pending` |

---

## 4. Adjuntar PDFs en el correo de declaración subida

**Fecha:** 2026-05-04

### Cambio
Cuando se sube una declaración provisional (`POST /api/documentos/declaraciones`), el correo de notificación a owners y supervisor ahora incluye como adjuntos:

- El **acuse de declaración** (`pdf_declaracion`).
- La **liga de pago** (`pdf_liga_pago`), si se subió.

### Archivos modificados
| Archivo | Cambio |
|---|---|
| `packages/core/src/email/transport.ts` | `EmailTransport.send` acepta un arreglo opcional de `EmailAttachment` y lo pasa a `nodemailer.sendMail` |
| `apps/api/src/services/email/email.service.ts` | `sendEmail` y `sendDocumentoSubido` aceptan y reenvían `attachments` |
| `apps/api/src/services/notify-upload.service.ts` | Nueva función `buildDeclaracionAttachments` que lee los PDFs de `declaraciones_provisionales` y los pasa al correo |
| `apps/api/src/controllers/documentos.controller.ts` | Se pasa `declaracionId` a `notifyDocumentoSubido` para poder recuperar los PDFs |

### Notas
- Los documentos extra (`POST /api/documentos/extras`) **no** incluyen adjuntos; solo cambia el flujo de declaraciones.
- Si los adjuntos superan los 20 MB, se omiten y se deja un aviso en el cuerpo del correo para evitar rechazos por límite de SMTP.

## 5. Nueva obligación: FONACOT

**Fecha:** 2026-05-04

### Cambio
Se agregó la obligación `fonacot` al catálogo maestro de obligaciones fiscales.

| ID | Nombre | Frecuencia | Fecha límite | Aplica a | Categoría | Condición | Recomendada por defecto |
|---|---|---|---|---|---|---|---|
| `fonacot` | Crédito FONACOT | Mensual | Día 5 del mes siguiente | PM/PF | Créditos de los trabajadores | Con empleados | ❌ |

### Archivo modificado
| Archivo | Cambio |
|---|---|
| `apps/api/src/constants/obligaciones-fiscales.ts` | Se agregó la entrada `fonacot` en la sección **Créditos de los trabajadores** |

## 6. Nueva obligación: Aviso de actividades vulnerables

**Fecha:** 2026-05-04

### Cambio
Se agregó la obligación `actividades-vulnerables` al catálogo maestro.

| ID | Nombre | Frecuencia | Fecha límite | Aplica a | Categoría | Condición | Recomendada por defecto |
|---|---|---|---|---|---|---|---|
| `actividades-vulnerables` | Aviso de actividades vulnerables | Mensual | Día 17 del mes siguiente | PM/PF | Federal mensual | — | ❌ |

### Archivo modificado
| Archivo | Cambio |
|---|---|
| `apps/api/src/constants/obligaciones-fiscales.ts` | Se agregó la entrada `actividades-vulnerables` en la sección **Federales mensuales** |

## 7. Nueva obligación: Declaración Informativa de transparencia

**Fecha:** 2026-05-04

### Cambio
Se agregó la obligación `declaracion-transparencia` al catálogo maestro.

| ID | Nombre | Frecuencia | Fecha límite | Aplica a | Categoría | Condición | Recomendada por defecto |
|---|---|---|---|---|---|---|---|
| `declaracion-transparencia` | Declaración Informativa de transparencia | Anual | Día 31 de mayo | PM | Federal anual | — | ❌ |

### Archivo modificado
| Archivo | Cambio |
|---|---|
| `apps/api/src/constants/obligaciones-fiscales.ts` | Se agregó la entrada `declaracion-transparencia` en la sección **Anuales PM** |

## 8. Nueva obligación: Declaración Informativa Múltiple del IEPS (trimestral)

**Fecha:** 2026-05-04

### Cambio
Se agregó la obligación `ieps-trimestral` al catálogo maestro.

| ID | Nombre | Frecuencia | Fecha límite | Aplica a | Categoría | Condición | Recomendada por defecto |
|---|---|---|---|---|---|---|---|
| `ieps-trimestral` | Declaración Informativa Múltiple del IEPS | Trimestral | Día 17 de abril, julio, octubre y enero | PM/PF | Federal trimestral | — | ❌ |

### Archivo modificado
| Archivo | Cambio |
|---|---|
| `apps/api/src/constants/obligaciones-fiscales.ts` | Se agregó la entrada `ieps-trimestral` en la nueva sección **Federales trimestrales** |

## 9. Nueva obligación: SISUB y soporte de frecuencia cuatrimestral

**Fecha:** 2026-05-04

### Cambio
Se agregó la obligación `sisub` al catálogo y se extendió el sistema para soportar obligaciones con frecuencia **cuatrimestral**.

| ID | Nombre | Frecuencia | Fecha límite | Aplica a | Categoría | Condición | Recomendada por defecto |
|---|---|---|---|---|---|---|---|
| `sisub` | Sistema de Información de Subcontratación | Cuatrimestral | Día 17 de enero, mayo y septiembre | PM/PF | Seguridad social | Con empleados | ❌ |

### Archivos modificados
| Archivo | Cambio |
|---|---|
| `apps/api/src/constants/obligaciones-fiscales.ts` | Agregada `sisub` y `cuatrimestral` al union type de `frecuencia` |
| `apps/api/src/services/obligaciones.service.ts` | `inferirFrecuencia` y `appliesTo` soportan `cuatrimestral` |
| `apps/api/src/services/calendario-fiscal.service.ts` | Generación de eventos para meses cuatrimestrales (`1, 5, 9`) |
| `apps/api/src/services/alertas-manuales.service.ts` | `appliesToPeriod` soporta `cuatrimestral` |
| `apps/api/src/services/declaraciones.service.ts` | `Periodicidad` incluye `cuatrimestral` |
| `apps/api/src/controllers/documentos.controller.ts` | Schema de declaraciones acepta `cuatrimestral` |
| `apps/api/src/migrations/tenant/052_declaraciones_cuatrimestral.sql` | CHECK de `periodicidad` permite `cuatrimestral` |
| `apps/web/app/(dashboard)/configuracion/obligaciones/page.tsx` | Badge de frecuencia `cuatrimestral` |
| `apps/web/app/(dashboard)/pendientes/page.tsx` | Badge de frecuencia `cuatrimestral` |

## 10. Fix: sincronización SAT — tipos de CFDI, UUID case-insensitive y reutilización de requestIds

**Fecha:** 2026-05-04

### Cambios
- La verificación de CFDIs incompletos (`hasIncompleteCfdis` / `getOldestIncompleteCfdiDate`) ahora incluye los tipos de comprobante **P** (pago) y **N** (nómina), además de **I** (ingreso) y **E** (egreso).
- Al guardar/actualizar CFDIs, la comparación de `uuid` se hace con `LOWER()` para evitar duplicados por diferencias de mayúsculas/minúsculas.
- Se desactivó la reutilización de `requestId` de jobs SAT previos. Reusarlos puede agotar el límite de descargas del SAT y devolver **"Máximo de descargas permitidas"**, bloqueando el recovery.
- Se exportó `runRecoverySyncJob` para permitir su invocación manual desde scripts.

### Archivos modificados
| Archivo | Cambio |
|---|---|
| `apps/api/src/jobs/sat-sync.job.ts` | Incluir `P` y `N` en consultas de CFDIs incompletos; exportar `runRecoverySyncJob` |
| `apps/api/src/services/sat/sat.service.ts` | Comparación `LOWER(uuid)`; comentar reutilización de `requestId` |

---

## 11. Fix: drill-down de CFDIs carga el CFDI completo al visualizar

**Fecha:** 2026-05-04

### Problema
En la vista de drill-down, al hacer clic en el ojo para ver un CFDI se usaba únicamente el objeto resumen de la lista, que no incluye conceptos ni todos los detalles.

### Solución
Ahora se llama a `getCfdiById(id)` para obtener el CFDI completo antes de abrir el visor, y se muestra un estado de carga mientras se resuelve la petición.

### Archivo modificado
| Archivo | Cambio |
|---|---|
| `apps/web/app/(dashboard)/drill-down/page.tsx` | Carga completa del CFDI al hacer clic en "Ver factura" |

---

## 12. Scripts de soporte: Demo Ventas y operaciones

**Fecha:** 2026-05-04

Se crearon varios scripts de utilería bajo `apps/api/scripts/` para tareas de soporte y configuración de la cuenta Demo Ventas.

### Scripts principales
| Script | Propósito |
|---|---|
| `create-demo-ventas.ts` | Crea el tenant Demo Ventas, su BD, usuario owner y suscripción custom gratuita |
| `update-demo-ventas.ts` | Agrega usuarios supervisor/auxiliar/cliente y 5 contribuyentes adicionales a Demo Ventas |
| `seed-demo-obligaciones-tareas.ts` | Siembra obligaciones fiscales y tareas recurrentes para todos los contribuyentes de Demo Ventas |
| `fix-demo-carteras-asignaciones.ts` | Crea la subcartera del auxiliar y asigna contribuyentes, obligaciones y tareas de forma válida |
| `reset-demo-asignaciones.ts` | Deja Demo Ventas en estado "tutorial": elimina subcarteras, asignaciones y relación auxiliar-supervisor |
| `change-user-email.ts` | Cambia el correo de un usuario, genera contraseña temporal e invalida sesiones |
| `resend-welcome.ts` | Reenvía el correo de bienvenida a un usuario |

> Estos scripts no son parte del flujo productivo; se ejecutan manualmente vía `npx tsx`.

## 13. Automatización de cierre de obligaciones fiscales

**Fecha:** 2026-05-04

### Cambio
Se automatiza el cierre de **todas las obligaciones fiscales** desde la sección existente **Documentos › Declaraciones**. Al subir una declaración o su comprobante de pago, el sistema crea automáticamente evidencias en `obligacion_evidencias` y actualiza el estado de cada obligación fiscal en `obligacion_periodos`.

### Reglas de cierre deterministas
- `requierePago = false` (informativas): se marcan completadas al subir la declaración (`declaracion`).
- `requierePago = true` (pago + declaración): la declaración marca `declaracion_presentada = true`; el periodo se cierra al subir el comprobante de pago (`pago`).
- Al subir una declaración con **monto $0**, se marca el pago como presentado automáticamente.

### Nuevas tablas y columnas
| Migración | Descripción |
|---|---|
| `053_obligacion_evidencias.sql` | Tabla genérica para evidencias de obligaciones (declaración, pago, acuse, complemento) |
| `054_obligacion_periodos_estados.sql` | Agrega `declaracion_presentada`, `pago_presentado` y `evidencia_id` a `obligacion_periodos` |
| `055_declaracion_obligaciones.sql` | Relaciona declaraciones provisionales con las obligaciones fiscales que cierran |

### Nuevos endpoints (uso interno / futuro)
| Método | Endpoint | Descripción |
|---|---|---|
| `GET` | `/api/documentos/obligacion-evidencias` | Listar evidencias por contribuyente/periodo/obligación |
| `POST` | `/api/documentos/obligacion-evidencias` | Subir nueva evidencia |
| `GET` | `/api/documentos/obligacion-evidencias/:id/pdf` | Descargar PDF de evidencia |
| `DELETE` | `/api/documentos/obligacion-evidencias/:id` | Eliminar evidencia y recalcular estado del periodo |

### Archivos creados
| Archivo | Cambio |
|---|---|
| `apps/api/src/services/obligacion-evidencias.service.ts` | Servicio para crear/listar/descargar/eliminar evidencias y actualizar `obligacion_periodos` |
| `apps/api/src/migrations/tenant/053_obligacion_evidencias.sql` | Tabla `obligacion_evidencias` |
| `apps/api/src/migrations/tenant/054_obligacion_periodos_estados.sql` | Columnas de estado en `obligacion_periodos` |
| `apps/api/src/migrations/tenant/055_declaracion_obligaciones.sql` | Relación declaración ↔ obligación |
| `apps/web/lib/api/obligaciones.ts` | Cliente API para obtener obligaciones por periodo |

### Archivos modificados
| Archivo | Cambio |
|---|---|
| `apps/api/src/constants/obligaciones-fiscales.ts` | Campo `requierePago` en todas las obligaciones del catálogo |
| `apps/api/src/services/declaraciones.service.ts` | Crea evidencias en las obligaciones seleccionadas; vincula declaración con obligaciones; mantiene fallback legacy por impuestos |
| `apps/api/src/services/obligaciones.service.ts` | `getObligacionesPorPeriodo` devuelve `requierePago`, `declaracionPresentada`, `pagoPresentado` |
| `apps/api/src/services/notify-upload.service.ts` | Soporte para notificaciones de `obligacion_evidencia` |
| `apps/api/src/services/email/templates/documento-subido.ts` | Template para evidencias de obligación |
| `apps/api/src/controllers/documentos.controller.ts` | Schema de declaraciones acepta `obligacionesIds` |
| `apps/api/src/routes/documentos.routes.ts` | Rutas de evidencias |
| `apps/web/lib/api/declaraciones.ts` | `CreateDeclaracionData` acepta `obligacionesIds` |
| `apps/web/app/(dashboard)/documentos/page.tsx` | Diálogo de subida reemplaza “Impuestos cubiertos” por selector de obligaciones fiscales del periodo |

## 15. Fix: quitar toggle de completado en Configuración › Obligaciones fiscales › Tareas

**Fecha:** 2026-06-22

### Problema
En **Configuración › Obligaciones fiscales › Tareas** seguía apareciendo el botón para marcar tareas como completadas/pendientes manualmente, pero el estado de las obligaciones fiscales ahora se actualiza automáticamente desde **Documentos › Declaraciones**.

### Solución
- Se convirtió el icono de check/círculo en un indicador visual de estado (completada, pendiente, atrasada) sin interacción.
- Se eliminaron las mutaciones de completar/descompletar periodo del frontend.

### Archivo modificado
| Archivo | Cambio |
|---|---|
| `apps/web/components/obligaciones/tareas-tab.tsx` | Icono de estado estático; eliminados `completarMutation` y `descompletarMutation` |

## 14. Fix: sugerencias de Clave Producto SAT en facturación

**Fecha:** 2026-06-22

### Problema
En **Facturación › Conceptos**, el campo **Clave Producto SAT** no mostraba sugerencias al escribir.

### Causa
La tabla `cat_clave_prod_serv` de la BD central estaba vacía; el catálogo nunca se había importado.

### Solución
- Se importó el catálogo oficial CFDI 4.0 (`c_ClaveProdServ`) desde los recursos de **phpcfdi/resources-sat-catalogs** (52,513 registros).
- Se creó el script `apps/api/scripts/import-clave-prod-serv.ts` para importaciones futuras.
- Se hizo más robusto el autocomplete del campo:
  - `AbortController` para cancelar búsquedas anteriores.
  - Manejo de errores y `autoComplete="off"`.
- Se sanitizó el fallback regex en el backend para evitar errores con caracteres especiales.

### Archivos creados
| Archivo | Cambio |
|---|---|
| `apps/api/scripts/import-clave-prod-serv.ts` | Importa el catálogo desde CSV a PostgreSQL |

### Archivos modificados
| Archivo | Cambio |
|---|---|
| `apps/api/src/controllers/catalogos.controller.ts` | Escapa regex en búsqueda fallback; búsqueda por clave insensible a mayúsculas |
| `apps/web/lib/api/catalogos.ts` | `searchClaveProdServ` acepta `AbortSignal` |
| `apps/web/app/(dashboard)/facturacion/page.tsx` | `handleSearchProduct` con `AbortController`, try/catch y `autoComplete="off"` |

## 16. Fix: CSF retry, backoff, delays entre tenants y timeouts aumentados

**Fecha:** 2026-06-01

### Cambios
- Se agregó **retry con backoff** en la descarga de Constancias de Situación Fiscal.
- Se agregaron **delays entre tenants** para no saturar el portal del SAT.
- Se aumentaron los timeouts del proceso de constancia.

### Archivos modificados
| Archivo | Cambio |
|---|---|
| `apps/api/src/services/constancia.service.ts` | Retry con backoff y manejo de errores |
| `apps/api/src/services/sat/sat-csf-login.ts` | Timeouts y delays ajustados |
| `apps/api/src/jobs/sat-sync.job.ts` | Delay entre ejecuciones de CSF por tenant |

---

## 17. Fix: múltiples fixes de producción en SAT, pagos y admin

**Fecha:** 2026-06-10

### Cambios
- Ajustes en el pool de conexiones a tenants (`database.ts`).
- Webhooks de MercadoPago más robustos ante eventos inesperados.
- Mejoras en `admin-clientes.service.ts` e `invoicing.service.ts`.
- Sweep de jobs SAT stale ajustado.

### Archivos modificados
| Archivo | Cambio |
|---|---|
| `apps/api/src/config/database.ts` | Configuración de pool |
| `apps/api/src/controllers/webhook.controller.ts` | Robustez en webhooks MP |
| `apps/api/src/jobs/sat-sync.job.ts` | Fixes en recovery y scheduling |
| `apps/api/src/services/admin-clientes.service.ts` | Fixes de admin |
| `apps/api/src/services/payment/invoicing.service.ts` | Fixes de facturación |
| `apps/api/src/services/sat/sat-client.service.ts` | Fixes de cliente SAT |
| `apps/api/src/services/sat/sweep-stale-jobs.service.ts` | Ajuste de stale jobs |

---

## 18. Feat: scorecards de notas de crédito en dashboard

**Fecha:** 2026-06-13

### Cambios
- Se agregaron scorecards de **notas de crédito emitidas** y **recibidas**.
- Se reordenaron las tarjetas del dashboard.
- Se ajustó el cálculo de **utilidad neta** considerando notas de crédito.

### Archivos modificados
| Archivo | Cambio |
|---|---|
| `apps/api/src/services/dashboard.service.ts` | Nuevos campos de notas de crédito |
| `apps/web/app/(dashboard)/dashboard/page.tsx` | Scorecards y utilidad ajustada |
| `packages/shared/src/types/dashboard.ts` | Tipos actualizados |

---

## 19. Feat: cron de recuperación SAT diario a las 10:00 AM

**Fecha:** 2026-06-14

### Cambio
Se agregó un cron diario a las 10:00 AM (`America/Mexico_City`) que ejecuta el **recovery sync** para contribuyentes con FIEL activa y sync incompleto.

### Archivo modificado
| Archivo | Cambio |
|---|---|
| `apps/api/src/jobs/sat-sync.job.ts` | Nuevo `runRecoverySyncJob` y scheduling |

---

## 20. Fix: pagar plan actual en trial_expired y planes > $10k

**Fecha:** 2026-06-16

### Problema
- Los tenants con suscripción `trial_expired` no podían pagar el plan actual.
- Planes Business Control/Enterprise superiores a $10,000 no podían usar preapproval de MercadoPago.

### Solución
- Se permite pagar el plan actual incluso en estado `trial_expired`.
- Planes caros usan una **Preference one-off anual** de MercadoPago en lugar de preapproval.
- Se agregó el campo `mpPreferenceId` a `subscriptions` para trackear la preference.
- El webhook de MP maneja tanto preapproval como preference.

### Archivos modificados
| Archivo | Cambio |
|---|---|
| `apps/api/src/controllers/despacho.controller.ts` | Lógica de pago de plan actual |
| `apps/api/src/controllers/subscription.controller.ts` | Soporte de preference |
| `apps/api/src/controllers/webhook.controller.ts` | Webhook para preference |
| `apps/api/src/services/payment/mercadopago.service.ts` | Creación de preference anual |
| `apps/api/src/services/payment/subscription.service.ts` | Pago de plan actual trial_expired |
| `apps/web/app/(dashboard)/configuracion/planes-despacho/page.tsx` | UI para pagar plan actual vencido |
| `apps/api/prisma/schema.prisma` | Campo `mpPreferenceId` |

---

## 21. Feat: configuración de notificaciones por rol

**Fecha:** 2026-06-17

### Cambio
Se reemplazó el sistema de preferencias de notificación por contribuyente por uno por **rol**:
- `owner`, `supervisor`, `auxiliar` y `cliente` pueden activar/desactivar tipos de correo.
- Tipos: `documento_subido`, `weekly_update`, `subscription_expiring`, `recordatorio_fiscal`, `alertas_nuevas`, `recordatorio_proximo`.
- La tabla `notification_role_preferences` almacena la configuración por tenant.

### Archivos creados
| Archivo | Cambio |
|---|---|
| `apps/api/src/migrations/tenant/051_notification_role_preferences.sql` | Tabla de preferencias por rol |

### Archivos modificados
| Archivo | Cambio |
|---|---|
| `apps/api/src/services/notification-preferences.service.ts` | Nuevo modelo por rol |
| `apps/api/src/controllers/notification-preferences.controller.ts` | CRUD por rol |
| `apps/api/src/services/notifications.service.ts` | Filtrado por rol |
| `apps/api/src/services/notify-upload.service.ts` | Usa preferencias por rol |
| `apps/api/src/services/payment/subscription.service.ts` | Usa preferencias por rol |
| `apps/api/src/jobs/weekly-update.job.ts` | Filtrado por rol |
| `apps/web/app/(dashboard)/configuracion/notificaciones/page.tsx` | UI de configuración |

---

## 22. Fix: quitar badge "Próximamente" de notificaciones ya existentes

**Fecha:** 2026-06-16

### Cambio
Se quitó el badge "Próximamente" de los tipos de notificación que ya estaban implementados.

### Archivos modificados
| Archivo | Cambio |
|---|---|
| `apps/api/src/services/notification-preferences.service.ts` | Marcado de implementados |
| `apps/web/app/(dashboard)/configuracion/notificaciones/page.tsx` | Badges actualizados |

---

## 23. Feat: drill-down en pestaña nueva, rol Vendedor y scripts demo

**Fecha:** 2026-06-22

### Cambios
- El **drill-down de impuestos** ahora se abre en una pestaña nueva (`/drill-down`).
- Se agregó el rol **Vendedor** con acceso limitado a invitaciones trial y dashboard.
- Se crearon scripts de soporte para demos:
  - `add-demo-cfdis.ts`
  - `add-demo-notas-credito.ts`
  - `create-vendedor-fernando.ts`

### Archivos creados
| Archivo | Cambio |
|---|---|
| `apps/api/scripts/add-demo-cfdis.ts` | CFDIs de demo |
| `apps/api/scripts/add-demo-notas-credito.ts` | Notas de crédito de demo |
| `apps/api/scripts/create-vendedor-fernando.ts` | Crea usuario vendedor de demo |

### Archivos modificados
| Archivo | Cambio |
|---|---|
| `apps/web/app/(dashboard)/drill-down/page.tsx` | Página de drill-down |
| `apps/web/app/(dashboard)/impuestos/page.tsx` | Link a drill-down en pestaña nueva |
| `apps/web/app/(dashboard)/dashboard/page.tsx` | Integración con drill-down |
| `apps/web/app/(dashboard)/admin/staff/page.tsx` | Rol Vendedor en staff |
| `apps/web/components/layouts/sidebar.tsx` | Menú para Vendedor |
| `packages/shared-ui/src/charts/kpi-card.tsx` | Soporte de link externo |

---

## 24. Fix: evita logout al cambiar de tenant

**Fecha:** 2026-06-22

### Problema
Al cambiar de empresa con el selector de tenants, múltiples requests en vuelo con el token viejo intentaban refrescar simultáneamente, causando logout o 401.

### Solución
- Se agregó un **mutex de refresh token** en el interceptor de Axios.
- Se marca el estado `isSwitching` en `membership-switcher` y `mis-empresas` para evitar clicks múltiples.
- El backend invalida el refresh token anterior al hacer switch.

### Archivos modificados
| Archivo | Cambio |
|---|---|
| `apps/web/lib/api/client.ts` | Mutex de refresh |
| `apps/web/components/membership-switcher.tsx` | Flag `isSwitching` |
| `apps/web/app/(dashboard)/mis-empresas/page.tsx` | Flag `isSwitching` |
| `apps/api/src/services/auth.service.ts` | Invalidación de refresh token anterior |

---

## 25. Fix: Vendedor accede a invitaciones trial

**Fecha:** 2026-06-22

### Cambio
El rol **Vendedor** puede ver y compartir invitaciones **trial**, pero no puede invitar clientes directamente.

### Archivos modificados
| Archivo | Cambio |
|---|---|
| `apps/api/src/controllers/client-invitations.controller.ts` | Permisos para vendedor |
| `apps/api/src/controllers/tenants.controller.ts` | Permisos para vendedor |
| `apps/web/app/(dashboard)/admin/staff/page.tsx` | UI de permisos |
| `apps/web/components/layouts/sidebar.tsx` | Menú visible para vendedor |

---

## 26. Feat: monitor de sincronización SAT

**Fecha:** 2026-06-22

### Cambio
Se agregó un cron de monitoreo de sincronizaciones SAT (cada 2 horas por defecto). Detecta:
- Jobs fallidos en las últimas N horas.
- Jobs pending sin atender.
- Jobs running atascados.
- Contribuyentes con FIEL activa pero sin sync inicial.

Envía un correo de alerta a `SAT_ALERT_EMAIL` (o `ADMIN_EMAIL` si no está configurado).

### Variables de entorno
| Variable | Default | Descripción |
|---|---|---|
| `SAT_ALERT_EMAIL` | `ADMIN_EMAIL` | Destino de alertas SAT |
| `SAT_MONITOR_SCHEDULE` | `0 */2 * * *` | Cron del monitor |
| `SAT_STUCK_RUNNING_HOURS` | `2` | Horas para considerar un job running como atorado |
| `SAT_FAILED_LOOKBACK_HOURS` | `24` | Ventana hacia atrás para reportar jobs fallidos |

### Archivos creados
| Archivo | Cambio |
|---|---|
| `apps/api/src/jobs/sat-sync-monitor.job.ts` | Lógica del monitor |
| `apps/api/src/services/email/templates/sat-sync-alert.ts` | Template de alerta |

### Archivos modificados
| Archivo | Cambio |
|---|---|
| `apps/api/src/index.ts` | Arranca el monitor |
| `apps/api/src/config/env.ts` | Validación de variables |
| `apps/api/.env.example` | Documentación de variables |
| `apps/api/src/services/email/email.service.ts` | `sendSatSyncAlert` |

---

## 27. Feat: reactivación de contribuyentes y limpieza al desactivar

**Fecha:** 2026-06-22

### Cambios
- Al crear un contribuyente cuyo RFC ya existía desactivado, se **reactiva** la entidad, se actualizan sus datos y se recupera su historial (CFDIs, FIEL, tareas, etc.).
- Al desactivar un contribuyente, se limpia de `cartera_entidades`, `cliente_accesos` y se desactiva su FIEL para que no siga sincronizándose.
- El timeout del proceso de constancia se aumentó a **5 minutos**.

### Archivos modificados
| Archivo | Cambio |
|---|---|
| `apps/api/src/services/contribuyente.service.ts` | Reactivación y limpieza al desactivar |
| `apps/api/src/controllers/contribuyente.controller.ts` | Retorna `reactivated` y status 200/201 |
| `apps/api/src/services/constancia.service.ts` | Timeout 5 min |
| `apps/web/app/(dashboard)/contribuyentes/page.tsx` | Alerta al reactivar |
| `apps/web/lib/api/contribuyentes.ts` | Tipo `reactivated` |

---

## 28. Fix: scraper de Constancia de Situación Fiscal más robusto

**Fecha:** 2026-06-22

### Cambio
Se reescribió el scraper de CSF para soportar los múltiples flujos del portal SAT:
- Búsqueda de PDF en frames, iframes, embeds, links `blob:`/`data:` y popups.
- Validación de que el buffer descargado comienza con `%PDF-`.
- Fallback de click por `dispatchEvent` si el primer click no dispara el handler JSF.

### Archivo modificado
| Archivo | Cambio |
|---|---|
| `apps/api/src/services/sat/sat-csf-scraper.ts` | Reescritura del scraper |

## 29. Actualización de precios: Business Control, Enterprise, RFCs adicionales y MSI

**Fecha:** 2026-06-25

### Cambio
Se actualizaron los precios de los planes anuales de despacho y la política de cobro por RFC adicional.

### Planes anuales

| Plan | Precio anterior | Nuevo precio (primer año / renovación) |
|---|---|---|
| **Business Control** (`business_control`) | $25,850 | **$30,850** |
| **Enterprise** (`business_cloud`) | $43,000 | **$68,850** |

Los cambios se aplican en la tabla `despacho_plan_prices` y afectan **nuevas suscripciones** y **renovaciones futuras** generadas por el sistema.

### RFCs adicionales (overage)

El precio mensual por cada contribuyente extra (más allá de los 100 incluidos) ahora depende del plan:

| Plan | Precio por RFC adicional |
|---|---|
| **Business Control** | $25/mes |
| **Enterprise** | $60/mes |

Se modificó `adjustDespachoOverage` para leer el precio desde el plan de la suscripción activa en lugar de usar un valor único de catálogo.

### Meses sin intereses (MSI)

Se habilitó el pago a **hasta 12 meses** en el checkout de MercadoPago para:
- Pago anual de Business Control / Enterprise (`createSubscriptionPreference`).
- Prorrateos de upgrade entre planes anuales (`createProrationPreference`).

> **Nota:** para que las cuotas aparezcan **sin intereses**, es necesario contar con promociones MSI activas en la cuenta de vendedor de MercadoPago. Sin promociones, el checkout mostrará meses con intereses.

### Archivos modificados

| Archivo | Cambio |
|---|---|
| `apps/api/src/services/payment/addon.service.ts` | Precio de overage por plan ($25 / $60) |
| `apps/api/src/services/payment/mercadopago.service.ts` | `payment_methods.installments: 12` en preferences anuales y prorrateos |
| `apps/web/app/(dashboard)/configuracion/planes-despacho/page.tsx` | Tarjetas de precios mostradas en UI: Business Control $30,850 y Enterprise $68,850; overage $25/$60 |
| `apps/api/prisma/seed` (implícito) | Valores actualizados en `despacho_plan_prices` vía SQL |

### Deploy

Build y reload de API para limpiar el cache de `despacho_plan_prices`:

```bash
cd /root/HoruxDespachosNuevo
pnpm --filter api build
pm2 reload horux-api
pm2 reload horux-web
```

**Estado:** ✅ Exitoso

---

## 30. Supervisor puede invitar Clientes y Auxiliares

**Fecha:** 2026-06-25

### Cambio
Antes los supervisores solo podían invitar usuarios con rol **Cliente**. Ahora también pueden invitar **Auxiliares**, siempre asignados a su propia supervisión.

### Backend

| Archivo | Cambio |
|---|---|
| `apps/api/src/controllers/usuarios.controller.ts` | Permite a `supervisor` invitar `cliente` y `auxiliar`; al invitar un auxiliar se asigna a sí mismo por defecto y no permite asignar a otro supervisor |
| `apps/api/src/routes/cartera.routes.ts` | `/carteras/supervisores` ahora acepta `owner` y `supervisor` |
| `apps/api/src/controllers/cartera.controller.ts` | `getSupervisores` filtra para que un supervisor solo se vea a sí mismo en el dropdown |

### Frontend

| Archivo | Cambio |
|---|---|
| `apps/web/app/(dashboard)/usuarios/page.tsx` | El dropdown de roles para supervisor incluye **Auxiliar**; al seleccionar auxiliar se preselecciona al supervisor logueado como responsable |

### Deploy

```bash
cd /root/HoruxDespachosNuevo
pnpm --filter api build
pnpm --filter web build
pm2 reload horux-api
pm2 reload horux-web
```

**Estado:** ✅ Exitoso

---

## 31. Owner puede editar nombre y rol de usuarios

**Fecha:** 2026-06-25

### Cambio
En la página **Configuración › Usuarios**, el **owner** ahora puede editar el **nombre** y el **rol** de cualquier usuario del tenant desde un modal.

### Detalles
- Se agregó el botón **Editar** en cada fila de usuario (solo visible para `owner`).
- El modal permite cambiar:
  - **Nombre**
  - **Rol** (los roles disponibles dependen de si es tenant despacho o legacy)
- Un owner **no puede cambiar su propio rol** (solo su nombre).
- El backend ya tenía el endpoint `PATCH /usuarios/:id`; se ajustó la UI para exponerlo.

### Archivos modificados

| Archivo | Cambio |
|---|---|
| `apps/web/app/(dashboard)/usuarios/page.tsx` | Botón Editar, modal de edición, validación de rol propio |

### Deploy

```bash
cd /root/HoruxDespachosNuevo
pnpm --filter web build
pm2 reload horux-web
```

**Estado:** ✅ Exitoso

---

## 32. Recordatorios periódicos en calendario

**Fecha:** 2026-06-25

### Cambio
Los recordatorios custom del calendario ahora pueden ser **periódicos**: mensual, bimestral, trimestral o anual. Se mantienen activos indefinidamente hasta que el usuario cancele la serie.

### Modelo
Se reutiliza la tabla `recordatorios` con metadatos de serie:
- **Maestro**: `serie_id IS NULL` y `recurrencia <> 'unica'`.
- **Instancias**: filas con `serie_id = maestro.id`, una por cada ocurrencia generada.

Esto permite completar una instancia sin afectar las demás, y el cron de emails de recordatorios próximos sigue funcionando sin cambios mayores.

### Funcionalidad
- Al crear un recordatorio periódico se generan instancias para un horizonte de 24 meses.
- Al **editar** una instancia se actualizan el maestro y **todas las ocurrencias futuras no completadas**.
- Al **eliminar** una instancia periódica se **cancela toda la serie**: se desactiva el maestro y se borran las instancias futuras no completadas.
- Un cron diario a las 6:00 AM extiende automáticamente las series activas para mantener el horizonte futuro.

### Frecuencias soportadas
- `mensual`
- `bimestral`
- `trimestral`
- `anual`

### Archivos modificados

| Archivo | Cambio |
|---|---|
| `apps/api/src/migrations/tenant/056_recordatorios_periodicos.sql` | Nuevas columnas `recurrencia`, `serie_id`, `activo`, `fecha_inicio`, `fecha_fin` e índices |
| `apps/api/src/services/recordatorios.service.ts` | Lógica de creación, edición, eliminación y regeneración de series periódicas |
| `apps/api/src/services/notifications.service.ts` | Excluir maestros del envío de recordatorios próximos |
| `apps/api/src/controllers/calendario.controller.ts` | Schemas aceptan `recurrencia` y `fechaFin` |
| `apps/api/src/jobs/recordatorios-periodicos.job.ts` | Nuevo cron de extensión de series |
| `apps/api/src/index.ts` | Registro del nuevo cron |
| `apps/web/app/(dashboard)/calendario/page.tsx` | Selector de recurrencia, fecha fin, indicador de serie y confirmación al cancelar |

### Deploy

```bash
cd /root/HoruxDespachosNuevo
pnpm --filter api build
pnpm --filter web build
pnpm --filter @horux/api db:migrate-tenants
pm2 reload horux-api
pm2 reload horux-web
```

**Estado:** ✅ Exitoso

---

## 33. Filtros de Conceptos con Aplicar/Limpiar

**Fecha:** 2026-06-30

### Cambio
En **CFDIs › Conceptos**, los filtros de encabezado de tabla (UUID, Clave SAT, Descripción, No. Identificación) ahora se aplican únicamente al hacer clic en **Aplicar**, en lugar de disparar la búsqueda en cada cambio de teclado.

### Antes
Los inputs usaban `onChange` para actualizar `conceptosFilters`, lo que provocaba que `useQuery` re-lanzara la petición por cada letra escrita.

### Ahora
- Se separó el estado en dos:
  - `conceptosDraftFilters`: valores que el usuario escribe en los popovers.
  - `appliedConceptosFilters`: valores realmente aplicados a la query.
- **Aplicar**: copia el draft al estado aplicado, resetea paginación y cierra el popover.
- **Limpiar**: borra ese campo, aplica el cambio y resetea paginación.
- El ordenamiento por importe sigue funcionando con un clic en el encabezado.

### Archivos modificados

| Archivo | Cambio |
|---|---|
| `apps/web/app/(dashboard)/cfdi/page.tsx` | Separación de draft/applied filters y nuevos handlers `applyConceptosFilters` / `clearConceptosFilter` |
| `apps/api/src/controllers/cfdi.controller.ts` | El endpoint `/cfdi/conceptos` ahora recibe y pasa `noIdentificacion` al servicio |

### Deploy

```bash
cd /root/HoruxDespachosNuevo
pnpm --filter api build
pnpm --filter web build
pm2 reload horux-api
pm2 reload horux-web
```

**Estado:** ✅ Exitoso

---

## 34. Filtro de fechas en CFDIs usa fecha de emisión

**Fecha:** 2026-06-30

### Cambio
El filtro de fechas en el listado y export de **CFDIs** ahora filtra por **`fecha_emision`** en lugar de `fecha_efectiva`.

### Antes
El where clause usaba:
```sql
COALESCE(fecha_efectiva, fecha_emision - interval '1 hour') BETWEEN fechaInicio AND fechaFin
```
Esto provocaba que facturas emitidas en meses anteriores pero pagadas/ejercidas en el rango seleccionado aparecieran en el resultado (por ejemplo, facturas de febrero con `fecha_efectiva` en junio).

### Ahora
El filtro usa directamente:
```sql
fecha_emision::date BETWEEN fechaInicio AND fechaFin
```
El export Excel hereda el mismo filtro, así que tabla y Excel son consistentes.

### Archivos modificados

| Archivo | Cambio |
|---|---|
| `apps/api/src/services/cfdi.service.ts` | Filtros de fecha en `getCfdis`, `getConceptosList` y `downloadXmlsZip` usan `fecha_emision::date` |

### Deploy

```bash
cd /root/HoruxDespachosNuevo
pnpm --filter api build
pnpm --filter web build
pm2 reload horux-api
pm2 reload horux-web
```

**Estado:** ✅ Exitoso

---

## Deploy histórico

### Preparación

1. Asegurarse de que `apps/api/.env` tenga las nuevas variables (tienen defaults, pero conviene declararlas):
   ```bash
   SAT_ALERT_EMAIL=carlos@horuxfin.com
   SAT_MONITOR_SCHEDULE=0 */2 * * *
   SAT_STUCK_RUNNING_HOURS=2
   SAT_FAILED_LOOKBACK_HOURS=24
   ```
2. Si hay scripts de debug antiguos en `apps/api/scripts/`, limpiar los que no sean necesarios antes de commitear.

### Comandos

```bash
cd /root/HoruxDespachosNuevo
pnpm --filter @horux/core build
pnpm --filter api build
pnpm --filter web build
npx tsx apps/api/scripts/migrate-tenants.ts
pm2 reload horux-api
pm2 reload horux-web
```

**Estado:** ✅ Exitoso
