Notas de versión — 3 de abril de 2026
Esta versión completa el sistema de aplicación de límites tras degradación — el conjunto completo de cambios de backend, frontend, correo e IU que garantizan que la aplicación gestione correctamente las situaciones en las que los datos existentes de una unidad superan los límites de su nuevo plan (inferior) tras una degradación de suscripción.
Aplicación de degradación: Gestión completa de recursos con límite superado
Descripción general
Cuando el propietario de un espacio de trabajo degrada de PRO a FREE (o de ENTERPRISE a PRO), sus datos existentes pueden superar los límites del nuevo plan. Esta versión garantiza que cada capa del sistema — API, IU, notificaciones — aplique y comunique correctamente esos límites.
Límites afectados:
| Recurso | Límite FREE | Límite PRO |
|---|---|---|
| Miembros del equipo | 3 | 20 |
| Proyectos (por espacio de trabajo) | 3 | Ilimitado |
| Tareas (por espacio de trabajo) | 15 | 70 |
| Entradas de tiempo (por espacio de trabajo) | 100 | 1.000 |
Aplicación en el backend
Bloqueo de escritura para usuarios bloqueados
Todos los endpoints de escritura (POST, PUT, PATCH, DELETE) en proyectos, tareas, equipo y entradas de tiempo ahora están protegidos por el middleware requireNotOverLimitLocked. Los usuarios cuya cuenta ha sido bloqueada debido a una degradación reciben una respuesta 403 ACCOUNT_LOCKED en cualquier intento de escritura.
Bloqueo del inicio del temporizador
El endpoint startTimer() aplica el límite de entradas de tiempo para el espacio de trabajo. Si se intenta iniciar un nuevo temporizador cuando el espacio de trabajo está en el límite o lo ha superado, se devuelve 403 LIMIT_EXCEEDED.
Bloqueo de invitación al equipo
El endpoint inviteUser() comprueba maxTeamMembers con el nivel actual antes de enviar una invitación. Las invitaciones que superan el límite devuelven 403 LIMIT_EXCEEDED.
Bloqueo de miembros al degradar (Webhook)
Cuando se recibe un evento customer.subscription.updated o customer.subscription.deleted y el nivel disminuye, markExcessMembersAsLocked() se ejecuta automáticamente:
- Consulta todos los miembros
activosypendientes(ambos cuentan para el uso) - Bloquea a los miembros más recientes que superan el nuevo límite
- Envía correos de notificación a cada miembro bloqueado
Notificaciones por correo
Correo de resumen de degradación (Propietario)
El propietario del espacio de trabajo recibe un correo inmediatamente tras la degradación con la lista de todos los recursos con límite superado:
- Número de miembros del equipo bloqueados
- Número de proyectos en exceso
- Enlaces para resolver cada problema (Equipo, Proyectos, Actualizar)
Correo de acceso limitado (Miembros bloqueados)
Cada miembro cuyo acceso ha sido bloqueado recibe un correo de notificación personal que explica:
- La cuenta ha sido degradada
- Su acceso ahora está limitado
- A quién contactar para restaurar el acceso completo
Frontend: IU de límite superado
Banner global de usuario bloqueado
Los usuarios con isOverLimitLocked: true ven un banner de advertencia persistente en toda la aplicación (renderizado en AppShell):
"El acceso a tu cuenta está limitado debido a una degradación del plan. Contacta con el propietario del espacio de trabajo para restaurar el acceso."
Banners de límite superado en todas las páginas de recursos
| Página | Condición del banner |
|---|---|
| Proyectos | actual > máx. proyectos |
| Tareas | actual >= máx. tareas |
| Seguimiento de tiempo | actual >= máx. entradas de tiempo |
| Miembros del equipo | actual > máx. miembros (corregido: era >=) |
Todos los banners incluyen un botón de acción Actualizar.
Banners de solo lectura para proyectos en exceso
- Página de detalles del proyecto — aparece un banner de advertencia cuando el proyecto está marcado como
isExcessProject: true, con una explicación y un enlace para resolver el problema - Pestaña Tareas del proyecto — la creación de tareas está desactivada con un banner que explica que el proyecto está en modo de solo lectura
Estados desactivados en los selectores
Los recursos bloqueados/en exceso se muestran visualmente desactivados (no ocultos) en todos los menús desplegables de selección:
| Componente | Elementos desactivados |
|---|---|
| Crear tarea — Proyecto | isExcessProject: true → insignia 🔒 "Límite superado" |
| Editar tarea — Proyecto | isExcessProject: true → insignia 🔒 "Límite superado" |
| Crear tarea — Asignados | isOverLimitLocked: true → insignia 🔒 "Acceso limitado" |
| Editar tarea — Asignados | isOverLimitLocked: true → insignia 🔒 "Acceso limitado" |
| Temporizador / Entrada manual — Proyecto/Tarea | isExcessProject: true → insignia 🔒 "Límite superado" |
Corrección del banner de límite superado — > estricto
Problema: El banner de advertencia de "Límite superado" en la página de Miembros del equipo aparecía exactamente en 3/3 (en el límite), no solo cuando se superaba estrictamente.
Corrección: El banner ahora usa actual > máx. (estrictamente superior). Estar en el límite es normal; estar por encima es un estado excepcional que requiere acción.
Correcciones de errores
Consulta de bloqueo de miembros: Miembros pendientes incluidos
markExcessMembersAsLocked() anteriormente solo consultaba miembros con status: 'active'. Dado que los miembros pending también cuentan para el límite de uso en UsageTrackingService, esto hacía que la lógica de bloqueo y el contador no concordaran.
Corrección: La consulta ahora usa { status: { $in: ['active', 'pending'] } }.
Límites de nivel del modelo Unit corregidos
El hook de pre-guardado del modelo Unit tenía valores hardcoded (FREE: 5 miembros, PRO: 25 miembros) que conflictuaban con los valores autoritativos de tiers.config.ts.
Corrección: El hook de pre-guardado ahora usa los valores correctos: FREE: 3, PRO: 20.
Herramientas para desarrolladores / Operaciones
Script manual-downgrade-to-free.mjs
Degrada manualmente una unidad a FREE en la base de datos — útil para flujos de trabajo de prueba y soporte sin activar un evento real de Stripe.
node scripts/manual-downgrade-to-free.mjs <unitBundle>
Acciones: Establece el nivel, límites, cancelAtPeriodEnd, currentPeriodEnd, y ejecuta markExcessMembersAsLocked() de inmediato.
Script test-downgrade-enforcement.mjs
Simula el ciclo completo de aplicación de degradación/actualización sin Stripe.
node scripts/test-downgrade-enforcement.mjs <unitBundle> [status|downgrade|upgrade]
| Escenario | Efecto |
|---|---|
status | Muestra el estado de bloqueo actual de todos los miembros |
downgrade | Simula PRO → FREE, bloquea los miembros en exceso |
upgrade | Simula la actualización de vuelta, desbloquea todos los miembros |
Resumen
| Área | Cambio |
|---|---|
| Backend | Verificación de límite en startTimer() — temporizador ya aplicado (confirmado) |
| Backend | Verificación de límite de miembros en inviteUser() — ya aplicado (confirmado) |
| Backend | Middleware requireNotOverLimitLocked — aplicado a todas las rutas de escritura (proyectos, tareas, equipo, entradas de tiempo) |
| Backend | markExcessMembersAsLocked ahora incluye miembros pendientes |
| Backend | Límites del hook de pre-guardado de Unit.model.ts corregidos (FREE: 3, PRO: 20) |
| Backend | El webhook de degradación envía correo de resumen al propietario |
| Backend | Los miembros bloqueados reciben correo de notificación de acceso limitado |
| Frontend | Banner de la página de Miembros del equipo: >= → > (límite superado estricto) |
| Frontend | Diálogo de creación de tarea: miembros bloqueados mostrados como desactivados con insignia |
| Frontend | Diálogo de edición de tarea: miembros bloqueados mostrados como desactivados con insignia |
| Frontend | Diálogo de creación de tarea: proyectos en exceso mostrados como desactivados con insignia |
| Frontend | Diálogo de edición de tarea: proyectos en exceso mostrados como desactivados con insignia |
| Frontend | Selector Proyecto/Tarea: recursos bloqueados mostrados como desactivados con insignia |
| Frontend | AppShell.tsx: banner persistente global para usuarios bloqueados |
| Frontend | ProjectDetailsPage: banner de solo lectura en proyectos en exceso |
| Frontend | ProjectTasksTab: banner de creación de tareas desactivada en proyectos en exceso |
| Frontend | TimeTrackingPage: banner de límite superado + bloqueo del widget del temporizador |
| Frontend | ProjectsPage: banner de límite superado |
| Frontend | TaskManagementPage: banner de límite superado |
| Scripts | manual-downgrade-to-free.mjs añadido |
| Scripts | test-downgrade-enforcement.mjs añadido |
Disponibilidad: Todos los planes (la aplicación se activa automáticamente al degradar)