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
Módulos API cubiertos
Section titled “Módulos API cubiertos”| Endpoint | Módulo | Descripción | Estado |
|---|---|---|---|
GET /public/user-plans | user-plans | Planes activos con precios y features (público) | ✅ Implementado |
GET /admin/user-plans | user-plans | Lista completa de planes (SUPER_ADMIN) | ✅ Implementado |
PATCH /admin/user-plans/:planId | user-plans | Editar precio, tagline, features y Stripe Price IDs | ✅ Implementado |
GET /users/me/plan | users | Información del plan activo: rol, precio, nextRenewal, facturas | ✅ Implementado |
GET /public/corporate-plans | admin-corporate | Planes corporativos activos para la página de empresa | ✅ Implementado |
POST /admin/corporate/organizations | admin-corporate | Crear organización corporativa (SUPER_ADMIN) | ✅ Implementado |
POST /admin/corporate/organizations/:orgId/change-plan | admin-corporate | Cambiar plan corporativo (SUPER_ADMIN) | ✅ Implementado |
POST /admin/corporate/organizations/:orgId/adjust-seats | admin-corporate | Ajustar número de seats (SUPER_ADMIN) | ✅ Implementado |
POST /organizations/:orgId/invitations | organizations | Invitar miembros al equipo corporativo (CORPORATE_ADMIN) | ✅ Implementado |
GET /organizations/:orgId/billing | organizations | Informació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 |
User Stories
Section titled “User Stories”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
Criterios de Aceptación
Section titled “Criterios de Aceptación”AC-PAY-001: Ver planes disponibles
Section titled “AC-PAY-001: Ver planes disponibles”US: US-PAY-001
AC-PAY-001.1 — Planes públicos retornados sin autenticación
Dado que soy visitante no autenticadoCuando hago GET /public/user-plansEntonces recibo 200 con un array de planes con isActive: trueY cada plan incluye id, name, tagline, monthlyPrice, annualMonthlyPrice y featuresY los planes están ordenados por sortOrderAC-PAY-001.2 — PricingPage muestra los 4 planes activos con toggle mensual/anual
Dado que navego a /pricingCuando la página termina de cargarseEntonces veo los planes activos con sus precios y funcionalidadesY puedo alternar entre facturación mensual y anualY el precio anual muestra el descuento del 20% sobre el mensualAC-PAY-001.3 — Plan inactivo no aparece en la página pública
Dado que un plan tiene isActive: false en BDCuando hago GET /public/user-plansEntonces ese plan no aparece en la respuestaAC-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 asignadoCuando hago GET /users/me/plan con JWT válidoEntonces 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 FREECuando hago GET /users/me/planEntonces recibo { plan: "FREE", nextRenewal: null, price: 0 }AC-PAY-002.3 — Endpoint requiere autenticación
Dado que hago GET /users/me/plan sin token JWTEntonces recibo 401AC-PAY-003: Iniciar checkout en Stripe
Section titled “AC-PAY-003: Iniciar checkout en Stripe”US: US-PAY-003
AC-PAY-003.1 — Usuario FREE es redirigido al registro con plan seleccionado
Dado que soy usuario FREE o visitanteCuando hago clic en "Empezar con Pro" en /pricingEntonces soy redirigido a /auth/register?plan=pro&billing=annualY el plan queda preseleccionado en el flujo de registroAC-PAY-003.2 — Plan FREE redirige directamente al registro sin parámetros de pago
Dado que soy visitanteCuando hago clic en "Empezar gratis"Entonces soy redirigido a /auth/register sin parámetros de plan ni billingAC-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 STARTERCuando el webhook es procesado correctamenteEntonces el usuario pasa a tener role: STARTER en la BDY stripeSubId y subPeriodEnd quedan almacenados en el registro del usuarioAC-PAY-004.2 — Webhook rechaza eventos sin firma válida de Stripe
Dado que recibo un POST a /stripe-webhooks sin la cabecera stripe-signatureO con una firma inválidaEntonces el servidor responde 400Y el evento no se procesaAC-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 STARTERCuando el webhook es procesado correctamenteEntonces el usuario pasa a tener role: FREE en la BDY subPeriodEnd queda marcado como pasado o nuloAC-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ónCuando intenta acceder a un endpoint con accessLevel PAID_STARTEREntonces recibe 403 con { code: "INSUFFICIENT_ACCESS_LEVEL", requiredLevel: "PAID_STARTER" }AC-PAY-006: Ver historial de facturas
Section titled “AC-PAY-006: Ver historial de facturas”US: US-PAY-006
AC-PAY-006.1 — Facturas del plan mostradas en PlanSection
Dado que soy usuario STARTER con historial de pagos en StripeCuando navego a /settings/planEntonces veo la sección "Historial de facturación" con cada facturaY cada fila muestra el plan, el intervalo, la fecha y el importeY hay un enlace al PDF de cada facturaAC-PAY-006.2 — Usuario FREE ve mensaje de historial vacío
Dado que soy usuario FREE sin historial de pagosCuando navego a /settings/planEntonces veo el mensaje "No hay facturas en tu historial"Y no veo ninguna fila en la sección de facturaciónAC-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 200Cuando accedo a contenido PAID_PRO (requiere nivel 3)Entonces recibo 403AC-PAY-007.3 — Usuario no autenticado bloqueado en contenido FREE_REGISTERED
Dado que soy visitante sin JWTCuando accedo a un endpoint con accessLevel FREE_REGISTEREDEntonces 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 200AC-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 403AC-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/invitationsY el invitado recibe el email con el enlace de invitaciónCuando el invitado acepta la invitación (crea cuenta o inicia sesión)Entonces su rol pasa a ser CORPORATE_USERY queda vinculado a la organización correctaAC-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 usarlaEntonces recibe error de invitación no válidaY su rol no cambiaAC-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 activosCuando el CORPORATE_ADMIN intenta enviar una invitación adicionalEntonces recibe un error indicando que se han agotado los seatsAC-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-plansCuando edito el precio mensual y las features del plan PRO y guardoEntonces PATCH /admin/user-plans/PRO retorna 200 con los datos actualizadosY GET /public/user-plans refleja el nuevo precio en la respuesta públicaAC-PAY-010.2 — Endpoint de admin requiere rol SUPER_ADMIN
Dado que soy usuario con rol FREE, STARTER, PRO o ELITECuando intento PATCH /admin/user-plans/:planIdEntonces recibo 403AC-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/billingEntonces recibo 200 con el plan activo, número de seats y estado de facturaciónAC-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 403AC-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_ADMINCuando hago POST /admin/corporate/organizations/:orgId/change-plan con el nuevo planEntonces recibo 200 o 201Y se crea una entrada en AuditLog con action: ORG_PLAN_CHANGEDAC-PAY-012.2 — Ajuste de seats queda registrado en audit log
Dado que soy SUPER_ADMINCuando hago POST /admin/corporate/organizations/:orgId/adjust-seatsEntonces recibo 200Y se crea una entrada en AuditLog con action: ORG_SEATS_ADJUSTEDTest Cases
Section titled “Test Cases”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 activosres.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 sortOrderconst 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úblicaawait page.goto('/pricing')await expect(page.locator('[data-testid="plan-card"]').first()).toBeVisible()
// Toggle de facturaciónawait page.getByRole('button', { name: /anual/i }).click()// El precio anual debe ser menor que el mensual para planes de pagoconst priceText = await page.locator('[data-testid="plan-price-pro"]').textContent()expect(priceText).toBeTruthy()
// CTA del plan FREE redirige al registro sin parámetros de pagoawait 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 Proawait 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 eventoconst 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 BDconst 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ónconst 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íoawait 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 muestranconst invoiceRows = page.locator('[data-testid="invoice-row"]')// La sección siempre debe estar visibleawait 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 JWTconst 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ímiteconst 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 seatsexpect(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_ADMINconst 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 testsawait 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 propioconst 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 denegadoconst 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 logconst 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 logconst 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)