Skip to content

Pagos y Suscripciones

Módulos API: user-plans, users (plan info), admin-corporate, organizations Páginas web: PricingPage (/pricing), PlanSection (/settings/plan), AdminUserPlansPage (/admin/user-plans) Spec Playwright: apps/e2e/tests/payments/payments.spec.ts — 📋 pendiente


EndpointMóduloDescripciónEstado
GET /public/user-plansuser-plansPlanes activos con precios y features (público)✅ Implementado
GET /admin/user-plansuser-plansLista completa de planes (SUPER_ADMIN)✅ Implementado
PATCH /admin/user-plans/:planIduser-plansEditar precio, tagline, features y Stripe Price IDs✅ Implementado
GET /users/me/planusersInformación del plan activo: rol, precio, nextRenewal, facturas✅ Implementado
GET /public/corporate-plansadmin-corporatePlanes corporativos activos para la página de empresa✅ Implementado
POST /admin/corporate/organizationsadmin-corporateCrear organización corporativa (SUPER_ADMIN)✅ Implementado
POST /admin/corporate/organizations/:orgId/change-planadmin-corporateCambiar plan corporativo (SUPER_ADMIN)✅ Implementado
POST /admin/corporate/organizations/:orgId/adjust-seatsadmin-corporateAjustar número de seats (SUPER_ADMIN)✅ Implementado
POST /organizations/:orgId/invitationsorganizationsInvitar miembros al equipo corporativo (CORPORATE_ADMIN)✅ Implementado
GET /organizations/:orgId/billingorganizationsInformación de facturación de la organización✅ Implementado
POST /stripe-webhooks (pendiente)Webhook de Stripe: checkout.session.completed, invoice.payment_failed, customer.subscription.deleted📋 Pendiente

US-PAY-001: Ver planes disponibles con precios y features

Section titled “US-PAY-001: Ver planes disponibles con precios y features”

Como visitante o usuario registrado Quiero ver todos los planes de suscripción con sus precios y funcionalidades incluidas Para comparar opciones y elegir el plan que se adapta a mis necesidades

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


US-PAY-002: Consultar información de mi suscripción activa

Section titled “US-PAY-002: Consultar información de mi suscripción activa”

Como usuario autenticado con plan de pago Quiero ver el estado de mi suscripción, el plan activo y la fecha de próxima renovación Para saber cuándo se renovará y cuánto pagaré

Criterios: AC-PAY-002 · Casos: TC-PAY-003, TC-PAY-004 · Prioridad: Alta · ✅ Implementado


US-PAY-003: Iniciar proceso de checkout para suscribirme a un plan de pago

Section titled “US-PAY-003: Iniciar proceso de checkout para suscribirme a un plan de pago”

Como usuario FREE o no registrado Quiero ser dirigido al proceso de pago al seleccionar un plan de pago en la página de precios Para suscribirme y obtener acceso inmediato al contenido premium

Criterios: AC-PAY-003 · Casos: TC-PAY-005 · Prioridad: Alta · 📋 Stripe pendiente


US-PAY-004: Recibir acceso inmediato al completar el pago

Section titled “US-PAY-004: Recibir acceso inmediato al completar el pago”

Como usuario que acaba de completar un pago en Stripe Quiero que mi rol en la plataforma se actualice inmediatamente Para acceder al contenido premium sin tener que volver a iniciar sesión

Criterios: AC-PAY-004 · Casos: TC-PAY-006 · Prioridad: Alta · 📋 Stripe pendiente


US-PAY-005: Perder acceso premium al expirar la suscripción sin renovar

Section titled “US-PAY-005: Perder acceso premium al expirar la suscripción sin renovar”

Como sistema Quiero que el rol del usuario cambie a FREE cuando su suscripción expira sin renovarse Para garantizar que únicamente los usuarios con suscripción activa accedan a contenido de pago

Criterios: AC-PAY-005 · Casos: TC-PAY-007 · Prioridad: Alta · 📋 Stripe pendiente


US-PAY-006: Ver el historial de facturas de mi suscripción

Section titled “US-PAY-006: Ver el historial de facturas de mi suscripción”

Como usuario con suscripción activa Quiero ver mis facturas pasadas con enlace al PDF Para tener un registro de mis pagos para contabilidad o reembolsos

Criterios: AC-PAY-006 · Casos: TC-PAY-008 · Prioridad: Media · 📋 Stripe pendiente


US-PAY-007: Usuario FREE no puede acceder a contenido de nivel PAID_STARTER o superior

Section titled “US-PAY-007: Usuario FREE no puede acceder a contenido de nivel PAID_STARTER o superior”

Como usuario FREE Cuando intento acceder a un endpoint o contenido con accessLevel PAID_STARTER o superior Entonces recibo un 403 con el código INSUFFICIENT_ACCESS_LEVEL

Criterios: AC-PAY-007 · Casos: TC-PAY-009, TC-PAY-010 · Prioridad: Alta · ✅ Implementado


US-PAY-008: El nivel de acceso CORPORATE otorga el mismo acceso que PAID_PRO

Section titled “US-PAY-008: El nivel de acceso CORPORATE otorga el mismo acceso que PAID_PRO”

Como usuario corporativo (CORPORATE_USER) Quiero poder acceder a contenido marcado como PAID_PRO o inferior Para disfrutar de las funcionalidades incluidas en el plan corporativo de mi empresa

Criterios: AC-PAY-008 · Casos: TC-PAY-011 · Prioridad: Alta · ✅ Implementado


US-PAY-009: Aceptar una invitación corporativa y obtener rol CORPORATE_USER

Section titled “US-PAY-009: Aceptar una invitación corporativa y obtener rol CORPORATE_USER”

Como empleado invitado por un CORPORATE_ADMIN Quiero aceptar la invitación enviada a mi email corporativo Para unirme al equipo y acceder al contenido asignado a mi organización

Criterios: AC-PAY-009 · Casos: TC-PAY-012, TC-PAY-013 · Prioridad: Alta · ✅ Implementado


US-PAY-010: Administrar los planes de usuario desde el panel admin

Section titled “US-PAY-010: Administrar los planes de usuario desde el panel admin”

Como SUPER_ADMIN Quiero editar los precios, taglines y features de cada plan desde /admin/user-plans Para actualizar la oferta comercial sin necesidad de despliegues

Criterios: AC-PAY-010 · Casos: TC-PAY-014, TC-PAY-015 · Prioridad: Media · ✅ Implementado


US-PAY-011: El billing page de una organización muestra el estado del plan y los seats

Section titled “US-PAY-011: El billing page de una organización muestra el estado del plan y los seats”

Como CORPORATE_ADMIN Quiero ver el estado de facturación de mi organización: plan activo, seats usados y facturas Para controlar el gasto y detectar desviaciones antes del cierre de mes

Criterios: AC-PAY-011 · Casos: TC-PAY-016 · Prioridad: Media · ✅ Implementado


US-PAY-012: El SUPER_ADMIN puede cambiar el plan o ajustar seats de cualquier organización

Section titled “US-PAY-012: El SUPER_ADMIN puede cambiar el plan o ajustar seats de cualquier organización”

Como SUPER_ADMIN Quiero poder cambiar el plan corporativo o el número de seats de una organización Para gestionar contratos personalizados o corregir errores de configuración

Criterios: AC-PAY-012 · Casos: TC-PAY-017, TC-PAY-018 · Prioridad: Media · ✅ Implementado


US: US-PAY-001

AC-PAY-001.1 — Planes públicos retornados sin autenticación

Dado que soy visitante no autenticado
Cuando hago GET /public/user-plans
Entonces recibo 200 con un array de planes con isActive: true
Y cada plan incluye id, name, tagline, monthlyPrice, annualMonthlyPrice y features
Y los planes están ordenados por sortOrder

AC-PAY-001.2 — PricingPage muestra los 4 planes activos con toggle mensual/anual

Dado que navego a /pricing
Cuando la página termina de cargarse
Entonces veo los planes activos con sus precios y funcionalidades
Y puedo alternar entre facturación mensual y anual
Y el precio anual muestra el descuento del 20% sobre el mensual

AC-PAY-001.3 — Plan inactivo no aparece en la página pública

Dado que un plan tiene isActive: false en BD
Cuando hago GET /public/user-plans
Entonces ese plan no aparece en la respuesta

AC-PAY-002: Consultar información de suscripción activa

Section titled “AC-PAY-002: Consultar información de suscripción activa”

US: US-PAY-002

AC-PAY-002.1 — Endpoint retorna plan, precio y fecha de renovación

Dado que soy usuario con rol STARTER y stripeSubId asignado
Cuando hago GET /users/me/plan con JWT válido
Entonces recibo 200 con { plan: "STARTER", planName: "Starter", nextRenewal: "<fecha ISO>", features: {...} }

AC-PAY-002.2 — Usuario FREE no tiene fecha de renovación

Dado que soy usuario con rol FREE
Cuando hago GET /users/me/plan
Entonces recibo { plan: "FREE", nextRenewal: null, price: 0 }

AC-PAY-002.3 — Endpoint requiere autenticación

Dado que hago GET /users/me/plan sin token JWT
Entonces recibo 401

US: US-PAY-003

AC-PAY-003.1 — Usuario FREE es redirigido al registro con plan seleccionado

Dado que soy usuario FREE o visitante
Cuando hago clic en "Empezar con Pro" en /pricing
Entonces soy redirigido a /auth/register?plan=pro&billing=annual
Y el plan queda preseleccionado en el flujo de registro

AC-PAY-003.2 — Plan FREE redirige directamente al registro sin parámetros de pago

Dado que soy visitante
Cuando hago clic en "Empezar gratis"
Entonces soy redirigido a /auth/register sin parámetros de plan ni billing

AC-PAY-004: Acceso inmediato tras completar pago

Section titled “AC-PAY-004: Acceso inmediato tras completar pago”

US: US-PAY-004

AC-PAY-004.1 — Webhook checkout.session.completed actualiza el rol del usuario

Dado que Stripe envía el evento checkout.session.completed para el usuario con email "user@test.com"
Y el plan asociado es STARTER
Cuando el webhook es procesado correctamente
Entonces el usuario pasa a tener role: STARTER en la BD
Y stripeSubId y subPeriodEnd quedan almacenados en el registro del usuario

AC-PAY-004.2 — Webhook rechaza eventos sin firma válida de Stripe

Dado que recibo un POST a /stripe-webhooks sin la cabecera stripe-signature
O con una firma inválida
Entonces el servidor responde 400
Y el evento no se procesa

AC-PAY-005: Perder acceso al expirar suscripción

Section titled “AC-PAY-005: Perder acceso al expirar suscripción”

US: US-PAY-005

AC-PAY-005.1 — Webhook customer.subscription.deleted revoca el acceso premium

Dado que Stripe envía customer.subscription.deleted para el stripeSubId de un usuario STARTER
Cuando el webhook es procesado correctamente
Entonces el usuario pasa a tener role: FREE en la BD
Y subPeriodEnd queda marcado como pasado o nulo

AC-PAY-005.2 — Usuario degradado a FREE no puede acceder a contenido PAID_STARTER

Dado que el usuario acaba de ser degradado a rol FREE por expiración
Cuando intenta acceder a un endpoint con accessLevel PAID_STARTER
Entonces recibe 403 con { code: "INSUFFICIENT_ACCESS_LEVEL", requiredLevel: "PAID_STARTER" }

US: US-PAY-006

AC-PAY-006.1 — Facturas del plan mostradas en PlanSection

Dado que soy usuario STARTER con historial de pagos en Stripe
Cuando navego a /settings/plan
Entonces veo la sección "Historial de facturación" con cada factura
Y cada fila muestra el plan, el intervalo, la fecha y el importe
Y hay un enlace al PDF de cada factura

AC-PAY-006.2 — Usuario FREE ve mensaje de historial vacío

Dado que soy usuario FREE sin historial de pagos
Cuando navego a /settings/plan
Entonces veo el mensaje "No hay facturas en tu historial"
Y no veo ninguna fila en la sección de facturación

AC-PAY-007: Bloqueo de acceso por nivel insuficiente

Section titled “AC-PAY-007: Bloqueo de acceso por nivel insuficiente”

US: US-PAY-007

AC-PAY-007.1 — Usuario FREE bloqueado en contenido PAID_STARTER

Dado que soy usuario con rol FREE (nivel jerárquico 1)
Cuando accedo a un endpoint decorado con @RequireAccessLevel(AccessLevel.PAID_STARTER)
Entonces recibo 403 con { code: "INSUFFICIENT_ACCESS_LEVEL", requiredLevel: "PAID_STARTER" }

AC-PAY-007.2 — Usuario STARTER puede acceder a contenido PAID_STARTER pero no a PAID_PRO

Dado que soy usuario con rol STARTER (nivel 2)
Cuando accedo a contenido PAID_STARTER (requiere nivel 2)
Entonces recibo 200
Cuando accedo a contenido PAID_PRO (requiere nivel 3)
Entonces recibo 403

AC-PAY-007.3 — Usuario no autenticado bloqueado en contenido FREE_REGISTERED

Dado que soy visitante sin JWT
Cuando accedo a un endpoint con accessLevel FREE_REGISTERED
Entonces recibo 403 con { code: "ACCESS_DENIED", message: "Authentication required" }

AC-PAY-008: Nivel de acceso del rol CORPORATE_USER

Section titled “AC-PAY-008: Nivel de acceso del rol CORPORATE_USER”

US: US-PAY-008

AC-PAY-008.1 — CORPORATE_USER accede a contenido PAID_PRO

Dado que soy usuario con rol CORPORATE_USER (nivel jerárquico 3)
Cuando accedo a un endpoint con accessLevel PAID_PRO (requiere nivel 3)
Entonces recibo 200

AC-PAY-008.2 — CORPORATE_USER no accede a contenido PAID_ELITE

Dado que soy usuario con rol CORPORATE_USER (nivel 3)
Cuando accedo a un endpoint con accessLevel PAID_ELITE (requiere nivel 4)
Entonces recibo 403

AC-PAY-009: Aceptar invitación corporativa

Section titled “AC-PAY-009: Aceptar invitación corporativa”

US: US-PAY-009

AC-PAY-009.1 — Invitación aceptada asigna rol CORPORATE_USER

Dado que un CORPORATE_ADMIN invita a "nuevo@empresa.com" vía POST /organizations/:orgId/invitations
Y el invitado recibe el email con el enlace de invitación
Cuando el invitado acepta la invitación (crea cuenta o inicia sesión)
Entonces su rol pasa a ser CORPORATE_USER
Y queda vinculado a la organización correcta

AC-PAY-009.2 — Invitación dirigida a email diferente al registrado es rechazada

Dado que la invitación fue enviada a "nuevo@empresa.com"
Cuando un usuario con email "otro@diferente.com" intenta usarla
Entonces recibe error de invitación no válida
Y su rol no cambia

AC-PAY-009.3 — CORPORATE_ADMIN no puede invitar más miembros que los seats disponibles

Dado que la organización tiene 10 seats y 10 miembros activos
Cuando el CORPORATE_ADMIN intenta enviar una invitación adicional
Entonces recibe un error indicando que se han agotado los seats

AC-PAY-010: Administrar planes desde el panel SUPER_ADMIN

Section titled “AC-PAY-010: Administrar planes desde el panel SUPER_ADMIN”

US: US-PAY-010

AC-PAY-010.1 — SUPER_ADMIN puede actualizar precio y features de un plan

Dado que soy SUPER_ADMIN en /admin/user-plans
Cuando edito el precio mensual y las features del plan PRO y guardo
Entonces PATCH /admin/user-plans/PRO retorna 200 con los datos actualizados
Y GET /public/user-plans refleja el nuevo precio en la respuesta pública

AC-PAY-010.2 — Endpoint de admin requiere rol SUPER_ADMIN

Dado que soy usuario con rol FREE, STARTER, PRO o ELITE
Cuando intento PATCH /admin/user-plans/:planId
Entonces recibo 403

AC-PAY-011: Billing de organización corporativa

Section titled “AC-PAY-011: Billing de organización corporativa”

US: US-PAY-011

AC-PAY-011.1 — CORPORATE_ADMIN puede consultar el billing de su organización

Dado que soy CORPORATE_ADMIN de la organización con id "org-123"
Cuando hago GET /organizations/org-123/billing
Entonces recibo 200 con el plan activo, número de seats y estado de facturación

AC-PAY-011.2 — CORPORATE_ADMIN no puede ver el billing de otra organización

Dado que soy CORPORATE_ADMIN de "org-123"
Cuando intento GET /organizations/org-999/billing (organización ajena)
Entonces recibo 403

AC-PAY-012: SUPER_ADMIN gestiona plan y seats corporativos

Section titled “AC-PAY-012: SUPER_ADMIN gestiona plan y seats corporativos”

US: US-PAY-012

AC-PAY-012.1 — Cambio de plan corporativo queda registrado en audit log

Dado que soy SUPER_ADMIN
Cuando hago POST /admin/corporate/organizations/:orgId/change-plan con el nuevo plan
Entonces recibo 200 o 201
Y se crea una entrada en AuditLog con action: ORG_PLAN_CHANGED

AC-PAY-012.2 — Ajuste de seats queda registrado en audit log

Dado que soy SUPER_ADMIN
Cuando hago POST /admin/corporate/organizations/:orgId/adjust-seats
Entonces recibo 200
Y se crea una entrada en AuditLog con action: ORG_SEATS_ADJUSTED

TC-PAY-001: Planes públicos retornados sin autenticación

Section titled “TC-PAY-001: Planes públicos retornados sin autenticación”

AC: AC-PAY-001.1 · Prioridad: Alta · Tipo: API · Estado: 📋 Pendiente Spec: apps/e2e/tests/payments/public-plans.spec.ts

const res = await apiClient.get('/public/user-plans')
expect(res.status).toBe(200)
expect(Array.isArray(res.data)).toBe(true)
// Todos los planes devueltos son activos
res.data.forEach((plan: any) => {
expect(plan.isActive).toBe(true)
expect(plan).toHaveProperty('id')
expect(plan).toHaveProperty('monthlyPrice')
expect(plan).toHaveProperty('annualMonthlyPrice')
expect(plan).toHaveProperty('features')
})
// Orden ascendente por sortOrder
const orders = res.data.map((p: any) => p.sortOrder)
expect(orders).toEqual([...orders].sort((a, b) => a - b))

TC-PAY-002: PricingPage renderiza planes con toggle de facturación

Section titled “TC-PAY-002: PricingPage renderiza planes con toggle de facturación”

AC: AC-PAY-001.2 · Prioridad: Alta · Tipo: E2E · Estado: 📋 Pendiente Spec: apps/e2e/tests/payments/pricing-page.spec.ts

Precondiciones: Al menos 2 planes activos en BD (FREE y un plan de pago)

// Sin autenticación — página pública
await page.goto('/pricing')
await expect(page.locator('[data-testid="plan-card"]').first()).toBeVisible()
// Toggle de facturación
await page.getByRole('button', { name: /anual/i }).click()
// El precio anual debe ser menor que el mensual para planes de pago
const priceText = await page.locator('[data-testid="plan-price-pro"]').textContent()
expect(priceText).toBeTruthy()
// CTA del plan FREE redirige al registro sin parámetros de pago
await page.getByRole('button', { name: /empezar gratis/i }).click()
await expect(page).toHaveURL('/auth/register')

TC-PAY-003: GET /users/me/plan retorna información correcta del plan activo

Section titled “TC-PAY-003: GET /users/me/plan retorna información correcta del plan activo”

AC: AC-PAY-002.1 · Prioridad: Alta · Tipo: API · Estado: 📋 Pendiente Spec: apps/e2e/tests/payments/plan-info.spec.ts

Precondiciones: fixtures.starter tiene role STARTER y subPeriodEnd configurado en BD de test

const res = await apiClient.get('/users/me/plan', {
headers: { Authorization: `Bearer ${fixtures.starter.token}` }
})
expect(res.status).toBe(200)
expect(res.data.plan).toBe('STARTER')
expect(res.data.planName).toBe('Starter')
expect(res.data.nextRenewal).not.toBeNull()
expect(res.data.features).not.toBeNull()
expect(res.data.features).toHaveProperty('aiTutor')

TC-PAY-004: GET /users/me/plan retorna nextRenewal null para usuario FREE

Section titled “TC-PAY-004: GET /users/me/plan retorna nextRenewal null para usuario FREE”

AC: AC-PAY-002.2 · Prioridad: Media · Tipo: API · Estado: 📋 Pendiente

const res = await apiClient.get('/users/me/plan', {
headers: { Authorization: `Bearer ${fixtures.free.token}` }
})
expect(res.status).toBe(200)
expect(res.data.plan).toBe('FREE')
expect(res.data.nextRenewal).toBeNull()
expect(res.data.price).toBe(0)

TC-PAY-005: CTA de plan de pago redirige al registro con parámetros correctos

Section titled “TC-PAY-005: CTA de plan de pago redirige al registro con parámetros correctos”

AC: AC-PAY-003.1 · Prioridad: Alta · Tipo: E2E · Estado: 📋 Pendiente Spec: apps/e2e/tests/payments/pricing-page.spec.ts

await page.goto('/pricing')
// Seleccionar facturación anual (por defecto)
await page.getByRole('button', { name: /anual/i }).click()
// Click en el CTA del plan Pro
await page.getByRole('button', { name: /empezar con pro/i }).click()
await expect(page).toHaveURL(/\/auth\/register\?plan=pro&billing=annual/)

TC-PAY-006: Webhook checkout.session.completed actualiza rol del usuario

Section titled “TC-PAY-006: Webhook checkout.session.completed actualiza rol del usuario”

AC: AC-PAY-004.1 · Prioridad: Alta · Tipo: Integration · Estado: 📋 Pendiente Spec: apps/e2e/tests/payments/stripe-webhooks.spec.ts

Nota: Test de integración — se mockea el evento Stripe con firma válida usando el secret de test configurado en .env.test.

import Stripe from 'stripe'
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY_TEST!, { apiVersion: '2024-06-20' })
// Construir payload del evento
const event = {
type: 'checkout.session.completed',
data: {
object: {
customer_email: fixtures.free.email,
subscription: 'sub_test_123',
metadata: { plan: 'STARTER', interval: 'monthly' },
},
},
}
const payload = JSON.stringify(event)
const signature = stripe.webhooks.generateTestHeaderString({
payload,
secret: process.env.STRIPE_WEBHOOK_SECRET_TEST!,
})
const res = await apiClient.post('/stripe-webhooks', payload, {
headers: { 'stripe-signature': signature, 'Content-Type': 'application/json' },
})
expect(res.status).toBe(200)
// Verificar que el rol cambió en BD
const planRes = await apiClient.get('/users/me/plan', {
headers: { Authorization: `Bearer ${fixtures.free.token}` },
})
expect(planRes.data.plan).toBe('STARTER')
expect(planRes.data.nextRenewal).not.toBeNull()

TC-PAY-007: Webhook customer.subscription.deleted degrada usuario a FREE

Section titled “TC-PAY-007: Webhook customer.subscription.deleted degrada usuario a FREE”

AC: AC-PAY-005.1 · Prioridad: Alta · Tipo: Integration · Estado: 📋 Pendiente Spec: apps/e2e/tests/payments/stripe-webhooks.spec.ts

Nota: Test de integración — requiere que fixtures.starter tenga un stripeSubId asignado en la BD de test.

const event = {
type: 'customer.subscription.deleted',
data: {
object: {
id: fixtures.starter.stripeSubId,
customer: fixtures.starter.stripeCustomerId,
},
},
}
// ... construir firma (igual que TC-PAY-006) ...
const res = await apiClient.post('/stripe-webhooks', payload, {
headers: { 'stripe-signature': signature, 'Content-Type': 'application/json' },
})
expect(res.status).toBe(200)
// Verificar degradación
const planRes = await apiClient.get('/users/me/plan', {
headers: { Authorization: `Bearer ${fixtures.starter.token}` },
})
expect(planRes.data.plan).toBe('FREE')

TC-PAY-008: PlanSection muestra historial de facturas o mensaje vacío

Section titled “TC-PAY-008: PlanSection muestra historial de facturas o mensaje vacío”

AC: AC-PAY-006.1 + AC-PAY-006.2 · Prioridad: Media · Tipo: E2E · Estado: 📋 Pendiente Spec: apps/e2e/tests/payments/plan-section.spec.ts

// Caso: usuario FREE — historial vacío
await loginAs(page, fixtures.free)
await page.goto('/settings/plan')
await expect(page.getByText('No hay facturas en tu historial')).toBeVisible()
// Caso: usuario STARTER con facturas (requiere datos en BD de test)
await loginAs(page, fixtures.starter)
await page.goto('/settings/plan')
// Si hay facturas en BD de test, verificar que se muestran
const invoiceRows = page.locator('[data-testid="invoice-row"]')
// La sección siempre debe estar visible
await expect(page.getByText('Historial de facturación')).toBeVisible()

TC-PAY-009: Usuario FREE bloqueado en endpoint PAID_STARTER

Section titled “TC-PAY-009: Usuario FREE bloqueado en endpoint PAID_STARTER”

AC: AC-PAY-007.1 · Prioridad: Alta · Tipo: API · Estado: 📋 Pendiente Spec: apps/e2e/tests/access-control/payment-access.spec.ts

Nota: Este test es BLOCKING en Bitbucket Pipelines. Cualquier fallo bloquea el merge.

// Endpoint real de ejemplo — ajustar a un endpoint real con @RequireAccessLevel(PAID_STARTER)
const res = await apiClient.get('/lessons/some-paid-lesson-id', {
headers: { Authorization: `Bearer ${fixtures.free.token}` }
})
expect(res.status).toBe(403)
expect(res.data.code).toBe('INSUFFICIENT_ACCESS_LEVEL')
expect(res.data.requiredLevel).toBe('PAID_STARTER')

TC-PAY-010: Usuario no autenticado bloqueado en contenido FREE_REGISTERED

Section titled “TC-PAY-010: Usuario no autenticado bloqueado en contenido FREE_REGISTERED”

AC: AC-PAY-007.3 · Prioridad: Alta · Tipo: API · Estado: 📋 Pendiente Spec: apps/e2e/tests/access-control/payment-access.spec.ts

// Sin token JWT
const res = await apiClient.get('/lessons/some-free-registered-lesson')
expect(res.status).toBe(403)
expect(res.data.code).toBe('ACCESS_DENIED')

TC-PAY-011: CORPORATE_USER accede a contenido PAID_PRO

Section titled “TC-PAY-011: CORPORATE_USER accede a contenido PAID_PRO”

AC: AC-PAY-008.1 · Prioridad: Alta · Tipo: API · Estado: 📋 Pendiente Spec: apps/e2e/tests/access-control/payment-access.spec.ts

// fixtures.corpUser → rol CORPORATE_USER (nivel 3 en ACCESS_HIERARCHY)
const res = await apiClient.get('/lessons/some-pro-lesson-id', {
headers: { Authorization: `Bearer ${fixtures.corpUser.token}` }
})
expect(res.status).toBe(200)
// CORPORATE_USER no accede a PAID_ELITE (nivel 4)
const eliteRes = await apiClient.get('/lessons/some-elite-lesson-id', {
headers: { Authorization: `Bearer ${fixtures.corpUser.token}` }
})
expect(eliteRes.status).toBe(403)

TC-PAY-012: CORPORATE_ADMIN puede invitar miembros a la organización

Section titled “TC-PAY-012: CORPORATE_ADMIN puede invitar miembros a la organización”

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

Precondiciones: fixtures.corpAdmin es CORPORATE_ADMIN de una organización con seats disponibles

const res = await apiClient.post(
`/organizations/${fixtures.corpAdmin.organizationId}/invitations`,
{ emails: [`qa-invite-${Date.now()}@test.com`], role: 'CORPORATE_USER' },
{ headers: { Authorization: `Bearer ${fixtures.corpAdmin.token}` } }
)
expect(res.status).toBe(201)
expect(res.data).toHaveProperty('invitations')
expect(res.data.invitations.length).toBeGreaterThan(0)

TC-PAY-013: Invitación corporativa rechazada si no quedan seats

Section titled “TC-PAY-013: Invitación corporativa rechazada si no quedan seats”

AC: AC-PAY-009.3 · Prioridad: Media · Tipo: API · Estado: 📋 Pendiente

// Requiere organización en BD de test con seats al límite
const res = await apiClient.post(
`/organizations/${fixtures.corpAdminFull.organizationId}/invitations`,
{ emails: ['overflow@test.com'] },
{ headers: { Authorization: `Bearer ${fixtures.corpAdminFull.token}` } }
)
expect(res.status).toBeGreaterThanOrEqual(400)
// Verificar que el mensaje alude a los seats
expect(JSON.stringify(res.data)).toMatch(/seat/i)

TC-PAY-014: SUPER_ADMIN actualiza el precio de un plan

Section titled “TC-PAY-014: SUPER_ADMIN actualiza el precio de un plan”

AC: AC-PAY-010.1 · Prioridad: Media · Tipo: API · Estado: 📋 Pendiente Spec: apps/e2e/tests/payments/admin-plans.spec.ts

// fixtures.superAdmin → rol SUPER_ADMIN
const originalRes = await apiClient.get('/admin/user-plans', {
headers: { Authorization: `Bearer ${fixtures.superAdmin.token}` }
})
const proPlan = originalRes.data.find((p: any) => p.id === 'PRO')
const originalPrice = proPlan.monthlyPrice
const patchRes = await apiClient.patch(
`/admin/user-plans/PRO`,
{ monthlyPrice: '29.99' },
{ headers: { Authorization: `Bearer ${fixtures.superAdmin.token}` } }
)
expect(patchRes.status).toBe(200)
expect(patchRes.data.monthlyPrice).toBe('29.99')
// Verificar que el precio público también cambió
const publicRes = await apiClient.get('/public/user-plans')
const publicPro = publicRes.data.find((p: any) => p.id === 'PRO')
expect(publicPro.monthlyPrice).toBe('29.99')
// Restaurar precio original para no contaminar otros tests
await apiClient.patch('/admin/user-plans/PRO',
{ monthlyPrice: originalPrice },
{ headers: { Authorization: `Bearer ${fixtures.superAdmin.token}` } }
)

TC-PAY-015: Non-SUPER_ADMIN no puede modificar planes

Section titled “TC-PAY-015: Non-SUPER_ADMIN no puede modificar planes”

AC: AC-PAY-010.2 · Prioridad: Alta · Tipo: API · Estado: 📋 Pendiente

for (const fixture of [fixtures.free, fixtures.starter, fixtures.pro, fixtures.elite]) {
const res = await apiClient.patch(
'/admin/user-plans/PRO',
{ monthlyPrice: '999' },
{ headers: { Authorization: `Bearer ${fixture.token}` } }
)
expect(res.status).toBe(403)
}

TC-PAY-016: CORPORATE_ADMIN consulta el billing de su propia organización

Section titled “TC-PAY-016: CORPORATE_ADMIN consulta el billing de su propia organización”

AC: AC-PAY-011.1 + AC-PAY-011.2 · Prioridad: Media · Tipo: API · Estado: 📋 Pendiente Spec: apps/e2e/tests/payments/corporate-billing.spec.ts

// Acceso propio
const ownRes = await apiClient.get(
`/organizations/${fixtures.corpAdmin.organizationId}/billing`,
{ headers: { Authorization: `Bearer ${fixtures.corpAdmin.token}` } }
)
expect(ownRes.status).toBe(200)
// Acceso a organización ajena denegado
const otherOrgId = 'some-other-org-id'
const foreignRes = await apiClient.get(
`/organizations/${otherOrgId}/billing`,
{ headers: { Authorization: `Bearer ${fixtures.corpAdmin.token}` } }
)
expect(foreignRes.status).toBe(403)

TC-PAY-017: SUPER_ADMIN cambia el plan de una organización

Section titled “TC-PAY-017: SUPER_ADMIN cambia el plan de una organización”

AC: AC-PAY-012.1 · Prioridad: Media · Tipo: API · Estado: 📋 Pendiente Spec: apps/e2e/tests/payments/admin-corporate.spec.ts

const res = await apiClient.post(
`/admin/corporate/organizations/${fixtures.testOrg.id}/change-plan`,
{ plan: 'BUSINESS', seats: 50 },
{ headers: { Authorization: `Bearer ${fixtures.superAdmin.token}` } }
)
expect(res.status).toBe(200)
// Verificar audit log
const auditRes = await apiClient.get(
`/admin/audit-logs?entityId=${fixtures.testOrg.id}&action=ORG_PLAN_CHANGED`,
{ headers: { Authorization: `Bearer ${fixtures.superAdmin.token}` } }
)
expect(auditRes.data.items.length).toBeGreaterThan(0)

TC-PAY-018: SUPER_ADMIN ajusta los seats de una organización

Section titled “TC-PAY-018: SUPER_ADMIN ajusta los seats de una organización”

AC: AC-PAY-012.2 · Prioridad: Media · Tipo: API · Estado: 📋 Pendiente Spec: apps/e2e/tests/payments/admin-corporate.spec.ts

const res = await apiClient.post(
`/admin/corporate/organizations/${fixtures.testOrg.id}/adjust-seats`,
{ seats: 25 },
{ headers: { Authorization: `Bearer ${fixtures.superAdmin.token}` } }
)
expect(res.status).toBe(200)
// Verificar audit log
const auditRes = await apiClient.get(
`/admin/audit-logs?entityId=${fixtures.testOrg.id}&action=ORG_SEATS_ADJUSTED`,
{ headers: { Authorization: `Bearer ${fixtures.superAdmin.token}` } }
)
expect(auditRes.data.items.length).toBeGreaterThan(0)