Earnings y Comisiones
Módulo API: creator-earnings
Controladores: CreatorEarningsController, AdminPayoutsController, PublicEarningsController
Páginas web: CreatorEarningsPage (/creator/earnings)
RTK Query slice: apps/web/src/store/api/creatorEarningsApi.ts
Spec Playwright: apps/e2e/tests/creator/earnings.spec.ts — 📋 pendiente de crear
Endpoints reales del módulo
Section titled “Endpoints reales del módulo”| Método | Ruta | Roles | Descripción |
|---|---|---|---|
GET | /creator/me/earnings/preview | CONTENT_ADMIN, SUPER_ADMIN | Preview del mes en curso (actualizado diariamente a las 03:15) |
GET | /creator/me/earnings | CONTENT_ADMIN, SUPER_ADMIN | Historial paginado de períodos cerrados |
GET | /creator/me/earnings/:periodMonth | CONTENT_ADMIN, SUPER_ADMIN | Detalle de un período cerrado con desglose por ruta |
PATCH | /creator/me/payment-data-note | CONTENT_ADMIN, SUPER_ADMIN | Actualizar referencia fiscal para Contabilidad |
PATCH | /creator/me/accept-earnings-terms | CONTENT_ADMIN, SUPER_ADMIN | Aceptar los términos del programa de ingresos |
GET | /public/creator-earnings/stats | Público (sin auth) | Estadísticas agregadas de la plataforma |
GET | /admin/creator-payouts/periods | SUPER_ADMIN | Listar todos los períodos de liquidación |
GET | /admin/creator-payouts/periods/:periodMonth | SUPER_ADMIN | Detalle de un período con todos los earnings |
PATCH | /admin/creator-payouts/periods/:periodMonth | SUPER_ADMIN | Actualizar configuración del período (grossRevenue, fees, pool %) |
POST | /admin/creator-payouts/periods/:periodMonth/close | SUPER_ADMIN | Cerrar y bloquear un período manualmente |
POST | /admin/creator-payouts/periods/:periodMonth/unlock | SUPER_ADMIN | Desbloquear un período cerrado (con razón obligatoria) |
PATCH | /admin/creator-payouts/earnings/:earningId/payment-status | SUPER_ADMIN | Actualizar estado de pago de un creador |
GET | /admin/creator-payouts/periods/:periodMonth/export | SUPER_ADMIN | Exportar período como CSV |
GET | /admin/creator-payouts/audit-log | SUPER_ADMIN | Log de cambios de estado de un período |
POST | /admin/creator-payouts/jobs/run-preview | SUPER_ADMIN | Disparar el job de preview manualmente |
Reglas de negocio
Section titled “Reglas de negocio”Fórmula de puntuación
Section titled “Fórmula de puntuación”score = completions × 0.5 + minutesConsumed × 0.3 + weightedRating × 0.2participationPct = score / totalScoreEligibleCreatorsgrossAmountEur = creatorPoolAmount × participationPctElegibilidad mensual
Section titled “Elegibilidad mensual”Un creador es elegible para cobro si cumple todos:
- Perfil no suspendido (
suspendedAt == null) - Al menos 1 ruta publicada
- Al menos 5 completados de lecciones en el período (configurable por
minCompletionsForEligibility)
Motivos de inelegibilidad devueltos por la API: PROFILE_SUSPENDED, NO_PUBLISHED_PATHS, INSUFFICIENT_COMPLETIONS.
Ciclo de vida de un período
Section titled “Ciclo de vida de un período”- El job
earnings-previewcorre cada día a las 03:15 y actualizaCreatorEarningPreviewcon datos del mes en curso. - El job
earnings-closurecorre el día 2 de cada mes a las 06:00 y cierra el mes anterior: calcula earnings finales, creaCreatorEarningLinepor ruta, bloquea elPlatformRevenueConfig(isLocked = true). - El SUPER_ADMIN puede cerrar un período manualmente o desbloquearlo (con razón registrada en
adminNotes).
Estados de pago (CreatorPaymentStatus)
Section titled “Estados de pago (CreatorPaymentStatus)”| Estado | Descripción |
|---|---|
PENDING_REVIEW | Estado inicial tras cierre del período |
IN_REVIEW | El equipo de Contabilidad está procesando |
APPROVED | Pago aprobado, pendiente de transferencia |
PAID | Transferencia realizada |
ON_HOLD | En espera por incidencia |
REJECTED | Pago rechazado (motivo en paymentNotes) |
Seguridad del campo paymentDataNote
Section titled “Seguridad del campo paymentDataNote”El servicio rechaza cualquier nota que contenga un IBAN completo (regex ^[A-Z]{2}\d{2}...) o un número de tarjeta de 16 dígitos consecutivos. Devuelve 422 UnprocessableEntity.
Prerequisito — aceptación de términos
Section titled “Prerequisito — aceptación de términos”El endpoint GET /creator/me/earnings/preview devuelve 403 { code: 'EARNINGS_TERMS_NOT_ACCEPTED' } si el creador no ha aceptado los términos previamente. El frontend muestra un modal bloqueante que requiere la aceptación explícita.
Hitos de ganancias acumuladas
Section titled “Hitos de ganancias acumuladas”El job de cierre mensual compara las ganancias acumuladas históricas con los hitos [100, 500, 1000, 5000, 10000] EUR. Si se cruza un hito, se dispara NotificationService.notifyEarningsMilestone() (fire-and-forget).
User Stories
Section titled “User Stories”US-EARN-001: Aceptar términos del programa de ingresos
Section titled “US-EARN-001: Aceptar términos del programa de ingresos”Como creador de contenido quiero ver y aceptar los términos del programa de ingresos para desbloquear el acceso a mi panel de ganancias.
Módulo: creator-earnings · Endpoint: PATCH /creator/me/accept-earnings-terms
Estado: ✅ Implementado
Prioridad: Alta
AC-EARN-001.1 — Panel bloqueado antes de aceptar términos
Section titled “AC-EARN-001.1 — Panel bloqueado antes de aceptar términos”Given que soy un CONTENT_ADMIN que NO ha aceptado los términosWhen navego a /creator/earnings y se llama GET /creator/me/earnings/previewThen la API devuelve 403 con code "EARNINGS_TERMS_NOT_ACCEPTED"And el frontend muestra un modal bloqueante con el disclaimer del programaAnd el modal contiene un botón "Acepto los términos"AC-EARN-001.2 — Aceptación exitosa desbloquea el panel
Section titled “AC-EARN-001.2 — Aceptación exitosa desbloquea el panel”Given que el modal de términos está visibleWhen hago clic en "Acepto los términos"Then la API llama PATCH /creator/me/accept-earnings-termsAnd la respuesta es 200 { ok: true, acceptedAt: "..." }And el modal se cierra y el panel de ingresos carga correctamenteAC-EARN-001.3 — Acceso directo al panel para creadores que ya aceptaron
Section titled “AC-EARN-001.3 — Acceso directo al panel para creadores que ya aceptaron”Given que soy un CONTENT_ADMIN que ya tiene earningsTermsAcceptedAt registradoWhen navego a /creator/earningsThen el panel carga directamente sin mostrar el modal de términosUS-EARN-002: Visualizar el preview del mes en curso
Section titled “US-EARN-002: Visualizar el preview del mes en curso”Como creador de contenido quiero ver mi estimación de ingresos del mes actual para saber si soy elegible y qué puedo esperar cobrar al cierre del período.
Módulo: creator-earnings · Endpoint: GET /creator/me/earnings/preview
Estado: ✅ Implementado
Prioridad: Alta
AC-EARN-002.1 — KPIs del mes actual visibles
Section titled “AC-EARN-002.1 — KPIs del mes actual visibles”Given que soy un creador elegible con al menos 5 completados en el mesWhen cargo el panel de ingresos en /creator/earningsThen veo los KPIs: completados únicos, minutos consumidos, valoración media ponderada y estimado bruto en EURAnd el banner de elegibilidad muestra "Elegible para pago este mes"And se muestra el pool estimado y mi porcentaje de participaciónAC-EARN-002.2 — Banner de inelegibilidad con motivo
Section titled “AC-EARN-002.2 — Banner de inelegibilidad con motivo”Given que soy un creador con solo 3 completados en el mes (< 5 requeridos)When cargo el panel de ingresosThen el banner muestra "No elegible para pago este mes"And el motivo visible es "Necesitas al menos 5 completados este mes"And el campo estimado bruto muestra "—"AC-EARN-002.3 — Preview vacío cuando el job aún no ha corrido
Section titled “AC-EARN-002.3 — Preview vacío cuando el job aún no ha corrido”Given que el job earnings-preview no ha corrido todavía este mesWhen cargo el panel de ingresosThen se muestra el banner de disclaimerAnd los KPIs muestran 0And el mensaje de ineligibilidad explica "Los datos del mes actual se calculan diariamente a las 03:15 AM."AC-EARN-002.4 — Disclaimer siempre visible
Section titled “AC-EARN-002.4 — Disclaimer siempre visible”Given que cargo el panel con cualquier estado de previewWhen visualizo la páginaThen siempre hay un banner amarillo con el texto del disclaimer legalAnd al final de la página hay un bloque "Aviso legal" con el mismo textoUS-EARN-003: Consultar el historial de períodos cerrados
Section titled “US-EARN-003: Consultar el historial de períodos cerrados”Como creador de contenido quiero ver el historial de todos mis períodos cerrados con su estado de pago para llevar un seguimiento de mis ingresos acumulados.
Módulo: creator-earnings · Endpoint: GET /creator/me/earnings
Estado: ✅ Implementado
Prioridad: Alta
AC-EARN-003.1 — Tabla de historial con paginación
Section titled “AC-EARN-003.1 — Tabla de historial con paginación”Given que tengo al menos 1 período cerrado en mi historialWhen cargo el panel de ingresosThen veo la tabla "Historial de períodos" con columnas: Período, Completados, Participación, Monto bruto, Estado pagoAnd los períodos están ordenados de más reciente a más antiguoAnd el período más reciente aparece primeroAC-EARN-003.2 — Período inelegible aparece atenuado
Section titled “AC-EARN-003.2 — Período inelegible aparece atenuado”Given que tengo un período cerrado donde no era elegible (INSUFFICIENT_COMPLETIONS)When visualizo la tabla de historialThen esa fila aparece con opacidad reducida (0.6)And en la columna "Monto bruto" aparece "—"And en la columna "Estado pago" aparece "No elegible" en lugar de un chip de estadoAC-EARN-003.3 — Chips de estado de pago con color correcto
Section titled “AC-EARN-003.3 — Chips de estado de pago con color correcto”Given que tengo períodos con distintos estados de pagoWhen veo la tabla de historialThen PENDING_REVIEW muestra chip gris "En revisión"And IN_REVIEW muestra chip amarillo "Procesando"And APPROVED muestra chip azul "Aprobado"And PAID muestra chip verde "✓ Pagado"And ON_HOLD muestra chip amarillo pálido "En espera"And REJECTED muestra chip rojo "Rechazado"US-EARN-004: Ver el detalle de un período cerrado con desglose por ruta
Section titled “US-EARN-004: Ver el detalle de un período cerrado con desglose por ruta”Como creador de contenido quiero ver el desglose de mis ingresos por cada ruta para un período concreto para entender qué rutas contribuyen más a mis ganancias.
Módulo: creator-earnings · Endpoint: GET /creator/me/earnings/:periodMonth
Estado: ✅ Implementado
Prioridad: Media
AC-EARN-004.1 — Modal de detalle al clicar una fila
Section titled “AC-EARN-004.1 — Modal de detalle al clicar una fila”Given que visualizo la tabla de historial de períodosWhen hago clic en una fila de un período elegibleThen se abre un modal con el título "Detalle — [NombreMes Año]"And el modal muestra una tabla con las rutas: nombre, completados, minutos, valoración, contribución %, monto brutoAC-EARN-004.2 — Notas del equipo visibles si existen
Section titled “AC-EARN-004.2 — Notas del equipo visibles si existen”Given que el SUPER_ADMIN ha añadido paymentNotes a mi earning de ese períodoWhen abro el modal de detalle del períodoThen aparece la sección "Notas del equipo" con el texto guardadoAC-EARN-004.3 — Rutas con cero completados no aparecen en el desglose
Section titled “AC-EARN-004.3 — Rutas con cero completados no aparecen en el desglose”Given que tengo 3 rutas publicadas pero solo 2 tuvieron completados ese mesWhen abro el detalle del períodoThen solo aparecen 2 líneas en la tabla (las rutas con completions > 0)US-EARN-005: Registrar datos fiscales para el equipo de Contabilidad
Section titled “US-EARN-005: Registrar datos fiscales para el equipo de Contabilidad”Como creador de contenido quiero introducir mi referencia fiscal (NIF/NIE/CIF, razón social) para que el equipo de Contabilidad pueda identificarme al procesar el pago.
Módulo: creator-earnings · Endpoint: PATCH /creator/me/payment-data-note
Estado: ✅ Implementado
Prioridad: Alta
AC-EARN-005.1 — Guardado exitoso de referencia fiscal
Section titled “AC-EARN-005.1 — Guardado exitoso de referencia fiscal”Given que soy un creador elegible y abro la sección "Datos para Contabilidad"When introduzco "NIF 12345678A · Autónomo/a · España" y pulso GuardarThen la API recibe PATCH /creator/me/payment-data-note con { note: "NIF 12345678A · Autónomo/a · España" }And la respuesta es 200 { ok: true }AC-EARN-005.2 — Rechazo de IBAN completo en la nota
Section titled “AC-EARN-005.2 — Rechazo de IBAN completo en la nota”Given que introduzco una nota que contiene un IBAN válido (ej. "ES9121000418450200051332")When pulso GuardarThen la API devuelve 422 Unprocessable EntityAnd el mensaje de error indica que no se pueden incluir IBANs o números de tarjetaAC-EARN-005.3 — Rechazo de número de tarjeta de 16 dígitos
Section titled “AC-EARN-005.3 — Rechazo de número de tarjeta de 16 dígitos”Given que introduzco una nota con "4242424242424242" (número de tarjeta)When pulso GuardarThen la API devuelve 422 Unprocessable EntityAC-EARN-005.4 — Límite de 500 caracteres
Section titled “AC-EARN-005.4 — Límite de 500 caracteres”Given que el campo de nota tiene un límite de 500 caracteresWhen el texto supera 500 caracteresThen el botón Guardar permanece desactivado o el campo bloquea la entrada adicionalUS-EARN-006: Control de acceso — solo CONTENT_ADMIN y SUPER_ADMIN
Section titled “US-EARN-006: Control de acceso — solo CONTENT_ADMIN y SUPER_ADMIN”Como sistema de seguridad quiero que los endpoints de earnings del creador estén protegidos por rol para que usuarios estudiantes no puedan acceder a datos financieros de los creadores.
Módulo: creator-earnings · Guards: JwtAuthGuard, RolesGuard
Estado: ✅ Implementado
Prioridad: Alta (candidato a BLOCKING)
AC-EARN-006.1 — Usuario no autenticado recibe 401
Section titled “AC-EARN-006.1 — Usuario no autenticado recibe 401”Given que hago una petición sin token JWTWhen llamo a GET /creator/me/earnings/previewThen la respuesta es 401 UnauthorizedAC-EARN-006.2 — Usuario FREE recibe 403
Section titled “AC-EARN-006.2 — Usuario FREE recibe 403”Given que tengo un token JWT de un usuario con role FREEWhen llamo a GET /creator/me/earningsThen la respuesta es 403 ForbiddenAC-EARN-006.3 — CONTENT_ADMIN puede acceder a sus propios earnings
Section titled “AC-EARN-006.3 — CONTENT_ADMIN puede acceder a sus propios earnings”Given que tengo un token JWT de un usuario con role CONTENT_ADMINWhen llamo a GET /creator/me/earningsThen la respuesta es 200 con sus datos de earningsAnd los datos corresponden únicamente al perfil del creador autenticadoAC-EARN-006.4 — Endpoints de admin solo accesibles para SUPER_ADMIN
Section titled “AC-EARN-006.4 — Endpoints de admin solo accesibles para SUPER_ADMIN”Given que tengo un token JWT de un usuario con role CONTENT_ADMINWhen llamo a GET /admin/creator-payouts/periodsThen la respuesta es 403 ForbiddenUS-EARN-007: Gestión de períodos de liquidación (SUPER_ADMIN)
Section titled “US-EARN-007: Gestión de períodos de liquidación (SUPER_ADMIN)”Como SUPER_ADMIN quiero poder ver, configurar y cerrar períodos de liquidación para controlar el proceso de pago a los creadores.
Módulo: creator-earnings · Controlador: AdminPayoutsController
Estado: ✅ Implementado
Prioridad: Alta
AC-EARN-007.1 — Listar períodos con resumen
Section titled “AC-EARN-007.1 — Listar períodos con resumen”Given que soy un SUPER_ADMIN autenticadoWhen llamo a GET /admin/creator-payouts/periodsThen recibo un array de configuraciones de período ordenadas de más reciente a más antiguaAnd cada elemento incluye: periodMonth, poolPercentage, isLocked, grossRevenue, creatorPoolAmount, earningsCountAC-EARN-007.2 — Cierre manual de período
Section titled “AC-EARN-007.2 — Cierre manual de período”Given que existe un PlatformRevenueConfig para "2026-04" con isLocked: false y creatorPoolAmount: 5000When el SUPER_ADMIN llama a POST /admin/creator-payouts/periods/2026-04/closeThen el sistema calcula earnings para todos los creadores públicos con el pool de 5000 EURAnd el período queda bloqueado (isLocked: true)And la respuesta incluye { ok: true, periodMonth: "2026-04", eligibleCount: N }AC-EARN-007.3 — No se puede cerrar un período ya bloqueado
Section titled “AC-EARN-007.3 — No se puede cerrar un período ya bloqueado”Given que el período "2026-03" ya tiene isLocked: trueWhen el SUPER_ADMIN intenta llamar a POST /admin/creator-payouts/periods/2026-03/closeThen la respuesta es 400 Bad Request con mensaje "Period 2026-03 is already locked"AC-EARN-007.4 — Desbloqueo de período con razón obligatoria
Section titled “AC-EARN-007.4 — Desbloqueo de período con razón obligatoria”Given que el período "2026-03" está bloqueadoWhen el SUPER_ADMIN llama a POST /admin/creator-payouts/periods/2026-03/unlock con { reason: "" }Then la respuesta es 403 Forbidden con mensaje "A reason is required to unlock a period"AC-EARN-007.5 — No modificar período bloqueado
Section titled “AC-EARN-007.5 — No modificar período bloqueado”Given que el período "2026-03" está bloqueadoWhen el SUPER_ADMIN intenta PATCH /admin/creator-payouts/periods/2026-03 con nuevos valores de grossRevenueThen la respuesta es 400 Bad Request con mensaje "Period 2026-03 is locked and cannot be modified"US-EARN-008: Actualizar estado de pago por creador (SUPER_ADMIN)
Section titled “US-EARN-008: Actualizar estado de pago por creador (SUPER_ADMIN)”Como SUPER_ADMIN quiero actualizar el estado de pago de cada creador en un período para reflejar el progreso real del proceso de transferencia.
Módulo: creator-earnings · Endpoint: PATCH /admin/creator-payouts/earnings/:earningId/payment-status
Estado: ✅ Implementado
Prioridad: Alta
AC-EARN-008.1 — Actualización de estado con notas
Section titled “AC-EARN-008.1 — Actualización de estado con notas”Given que existe un earning con id "earning-123" en estado PENDING_REVIEWWhen el SUPER_ADMIN llama a PATCH /admin/creator-payouts/earnings/earning-123/payment-status con { status: "PAID", notes: "Transferencia SEPA realizada el 2026-05-15" }Then el earning queda con paymentStatus: "PAID" y paymentNotes actualizadasAnd la respuesta incluye paymentStatusUpdatedAt y paymentStatusUpdatedByAC-EARN-008.2 — Estado inválido es rechazado
Section titled “AC-EARN-008.2 — Estado inválido es rechazado”Given que el SUPER_ADMIN envía un status desconocidoWhen llama a PATCH /admin/creator-payouts/earnings/:id/payment-status con { status: "TRANSFERRED" }Then la respuesta es 400 Bad Request por validación del enum CreatorPaymentStatusUS-EARN-009: Exportar período de liquidación como CSV
Section titled “US-EARN-009: Exportar período de liquidación como CSV”Como SUPER_ADMIN quiero exportar el listado de earnings de un período a CSV para procesarlo con el equipo de Contabilidad.
Módulo: creator-earnings · Endpoint: GET /admin/creator-payouts/periods/:periodMonth/export
Estado: ✅ Implementado
Prioridad: Media
AC-EARN-009.1 — CSV descargable con cabeceras correctas
Section titled “AC-EARN-009.1 — CSV descargable con cabeceras correctas”Given que existe el período "2026-04" con creadores procesadosWhen el SUPER_ADMIN llama a GET /admin/creator-payouts/periods/2026-04/exportThen la respuesta tiene Content-Type: text/csvAnd el header Content-Disposition incluye filename="creator-earnings-2026-04.csv"And el CSV contiene las columnas: periodMonth, creatorId, displayName, email, grossAmountEur, paymentStatus, completionsCount, minutesConsumed, weightedRating, participationPct, paymentDataNote, paymentNotesAC-EARN-009.2 — Se registra exportedAt en cada earning
Section titled “AC-EARN-009.2 — Se registra exportedAt en cada earning”Given que el SUPER_ADMIN exporta el período "2026-04"When la descarga se completaThen todos los CreatorMonthlyEarning de ese período tienen exportedAt y exportedBy actualizadosUS-EARN-010: Estadísticas públicas del programa de ingresos
Section titled “US-EARN-010: Estadísticas públicas del programa de ingresos”Como visitante de la plataforma (sin auth) quiero ver estadísticas reales del programa de ingresos para evaluar si vale la pena convertirme en creador.
Módulo: creator-earnings · Endpoint: GET /public/creator-earnings/stats
Estado: ✅ Implementado
Prioridad: Media
AC-EARN-010.1 — Estadísticas del último período bloqueado
Section titled “AC-EARN-010.1 — Estadísticas del último período bloqueado”Given que existe al menos un período de liquidación bloqueado con creadores pagadosWhen llamo a GET /public/creator-earnings/stats sin autenticaciónThen la respuesta es 200 con { avgMonthlyEarningsTopCreator, totalPaidToCreators, activeCreatorsCount, poolPercentage }And totalPaidToCreators refleja la suma histórica de earnings con paymentStatus = "PAID"And activeCreatorsCount incluye creadores con completionsCount > 0 en los últimos 3 mesesAC-EARN-010.2 — Respuesta vacía cuando no hay períodos cerrados
Section titled “AC-EARN-010.2 — Respuesta vacía cuando no hay períodos cerrados”Given que no existe ningún PlatformRevenueConfig bloqueadoWhen llamo a GET /public/creator-earnings/statsThen la respuesta es 200 con todos los valores a 0 y poolPercentage: 0.25Test Cases
Section titled “Test Cases”TC-EARN-001 — Control de acceso: 401 sin auth en endpoints del creador
Section titled “TC-EARN-001 — Control de acceso: 401 sin auth en endpoints del creador”Cubre: AC-EARN-006.1 Tipo: E2E / API
import { test, expect } from '@playwright/test'import { apiClient } from '../../utils/api-client'
test.describe('Creator Earnings — Access Control', () => { test('TC-EARN-001: GET /creator/me/earnings/preview returns 401 without auth', async () => { const res = await apiClient.get('/creator/me/earnings/preview') expect(res.status).toBe(401) })
test('TC-EARN-001b: GET /creator/me/earnings returns 401 without auth', async () => { const res = await apiClient.get('/creator/me/earnings') expect(res.status).toBe(401) })
test('TC-EARN-001c: PATCH /creator/me/accept-earnings-terms returns 401 without auth', async () => { const res = await apiClient.patch('/creator/me/accept-earnings-terms') expect(res.status).toBe(401) })})TC-EARN-002 — Control de acceso: 403 para role FREE
Section titled “TC-EARN-002 — Control de acceso: 403 para role FREE”Cubre: AC-EARN-006.2 Tipo: E2E / API
import { test, expect } from '@playwright/test'import { apiClient } from '../../utils/api-client'import { fixtures } from '../../fixtures/users.fixture'
test('TC-EARN-002: FREE user gets 403 on creator earnings endpoints', async () => { const preview = await apiClient.get('/creator/me/earnings/preview', fixtures.free.token) expect(preview.status).toBe(403)
const history = await apiClient.get('/creator/me/earnings', fixtures.free.token) expect(history.status).toBe(403)})TC-EARN-003 — Control de acceso: CONTENT_ADMIN en panel admin
Section titled “TC-EARN-003 — Control de acceso: CONTENT_ADMIN en panel admin”Cubre: AC-EARN-006.4 Tipo: E2E / API
import { test, expect } from '@playwright/test'import { apiClient } from '../../utils/api-client'import { fixtures } from '../../fixtures/users.fixture'
test('TC-EARN-003: CONTENT_ADMIN cannot access SUPER_ADMIN payout periods', async () => { const res = await apiClient.get('/admin/creator-payouts/periods', fixtures.contentAdmin.token) expect(res.status).toBe(403)})TC-EARN-004 — Modal de términos bloquea el panel
Section titled “TC-EARN-004 — Modal de términos bloquea el panel”Cubre: AC-EARN-001.1, AC-EARN-001.2 Tipo: E2E / UI
import { test, expect } from '@playwright/test'import { fixtures } from '../../fixtures/users.fixture'
test('TC-EARN-004: Earnings terms modal blocks panel on first access', async ({ page }) => { // Asumimos que el creador de prueba NO ha aceptado los términos await page.goto('/login') await page.fill('[name="email"]', fixtures.contentAdmin.email) await page.fill('[name="password"]', fixtures.contentAdmin.password) await page.click('[type="submit"]') await page.waitForURL('/dashboard')
await page.goto('/creator/earnings')
// El modal debe ser visible await expect(page.getByText('Términos del Programa de Ingresos')).toBeVisible() await expect(page.getByText('Acepto los términos')).toBeVisible()
// El panel de KPIs no debe ser visible todavía await expect(page.getByText('Completados únicos')).not.toBeVisible()})TC-EARN-005 — Preview del mes con creador elegible
Section titled “TC-EARN-005 — Preview del mes con creador elegible”Cubre: AC-EARN-002.1 Tipo: Integration / API
import { test, expect } from '@playwright/test'import { apiClient } from '../../utils/api-client'import { fixtures } from '../../fixtures/users.fixture'
test('TC-EARN-005: Eligible creator gets non-zero preview', async () => { // Asume que el creador de prueba tiene earningsTermsAcceptedAt y >= 5 completados const res = await apiClient.get('/creator/me/earnings/preview', fixtures.contentAdmin.token) expect(res.status).toBe(200)
const data = res.data expect(typeof data.periodMonth).toBe('string') expect(data.periodMonth).toMatch(/^\d{4}-\d{2}$/) expect(typeof data.completionsCount).toBe('number') expect(typeof data.estimatedGrossEur).toBe('number') expect(typeof data.isEligible).toBe('boolean') expect(typeof data.disclaimer).toBe('string') expect(data.disclaimer.length).toBeGreaterThan(10)})TC-EARN-006 — Historial paginado de períodos cerrados
Section titled “TC-EARN-006 — Historial paginado de períodos cerrados”Cubre: AC-EARN-003.1 Tipo: Integration / API
import { test, expect } from '@playwright/test'import { apiClient } from '../../utils/api-client'import { fixtures } from '../../fixtures/users.fixture'
test('TC-EARN-006: Earnings history returns paginated data', async () => { const res = await apiClient.get('/creator/me/earnings?page=1&limit=12', fixtures.contentAdmin.token) expect(res.status).toBe(200)
const data = res.data expect(Array.isArray(data.data)).toBe(true) expect(typeof data.total).toBe('number') expect(data.page).toBe(1) expect(data.limit).toBe(12)
if (data.data.length > 0) { const item = data.data[0] expect(typeof item.id).toBe('string') expect(typeof item.periodMonth).toBe('string') expect(typeof item.grossAmountEur).toBe('number') expect(typeof item.paymentStatus).toBe('string') expect(typeof item.isEligible).toBe('boolean') }})TC-EARN-007 — Detalle de un período cerrado con líneas
Section titled “TC-EARN-007 — Detalle de un período cerrado con líneas”Cubre: AC-EARN-004.1, AC-EARN-004.3 Tipo: Integration / API
import { test, expect } from '@playwright/test'import { apiClient } from '../../utils/api-client'import { fixtures } from '../../fixtures/users.fixture'
test('TC-EARN-007: Earning detail includes non-zero path lines', async () => { // Primero obtener el historial para tener un periodMonth real const historyRes = await apiClient.get('/creator/me/earnings?limit=1', fixtures.contentAdmin.token) expect(historyRes.status).toBe(200)
if (historyRes.data.data.length === 0) { // No hay períodos cerrados todavía, skip return }
const periodMonth = historyRes.data.data[0].periodMonth const detailRes = await apiClient.get(`/creator/me/earnings/${periodMonth}`, fixtures.contentAdmin.token) expect(detailRes.status).toBe(200)
const detail = detailRes.data expect(typeof detail.grossAmountEur).toBe('number') expect(Array.isArray(detail.lines)).toBe(true)
// Todas las líneas deben tener completionsCount > 0 for (const line of detail.lines) { expect(line.completionsCount).toBeGreaterThan(0) expect(typeof line.pathTitle).toBe('string') expect(typeof line.grossAmountEur).toBe('number') expect(typeof line.contributionPct).toBe('number') }})TC-EARN-008 — Rechazo de IBAN en paymentDataNote
Section titled “TC-EARN-008 — Rechazo de IBAN en paymentDataNote”Cubre: AC-EARN-005.2, AC-EARN-005.3 Tipo: Integration / API
import { test, expect } from '@playwright/test'import { apiClient } from '../../utils/api-client'import { fixtures } from '../../fixtures/users.fixture'
test('TC-EARN-008a: Payment note with IBAN is rejected with 422', async () => { const res = await apiClient.patch( '/creator/me/payment-data-note', { note: 'ES9121000418450200051332' }, fixtures.contentAdmin.token, ) expect(res.status).toBe(422)})
test('TC-EARN-008b: Payment note with 16-digit card number is rejected with 422', async () => { const res = await apiClient.patch( '/creator/me/payment-data-note', { note: 'Mi tarjeta es 4242424242424242' }, fixtures.contentAdmin.token, ) expect(res.status).toBe(422)})
test('TC-EARN-008c: Valid payment note is accepted', async () => { const res = await apiClient.patch( '/creator/me/payment-data-note', { note: 'NIF 12345678A · Autónomo/a · España' }, fixtures.contentAdmin.token, ) expect(res.status).toBe(200) expect(res.data.ok).toBe(true)})TC-EARN-009 — Cierre de período y bloqueo
Section titled “TC-EARN-009 — Cierre de período y bloqueo”Cubre: AC-EARN-007.2, AC-EARN-007.3 Tipo: Integration / API (SUPER_ADMIN)
import { test, expect } from '@playwright/test'import { apiClient } from '../../utils/api-client'import { fixtures } from '../../fixtures/users.fixture'
test('TC-EARN-009: Closing an unlocked period locks it', async () => { // Este test requiere un período de prueba pre-creado y unlocked const testPeriod = '2025-01' // período de prueba sin datos reales
const closeRes = await apiClient.post( `/admin/creator-payouts/periods/${testPeriod}/close`, {}, fixtures.superAdmin.token, ) expect(closeRes.status).toBe(201) expect(closeRes.data.ok).toBe(true) expect(closeRes.data.periodMonth).toBe(testPeriod) expect(typeof closeRes.data.eligibleCount).toBe('number')
// Verificar que está bloqueado const detailRes = await apiClient.get( `/admin/creator-payouts/periods/${testPeriod}`, fixtures.superAdmin.token, ) expect(detailRes.data.config.isLocked).toBe(true)})
test('TC-EARN-009b: Closing an already-locked period returns 400', async () => { // Usar un período que ya esté bloqueado const lockedPeriod = '2025-01' const res = await apiClient.post( `/admin/creator-payouts/periods/${lockedPeriod}/close`, {}, fixtures.superAdmin.token, ) expect(res.status).toBe(400)})TC-EARN-010 — Exportación CSV de un período
Section titled “TC-EARN-010 — Exportación CSV de un período”Cubre: AC-EARN-009.1, AC-EARN-009.2 Tipo: Integration / API (SUPER_ADMIN)
import { test, expect } from '@playwright/test'import { apiClient } from '../../utils/api-client'import { fixtures } from '../../fixtures/users.fixture'
test('TC-EARN-010: CSV export has correct headers and content-type', async ({ request }) => { const response = await request.get('/api/admin/creator-payouts/periods/2026-04/export', { headers: { Authorization: `Bearer ${fixtures.superAdmin.token}` }, })
expect(response.ok()).toBe(true) expect(response.headers()['content-type']).toContain('text/csv') expect(response.headers()['content-disposition']).toContain('creator-earnings-2026-04.csv')
const body = await response.text() const lines = body.trim().split('\n') expect(lines.length).toBeGreaterThanOrEqual(1)
// Verificar cabeceras CSV const headers = lines[0].split(',') expect(headers).toContain('periodMonth') expect(headers).toContain('creatorId') expect(headers).toContain('email') expect(headers).toContain('grossAmountEur') expect(headers).toContain('paymentStatus') expect(headers).toContain('paymentDataNote')})TC-EARN-011 — Estadísticas públicas sin autenticación
Section titled “TC-EARN-011 — Estadísticas públicas sin autenticación”Cubre: AC-EARN-010.1, AC-EARN-010.2 Tipo: Integration / API
import { test, expect } from '@playwright/test'import { apiClient } from '../../utils/api-client'
test('TC-EARN-011: Public earnings stats accessible without auth', async () => { // Sin token const res = await apiClient.get('/public/creator-earnings/stats') expect(res.status).toBe(200)
const data = res.data expect(typeof data.avgMonthlyEarningsTopCreator).toBe('number') expect(typeof data.totalPaidToCreators).toBe('number') expect(typeof data.activeCreatorsCount).toBe('number') expect(typeof data.poolPercentage).toBe('number') // Pool percentage por defecto es 0.25 (25%) expect(data.poolPercentage).toBeGreaterThan(0) expect(data.poolPercentage).toBeLessThanOrEqual(1)})TC-EARN-012 — Desbloqueo de período requiere razón
Section titled “TC-EARN-012 — Desbloqueo de período requiere razón”Cubre: AC-EARN-007.4 Tipo: Integration / API (SUPER_ADMIN)
import { test, expect } from '@playwright/test'import { apiClient } from '../../utils/api-client'import { fixtures } from '../../fixtures/users.fixture'
test('TC-EARN-012: Unlocking period without reason returns 403', async () => { const res = await apiClient.post( '/admin/creator-payouts/periods/2025-01/unlock', { reason: '' }, fixtures.superAdmin.token, ) expect(res.status).toBe(403)})
test('TC-EARN-012b: Unlocking period with reason succeeds', async () => { const res = await apiClient.post( '/admin/creator-payouts/periods/2025-01/unlock', { reason: 'Corrección de datos de un creador detectada en auditoría Q2' }, fixtures.superAdmin.token, ) expect(res.status).toBe(201) expect(res.data.ok).toBe(true)})Jobs automatizados
Section titled “Jobs automatizados”| Slug registrado | Cron | Descripción |
|---|---|---|
earnings-calculator | 15 3 * * * (03:15 diario) | Actualiza CreatorEarningPreview con métricas del mes en curso |
earnings-closure | 0 6 2 * * (día 2 de cada mes, 06:00) | Cierra el mes anterior: calcula earnings finales y bloquea el período |
Ambos jobs están registrados en ScheduledJobsHandlerRegistry y son visibles desde /admin/platform/jobs. El SUPER_ADMIN puede deshabilitar o disparar manualmente cada uno. El endpoint POST /admin/creator-payouts/jobs/run-preview permite disparar el job de preview manualmente sin esperar al cron.
Cobertura actual
Section titled “Cobertura actual”| Área | Estado |
|---|---|
| Aceptación de términos (modal bloqueante) | ✅ Implementado |
| Preview diario del mes en curso | ✅ Implementado |
| Historial de períodos cerrados (paginado) | ✅ Implementado |
| Detalle de período con desglose por ruta | ✅ Implementado |
| Referencia fiscal (paymentDataNote) | ✅ Implementado |
| Cierre mensual automático (cron día 2) | ✅ Implementado |
| Panel admin de liquidación (SUPER_ADMIN) | ✅ Implementado |
| Exportación CSV por período | ✅ Implementado |
| Audit log de cambios de estado | ✅ Implementado |
| Estadísticas públicas | ✅ Implementado |
| Hitos de ganancias acumuladas (notificación) | ✅ Implementado |
Spec Playwright earnings.spec.ts | 📋 Pendiente de crear |