Skip to content

Gestión de Equipo

Módulos API: organizations · Páginas web: OrgMembersPage, OrgPathsPage, TeamProgressPage, OrgDashboard Spec Playwright: apps/e2e/tests/corporate/team.spec.ts — 📋 pendiente


EndpointMétodoDescripción
GET /organizations/:orgId/membersGETListado de miembros activos con estado y progreso
GET /organizations/:orgId/members/statsGETKPIs de miembros: seats, activos, pendientes
GET /organizations/:orgId/members/:memberIdGETDetalle de un miembro individual
PATCH /organizations/:orgId/members/:memberIdPATCHActualizar rol de un miembro
DELETE /organizations/:orgId/members/:memberIdDELETEEliminar miembro (pierde acceso corporativo)
GET /organizations/:orgId/invitationsGETInvitaciones pendientes
POST /organizations/:orgId/invitationsPOSTInvitar uno o varios usuarios por email
POST /organizations/:orgId/invitations/:id/resendPOSTReenviar invitación pendiente
GET /organizations/:orgId/pathsGETPaths asignados a la organización
POST /organizations/:orgId/pathsPOSTAñadir path del catálogo a la organización
POST /organizations/:orgId/paths/:pathId/assignPOSTAsignar path a miembros específicos
GET /organizations/:orgId/paths/catalogGETPaths del catálogo no añadidos aún
POST /organizations/:orgId/members/:memberId/nudgePOSTEnviar recordatorio a un miembro
GET /corporate/dashboardGETDashboard corporativo con métricas y alertas
GET /organizations/:orgId/members/exportGETExportar miembros como CSV

US-CORP-001: Invitar usuario al equipo corporativo por email

Section titled “US-CORP-001: Invitar usuario al equipo corporativo por email”

Como CORPORATE_ADMIN Quiero invitar uno o varios usuarios por email al equipo de mi organización Para que obtengan acceso corporativo a los contenidos de la plataforma

Criterios: AC-CORP-001 · Casos: TC-CORP-001, TC-CORP-002, TC-CORP-003 · Prioridad: Alta · ✅ Implementado


US-CORP-002: Ver estado de las invitaciones enviadas

Section titled “US-CORP-002: Ver estado de las invitaciones enviadas”

Como CORPORATE_ADMIN Quiero ver el listado de invitaciones pendientes con su estado y fecha de expiración Para saber qué usuarios aún no han aceptado su invitación

Criterios: AC-CORP-002 · Casos: TC-CORP-004 · Prioridad: Alta · ✅ Implementado


US-CORP-003: Reenviar una invitación pendiente

Section titled “US-CORP-003: Reenviar una invitación pendiente”

Como CORPORATE_ADMIN Quiero poder reenviar una invitación que ya emitió sin crear una nueva Para renovar su validez y que el usuario la reciba de nuevo

Criterios: AC-CORP-003 · Casos: TC-CORP-005 · Prioridad: Media · ✅ Implementado


US-CORP-004: Ver listado de miembros activos del equipo

Section titled “US-CORP-004: Ver listado de miembros activos del equipo”

Como CORPORATE_ADMIN Quiero ver la lista de miembros activos con su estado, rol y última actividad Para tener visibilidad sobre el equipo y detectar miembros inactivos

Criterios: AC-CORP-004 · Casos: TC-CORP-006, TC-CORP-007 · Prioridad: Alta · ✅ Implementado


Como CORPORATE_ADMIN Quiero revocar el acceso de un miembro a la organización Para que deje de consumir un seat y pierda los privilegios corporativos

Criterios: AC-CORP-005 · Casos: TC-CORP-008, TC-CORP-009 · Prioridad: Alta · ✅ Implementado


US-CORP-006: Asignar un path a miembros del equipo

Section titled “US-CORP-006: Asignar un path a miembros del equipo”

Como CORPORATE_ADMIN Quiero asignar un path de aprendizaje a uno o varios miembros Para estructurar el plan formativo del equipo y que reciban notificación de la asignación

Criterios: AC-CORP-006 · Casos: TC-CORP-010, TC-CORP-011 · Prioridad: Alta · ✅ Implementado


US-CORP-007: Ver progreso de miembros en asignaciones

Section titled “US-CORP-007: Ver progreso de miembros en asignaciones”

Como CORPORATE_ADMIN Quiero ver el progreso individual de cada miembro en los paths asignados Para identificar quiénes están avanzando y quiénes necesitan apoyo

Criterios: AC-CORP-007 · Casos: TC-CORP-012 · Prioridad: Alta · ✅ Implementado


US-CORP-008: Enviar recordatorio (nudge) a un miembro inactivo

Section titled “US-CORP-008: Enviar recordatorio (nudge) a un miembro inactivo”

Como CORPORATE_ADMIN Quiero enviar un recordatorio personalizado a un miembro con poca actividad Para re-engancharle en su formación sin necesidad de contactarle por otro canal

Criterios: AC-CORP-008 · Casos: TC-CORP-013 · Prioridad: Media · ✅ Implementado


US-CORP-009: Controlar el límite de seats del plan

Section titled “US-CORP-009: Controlar el límite de seats del plan”

Como CORPORATE_ADMIN Quiero recibir una advertencia visual cuando el plan alcance su límite de seats Para no superar la capacidad contratada y saber cuándo ampliar el plan

Criterios: AC-CORP-009 · Casos: TC-CORP-014 · Prioridad: Alta · ✅ Implementado


US-CORP-010: Denegar acceso al portal corporativo a usuarios sin rol adecuado

Section titled “US-CORP-010: Denegar acceso al portal corporativo a usuarios sin rol adecuado”

Como sistema Quiero bloquear el acceso a /corporate/* a usuarios que no sean CORPORATE_ADMIN o SUPER_ADMIN Para proteger la información del equipo y los datos corporativos

Criterios: AC-CORP-010 · Casos: TC-CORP-015, TC-CORP-016 · Prioridad: Alta · ✅ Implementado


US: US-CORP-001

AC-CORP-001.1 — Invitación exitosa a un email válido

Dado que soy CORPORATE_ADMIN con seats disponibles
Cuando envío una invitación al email "nuevo@empresa.com"
Entonces la API devuelve { sent: 1 } con status 201
Y aparece una entrada en invitaciones pendientes con status "PENDING"
Y la invitación expira en 7 días

AC-CORP-001.2 — Invitación masiva a varios emails

Dado que soy CORPORATE_ADMIN con suficientes seats disponibles
Cuando envío invitaciones a ["a@e.com", "b@e.com", "c@e.com"]
Entonces la API devuelve { sent: 3 }
Y se crean 3 entradas en invitaciones pendientes

AC-CORP-001.3 — Error al superar el límite de seats

Dado que el plan tiene 10 seats y ya hay 9 miembros activos + 0 pendientes
Cuando intento invitar a 2 nuevos emails
Entonces la API devuelve 403 con código "SEATS_LIMIT_REACHED"
Y el cuerpo incluye { maxSeats: 10, currentUsed: 9, requested: 2 }
Y no se crea ninguna invitación

AC-CORP-001.4 — Rol del invitado configurable

Dado que envío una invitación con role: "CORPORATE_ADMIN"
Cuando el invitado acepta la invitación
Entonces el usuario obtiene rol CORPORATE_ADMIN en la organización

US: US-CORP-002

AC-CORP-002.1 — Listado de invitaciones

Dado que existen 3 invitaciones con status PENDING en mi organización
Cuando accedo a "GET /organizations/:orgId/invitations"
Entonces la API devuelve exactamente 3 objetos
Y cada objeto contiene: id, email, role, status, expiresAt, createdAt

AC-CORP-002.2 — Solo se muestran invitaciones PENDING

Dado que existen invitaciones con status ACCEPTED y PENDING
Cuando consulto las invitaciones
Entonces solo recibo las que tienen status "PENDING"

US: US-CORP-003

AC-CORP-003.1 — Reenvío exitoso

Dado que existe una invitación PENDING con id "inv-123"
Cuando llamo a "POST /organizations/:orgId/invitations/inv-123/resend"
Entonces la invitación se actualiza con una nueva expiresAt (7 días desde ahora)
Y el status permanece en "PENDING"
Y la API devuelve el objeto de invitación actualizado

AC-CORP-003.2 — Invitación no encontrada

Dado que el id "inv-999" no existe en la organización
Cuando intento reenviarla
Entonces la API devuelve 404

AC-CORP-004: Ver listado de miembros activos

Section titled “AC-CORP-004: Ver listado de miembros activos”

US: US-CORP-004

AC-CORP-004.1 — Listado con estado calculado

Dado que un miembro tiene lastActiveAt hace 3 días
Cuando consulto el listado de miembros
Entonces ese miembro aparece con status "active"
Y si tiene lastActiveAt hace 15 días, aparece con status "inactive"
Y si lleva menos de 7 días en la plataforma, aparece con status "new"

AC-CORP-004.2 — Filtro por estado

Dado que el listado tiene miembros con estados activo, inactivo y nuevo
Cuando llamo con query "?filter=active"
Entonces solo se devuelven los miembros con status "active"

AC-CORP-004.3 — Búsqueda por nombre o email

Dado que hay un miembro con nombre "Ana García"
Cuando llamo con query "?q=ana"
Entonces solo se devuelven los miembros cuyo nombre o email contiene "ana" (case-insensitive)

US: US-CORP-005

AC-CORP-005.1 — Eliminación exitosa

Dado que "miembro-123" pertenece a mi organización
Cuando llamo a "DELETE /organizations/:orgId/members/miembro-123"
Entonces la API devuelve 204
Y el usuario queda con organizationId: null y role: "FREE"
Y deja de aparecer en el listado de miembros de la organización

AC-CORP-005.2 — No puede eliminarse a sí mismo

Dado que soy el CORPORATE_ADMIN con id "admin-001"
Cuando llamo a "DELETE /organizations/:orgId/members/admin-001"
Entonces la API devuelve 400 con mensaje "Cannot remove yourself"

AC-CORP-005.3 — Miembro no pertenece a la organización

Dado que "usuario-ajeno" no pertenece a mi organización
Cuando intento eliminarlo
Entonces la API devuelve 404

US: US-CORP-006

AC-CORP-006.1 — Asignación exitosa

Dado que el path "path-abc" existe y el miembro "miembro-001" pertenece a mi organización
Cuando llamo a "POST /organizations/:orgId/paths/path-abc/assign" con { memberIds: ["miembro-001"] }
Entonces la API devuelve { assigned: 1 }
Y el path queda añadido a la organización si no estaba
Y el miembro queda matriculado en el path

AC-CORP-006.2 — Notificación enviada al asignar

Dado que asigno el path "IA en el trabajo" a los miembros ["m1", "m2"]
Cuando la asignación se completa
Entonces se envía una notificación CORP_PATH_ASSIGNED a cada miembro asignado
Y la notificación contiene el título del path y el nombre de la organización

AC-CORP-006.3 — Path no existe

Dado que el path "path-inexistente" no existe en la BD
Cuando intento asignarlo
Entonces la API devuelve 404

US: US-CORP-007

AC-CORP-007.1 — Progreso visible por path y miembro

Dado que el miembro "miembro-001" tiene 3 paths asignados
Cuando accedo al detalle del miembro via "GET /organizations/:orgId/members/:memberId"
Entonces la respuesta incluye un array "paths" con cada path asignado
Y cada path muestra su título y progreso

AC-CORP-007.2 — Dashboard muestra top members con progreso

Dado que mi organización tiene miembros con distintos scores de fluidez
Cuando accedo al corporate dashboard
Entonces la respuesta incluye "topMembers" con los primeros 6 miembros
Y cada uno muestra score, progress y lastSeenAt

AC-CORP-008: Enviar nudge a miembro inactivo

Section titled “AC-CORP-008: Enviar nudge a miembro inactivo”

US: US-CORP-008

AC-CORP-008.1 — Nudge individual exitoso

Dado que el plan tiene la feature "churnPrediction" activada
Y el miembro "miembro-001" pertenece a mi organización
Cuando llamo a "POST /organizations/:orgId/members/miembro-001/nudge"
Entonces la API devuelve { message: "Nudge sent to ..." } con status 200

AC-CORP-008.2 — Nudge bloqueado si el plan no lo incluye

Dado que el plan de mi organización no tiene la feature "churnPrediction"
Cuando intento enviar un nudge
Entonces la API devuelve un error indicando que la feature no está disponible en el plan

US: US-CORP-009

AC-CORP-009.1 — Banner de seats visible con progreso

Dado que mi organización tiene 10 seats y 7 usados
Cuando accedo a la página de gestión de miembros
Entonces veo un banner con "7 / 10 usados"
Y la barra de progreso muestra un 70% de ocupación

AC-CORP-009.2 — Botón invitar deshabilitado al 100% de ocupación

Dado que usedSeats + pendingInvites = maxSeats
Cuando accedo a la página de gestión de miembros
Entonces el botón "Invitar miembro" está deshabilitado
Y al pasar el cursor sobre él se muestra el tooltip "Has alcanzado el límite de asientos del plan"

AC-CORP-009.3 — Barra de seats en rojo al superar el 90%

Dado que el ratio usedSeats / maxSeats supera el 90%
Cuando visualizo el banner de seats
Entonces la barra de progreso se muestra en rojo en lugar de púrpura

AC-CORP-010: Control de acceso al portal corporativo

Section titled “AC-CORP-010: Control de acceso al portal corporativo”

US: US-CORP-010

AC-CORP-010.1 — Acceso denegado a usuario FREE

Dado que soy usuario con rol FREE
Cuando accedo a "/corporate/team"
Entonces soy redirigido a "/dashboard"
Y no veo ningún dato corporativo

AC-CORP-010.2 — Acceso denegado a STARTER y PRO

Dado que soy usuario con rol STARTER
Cuando accedo directamente a "/corporate/team"
Entonces soy redirigido a "/dashboard"
Y la API devuelve 403 si intento llamar a "GET /organizations/:orgId/members"

AC-CORP-010.3 — CORPORATE_USER no puede acceder al portal de admin corporativo

Dado que soy usuario con rol CORPORATE_USER (miembro del equipo, no admin)
Cuando accedo a "/corporate/team"
Entonces soy redirigido a "/dashboard"

AC-CORP-010.4 — CORPORATE_ADMIN accede correctamente

Dado que soy usuario con rol CORPORATE_ADMIN y tengo organizationId asignado
Cuando accedo a "/corporate/team"
Entonces veo la página de gestión de miembros sin redirección

Fixtures utilizados en estos test cases:

// fixtures.corpAdmin → usuario con rol CORPORATE_ADMIN y organizationId asignado
// fixtures.corpUser → usuario con rol CORPORATE_USER (miembro ya en el equipo)
// fixtures.free → usuario con rol FREE (a invitar o sin acceso corporativo)
// fixtures.starter → usuario con rol STARTER (sin acceso corporativo)

TC-CORP-001: Invitación exitosa a un email

Section titled “TC-CORP-001: Invitación exitosa a un email”

AC: AC-CORP-001.1 · Prioridad: Alta · Tipo: API · Estado: 📋 Pendiente Spec: apps/e2e/tests/corporate/invitations.spec.ts

const newEmail = `qa-corp-invite-${Date.now()}@nappai.test`
const res = await apiClient
.withToken(fixtures.corpAdmin.token)
.post(`/organizations/${fixtures.corpAdmin.organizationId}/invitations`, {
emails: [newEmail],
role: 'CORPORATE_USER',
})
expect(res.status).toBe(201)
expect(res.data).toEqual({ sent: 1 })
// Verificar que la invitación aparece en el listado
const list = await apiClient
.withToken(fixtures.corpAdmin.token)
.get(`/organizations/${fixtures.corpAdmin.organizationId}/invitations`)
expect(list.data).toContainEqual(
expect.objectContaining({ email: newEmail, status: 'PENDING' })
)

TC-CORP-002: Invitación masiva a varios emails

Section titled “TC-CORP-002: Invitación masiva a varios emails”

AC: AC-CORP-001.2 · Prioridad: Alta · Tipo: API · Estado: 📋 Pendiente

const emails = [
`qa-bulk-a-${Date.now()}@nappai.test`,
`qa-bulk-b-${Date.now()}@nappai.test`,
`qa-bulk-c-${Date.now()}@nappai.test`,
]
const res = await apiClient
.withToken(fixtures.corpAdmin.token)
.post(`/organizations/${fixtures.corpAdmin.organizationId}/invitations`, { emails })
expect(res.status).toBe(201)
expect(res.data.sent).toBe(3)

TC-CORP-003: Error al superar el límite de seats

Section titled “TC-CORP-003: Error al superar el límite de seats”

AC: AC-CORP-001.3 · Prioridad: Alta · Tipo: API · Estado: 📋 Pendiente

Precondiciones: La organización de test debe tener el plan con maxSeats conocido y estar al límite.

// Usar una organización de test configurada al límite de seats
const res = await apiClient
.withToken(fixtures.corpAdmin.token)
.post(`/organizations/${fixtures.corpAdmin.organizationId}/invitations`, {
emails: ['overflow1@nappai.test', 'overflow2@nappai.test'],
})
expect(res.status).toBe(403)
expect(res.data.code).toBe('SEATS_LIMIT_REACHED')
expect(res.data).toHaveProperty('maxSeats')
expect(res.data).toHaveProperty('currentUsed')
expect(res.data).toHaveProperty('requested')

TC-CORP-004: Listado de invitaciones solo muestra PENDING

Section titled “TC-CORP-004: Listado de invitaciones solo muestra PENDING”

AC: AC-CORP-002.1 + AC-CORP-002.2 · Prioridad: Alta · Tipo: API · Estado: 📋 Pendiente

const res = await apiClient
.withToken(fixtures.corpAdmin.token)
.get(`/organizations/${fixtures.corpAdmin.organizationId}/invitations`)
expect(res.status).toBe(200)
expect(Array.isArray(res.data)).toBe(true)
// Todas las invitaciones devueltas tienen status PENDING
for (const inv of res.data) {
expect(inv.status).toBe('PENDING')
expect(inv).toHaveProperty('email')
expect(inv).toHaveProperty('role')
expect(inv).toHaveProperty('expiresAt')
expect(inv).toHaveProperty('createdAt')
}

TC-CORP-005: Reenviar invitación renueva expiración

Section titled “TC-CORP-005: Reenviar invitación renueva expiración”

AC: AC-CORP-003.1 · Prioridad: Media · Tipo: API · Estado: 📋 Pendiente

// Crear una invitación primero
const createRes = await apiClient
.withToken(fixtures.corpAdmin.token)
.post(`/organizations/${fixtures.corpAdmin.organizationId}/invitations`, {
emails: [`qa-resend-${Date.now()}@nappai.test`],
})
expect(createRes.data.sent).toBe(1)
// Obtener el id de la invitación creada
const listRes = await apiClient
.withToken(fixtures.corpAdmin.token)
.get(`/organizations/${fixtures.corpAdmin.organizationId}/invitations`)
const inv = listRes.data[0]
const originalExpiry = inv.expiresAt
// Reenviar
const resendRes = await apiClient
.withToken(fixtures.corpAdmin.token)
.post(`/organizations/${fixtures.corpAdmin.organizationId}/invitations/${inv.id}/resend`)
expect(resendRes.status).toBe(201)
expect(resendRes.data.status).toBe('PENDING')
// La nueva expiración es >= a la original (renovada en 7 días)
expect(new Date(resendRes.data.expiresAt).getTime()).toBeGreaterThanOrEqual(
new Date(originalExpiry).getTime()
)

TC-CORP-006: Listado de miembros con filtro de estado

Section titled “TC-CORP-006: Listado de miembros con filtro de estado”

AC: AC-CORP-004.1 + AC-CORP-004.2 · Prioridad: Alta · Tipo: API · Estado: 📋 Pendiente Spec: apps/e2e/tests/corporate/members.spec.ts

const allRes = await apiClient
.withToken(fixtures.corpAdmin.token)
.get(`/organizations/${fixtures.corpAdmin.organizationId}/members`)
expect(allRes.status).toBe(200)
expect(Array.isArray(allRes.data)).toBe(true)
// Verificar que cada miembro tiene los campos esperados
for (const member of allRes.data) {
expect(member).toHaveProperty('id')
expect(member).toHaveProperty('name')
expect(member).toHaveProperty('email')
expect(member).toHaveProperty('role')
expect(member).toHaveProperty('status')
expect(['active', 'risk', 'inactive', 'new']).toContain(member.status)
}
// Filtro por activos
const activeRes = await apiClient
.withToken(fixtures.corpAdmin.token)
.get(`/organizations/${fixtures.corpAdmin.organizationId}/members?filter=active`)
for (const m of activeRes.data) {
expect(m.status).toBe('active')
}

TC-CORP-007: Búsqueda de miembros por nombre o email (E2E)

Section titled “TC-CORP-007: Búsqueda de miembros por nombre o email (E2E)”

AC: AC-CORP-004.3 · Prioridad: Media · Tipo: E2E · Estado: 📋 Pendiente Spec: apps/e2e/tests/corporate/members.spec.ts

await loginAs(page, fixtures.corpAdmin)
await page.goto('/corporate/team')
// data-testid a confirmar en OrgMembersPage:
// 'members-search', 'member-row'
const searchInput = page.getByTestId('members-search')
await searchInput.fill(fixtures.corpUser.email.split('@')[0])
await expect(page.getByTestId('member-row')).toHaveCount(1)

TC-CORP-008: Eliminación de miembro — flujo completo

Section titled “TC-CORP-008: Eliminación de miembro — flujo completo”

AC: AC-CORP-005.1 · Prioridad: Alta · Tipo: API · Estado: 📋 Pendiente

// Verificar que el miembro existe antes de eliminar
const beforeRes = await apiClient
.withToken(fixtures.corpAdmin.token)
.get(`/organizations/${fixtures.corpAdmin.organizationId}/members/${fixtures.corpUser.id}`)
expect(beforeRes.status).toBe(200)
// Eliminar
const deleteRes = await apiClient
.withToken(fixtures.corpAdmin.token)
.delete(`/organizations/${fixtures.corpAdmin.organizationId}/members/${fixtures.corpUser.id}`)
expect(deleteRes.status).toBe(204)
// Verificar que ya no aparece en la organización
const afterRes = await apiClient
.withToken(fixtures.corpAdmin.token)
.get(`/organizations/${fixtures.corpAdmin.organizationId}/members/${fixtures.corpUser.id}`)
expect(afterRes.status).toBe(404)

TC-CORP-009: Admin no puede eliminarse a sí mismo

Section titled “TC-CORP-009: Admin no puede eliminarse a sí mismo”

AC: AC-CORP-005.2 · Prioridad: Alta · Tipo: API · Estado: 📋 Pendiente

const res = await apiClient
.withToken(fixtures.corpAdmin.token)
.delete(`/organizations/${fixtures.corpAdmin.organizationId}/members/${fixtures.corpAdmin.id}`)
expect(res.status).toBe(400)

TC-CORP-010: Asignación de path a miembros

Section titled “TC-CORP-010: Asignación de path a miembros”

AC: AC-CORP-006.1 · Prioridad: Alta · Tipo: API · Estado: 📋 Pendiente Spec: apps/e2e/tests/corporate/paths.spec.ts

// Primero añadir el path al catálogo de la organización si no está
await apiClient
.withToken(fixtures.corpAdmin.token)
.post(`/organizations/${fixtures.corpAdmin.organizationId}/paths`, {
pathId: fixtures.testPathId,
})
// Asignar a un miembro
const assignRes = await apiClient
.withToken(fixtures.corpAdmin.token)
.post(`/organizations/${fixtures.corpAdmin.organizationId}/paths/${fixtures.testPathId}/assign`, {
memberIds: [fixtures.corpUser.id],
})
expect(assignRes.status).toBe(201)
expect(assignRes.data.assigned).toBe(1)

TC-CORP-011: Path inexistente devuelve 404

Section titled “TC-CORP-011: Path inexistente devuelve 404”

AC: AC-CORP-006.3 · Prioridad: Media · Tipo: API · Estado: 📋 Pendiente

const res = await apiClient
.withToken(fixtures.corpAdmin.token)
.post(`/organizations/${fixtures.corpAdmin.organizationId}/paths/non-existent-path-id/assign`, {
memberIds: [fixtures.corpUser.id],
})
expect(res.status).toBe(404)

TC-CORP-012: Detalle de miembro incluye paths y actividad reciente

Section titled “TC-CORP-012: Detalle de miembro incluye paths y actividad reciente”

AC: AC-CORP-007.1 · Prioridad: Alta · Tipo: API · Estado: 📋 Pendiente

const res = await apiClient
.withToken(fixtures.corpAdmin.token)
.get(`/organizations/${fixtures.corpAdmin.organizationId}/members/${fixtures.corpUser.id}`)
expect(res.status).toBe(200)
expect(Array.isArray(res.data.paths)).toBe(true)
expect(Array.isArray(res.data.recentActivity)).toBe(true)
for (const path of res.data.paths) {
expect(path).toHaveProperty('id')
expect(path).toHaveProperty('title')
expect(path).toHaveProperty('progress')
}

TC-CORP-013: Nudge rechazado si plan no incluye la feature

Section titled “TC-CORP-013: Nudge rechazado si plan no incluye la feature”

AC: AC-CORP-008.2 · Prioridad: Media · Tipo: API · Estado: 📋 Pendiente

Precondiciones: La organización de test tiene churnPrediction: false en el plan.

const res = await apiClient
.withToken(fixtures.corpAdmin.token)
.post(`/organizations/${fixtures.corpAdmin.organizationId}/members/${fixtures.corpUser.id}/nudge`)
// Debe devolver error de feature no disponible (403 o 422 según implementación)
expect([403, 422]).toContain(res.status)

TC-CORP-014: Banner de seats en rojo al superar 90% (E2E)

Section titled “TC-CORP-014: Banner de seats en rojo al superar 90% (E2E)”

AC: AC-CORP-009.3 · Prioridad: Media · Tipo: E2E · Estado: 📋 Pendiente Spec: apps/e2e/tests/corporate/members.spec.ts

// Requiere organización de test configurada con seats al 91% o más
await loginAs(page, fixtures.corpAdmin)
await page.goto('/corporate/team')
// data-testid a confirmar en OrgMembersPage: 'seats-progress-bar'
const progressBar = page.getByTestId('seats-progress-bar')
const bgColor = await progressBar.evaluate(el =>
window.getComputedStyle(el).backgroundColor
)
// La barra debe estar en rojo cuando supera el 90%
expect(bgColor).toMatch(/rgb\(220,\s*38,\s*38\)/) // var(--red)

TC-CORP-015: Redirección al dashboard para usuario FREE

Section titled “TC-CORP-015: Redirección al dashboard para usuario FREE”

AC: AC-CORP-010.1 · Prioridad: Alta · Tipo: E2E · Estado: 📋 Pendiente Spec: apps/e2e/tests/corporate/access-control.spec.ts

await loginAs(page, fixtures.free)
await page.goto('/corporate/team')
// CorporateGuard redirige a /dashboard
await expect(page).toHaveURL(/\/dashboard/)

TC-CORP-016: API devuelve 403 para STARTER intentando acceder a datos corporativos

Section titled “TC-CORP-016: API devuelve 403 para STARTER intentando acceder a datos corporativos”

AC: AC-CORP-010.2 · Prioridad: Alta · Tipo: API · Estado: 📋 Pendiente Spec: apps/e2e/tests/corporate/access-control.spec.ts

// Usar el token de un usuario STARTER (no CORPORATE_ADMIN)
const res = await apiClient
.withToken(fixtures.starter.token)
.get(`/organizations/any-org-id/members`)
expect(res.status).toBe(403)