Skip to content

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


MétodoRutaRolesDescripción
GET/creator/me/earnings/previewCONTENT_ADMIN, SUPER_ADMINPreview del mes en curso (actualizado diariamente a las 03:15)
GET/creator/me/earningsCONTENT_ADMIN, SUPER_ADMINHistorial paginado de períodos cerrados
GET/creator/me/earnings/:periodMonthCONTENT_ADMIN, SUPER_ADMINDetalle de un período cerrado con desglose por ruta
PATCH/creator/me/payment-data-noteCONTENT_ADMIN, SUPER_ADMINActualizar referencia fiscal para Contabilidad
PATCH/creator/me/accept-earnings-termsCONTENT_ADMIN, SUPER_ADMINAceptar los términos del programa de ingresos
GET/public/creator-earnings/statsPúblico (sin auth)Estadísticas agregadas de la plataforma
GET/admin/creator-payouts/periodsSUPER_ADMINListar todos los períodos de liquidación
GET/admin/creator-payouts/periods/:periodMonthSUPER_ADMINDetalle de un período con todos los earnings
PATCH/admin/creator-payouts/periods/:periodMonthSUPER_ADMINActualizar configuración del período (grossRevenue, fees, pool %)
POST/admin/creator-payouts/periods/:periodMonth/closeSUPER_ADMINCerrar y bloquear un período manualmente
POST/admin/creator-payouts/periods/:periodMonth/unlockSUPER_ADMINDesbloquear un período cerrado (con razón obligatoria)
PATCH/admin/creator-payouts/earnings/:earningId/payment-statusSUPER_ADMINActualizar estado de pago de un creador
GET/admin/creator-payouts/periods/:periodMonth/exportSUPER_ADMINExportar período como CSV
GET/admin/creator-payouts/audit-logSUPER_ADMINLog de cambios de estado de un período
POST/admin/creator-payouts/jobs/run-previewSUPER_ADMINDisparar el job de preview manualmente

score = completions × 0.5 + minutesConsumed × 0.3 + weightedRating × 0.2
participationPct = score / totalScoreEligibleCreators
grossAmountEur = creatorPoolAmount × participationPct

Un creador es elegible para cobro si cumple todos:

  1. Perfil no suspendido (suspendedAt == null)
  2. Al menos 1 ruta publicada
  3. 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.

  1. El job earnings-preview corre cada día a las 03:15 y actualiza CreatorEarningPreview con datos del mes en curso.
  2. El job earnings-closure corre el día 2 de cada mes a las 06:00 y cierra el mes anterior: calcula earnings finales, crea CreatorEarningLine por ruta, bloquea el PlatformRevenueConfig (isLocked = true).
  3. El SUPER_ADMIN puede cerrar un período manualmente o desbloquearlo (con razón registrada en adminNotes).
EstadoDescripción
PENDING_REVIEWEstado inicial tras cierre del período
IN_REVIEWEl equipo de Contabilidad está procesando
APPROVEDPago aprobado, pendiente de transferencia
PAIDTransferencia realizada
ON_HOLDEn espera por incidencia
REJECTEDPago rechazado (motivo en paymentNotes)

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.

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.

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).


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érminos
When navego a /creator/earnings y se llama GET /creator/me/earnings/preview
Then la API devuelve 403 con code "EARNINGS_TERMS_NOT_ACCEPTED"
And el frontend muestra un modal bloqueante con el disclaimer del programa
And 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á visible
When hago clic en "Acepto los términos"
Then la API llama PATCH /creator/me/accept-earnings-terms
And la respuesta es 200 { ok: true, acceptedAt: "..." }
And el modal se cierra y el panel de ingresos carga correctamente

AC-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 registrado
When navego a /creator/earnings
Then el panel carga directamente sin mostrar el modal de términos

US-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 mes
When cargo el panel de ingresos en /creator/earnings
Then veo los KPIs: completados únicos, minutos consumidos, valoración media ponderada y estimado bruto en EUR
And el banner de elegibilidad muestra "Elegible para pago este mes"
And se muestra el pool estimado y mi porcentaje de participación

AC-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 ingresos
Then 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 mes
When cargo el panel de ingresos
Then se muestra el banner de disclaimer
And los KPIs muestran 0
And 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 preview
When visualizo la página
Then siempre hay un banner amarillo con el texto del disclaimer legal
And al final de la página hay un bloque "Aviso legal" con el mismo texto

US-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 historial
When cargo el panel de ingresos
Then veo la tabla "Historial de períodos" con columnas: Período, Completados, Participación, Monto bruto, Estado pago
And los períodos están ordenados de más reciente a más antiguo
And el período más reciente aparece primero

AC-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 historial
Then 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 estado

AC-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 pago
When veo la tabla de historial
Then 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íodos
When hago clic en una fila de un período elegible
Then 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 bruto

AC-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íodo
When abro el modal de detalle del período
Then aparece la sección "Notas del equipo" con el texto guardado

AC-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 mes
When abro el detalle del período
Then 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 Guardar
Then 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 Guardar
Then la API devuelve 422 Unprocessable Entity
And el mensaje de error indica que no se pueden incluir IBANs o números de tarjeta

AC-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 Guardar
Then la API devuelve 422 Unprocessable Entity

AC-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 caracteres
When el texto supera 500 caracteres
Then el botón Guardar permanece desactivado o el campo bloquea la entrada adicional

US-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 JWT
When llamo a GET /creator/me/earnings/preview
Then la respuesta es 401 Unauthorized
Given que tengo un token JWT de un usuario con role FREE
When llamo a GET /creator/me/earnings
Then la respuesta es 403 Forbidden

AC-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_ADMIN
When llamo a GET /creator/me/earnings
Then la respuesta es 200 con sus datos de earnings
And los datos corresponden únicamente al perfil del creador autenticado

AC-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_ADMIN
When llamo a GET /admin/creator-payouts/periods
Then la respuesta es 403 Forbidden

US-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 autenticado
When llamo a GET /admin/creator-payouts/periods
Then recibo un array de configuraciones de período ordenadas de más reciente a más antigua
And cada elemento incluye: periodMonth, poolPercentage, isLocked, grossRevenue, creatorPoolAmount, earningsCount

AC-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: 5000
When el SUPER_ADMIN llama a POST /admin/creator-payouts/periods/2026-04/close
Then el sistema calcula earnings para todos los creadores públicos con el pool de 5000 EUR
And 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: true
When el SUPER_ADMIN intenta llamar a POST /admin/creator-payouts/periods/2026-03/close
Then 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á bloqueado
When 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á bloqueado
When el SUPER_ADMIN intenta PATCH /admin/creator-payouts/periods/2026-03 con nuevos valores de grossRevenue
Then 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_REVIEW
When 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 actualizadas
And la respuesta incluye paymentStatusUpdatedAt y paymentStatusUpdatedBy

AC-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 desconocido
When 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 CreatorPaymentStatus

US-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 procesados
When el SUPER_ADMIN llama a GET /admin/creator-payouts/periods/2026-04/export
Then la respuesta tiene Content-Type: text/csv
And 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, paymentNotes

AC-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 completa
Then todos los CreatorMonthlyEarning de ese período tienen exportedAt y exportedBy actualizados

US-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 pagados
When llamo a GET /public/creator-earnings/stats sin autenticación
Then 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 meses

AC-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 bloqueado
When llamo a GET /public/creator-earnings/stats
Then la respuesta es 200 con todos los valores a 0 y poolPercentage: 0.25

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)
})

Slug registradoCronDescripción
earnings-calculator15 3 * * * (03:15 diario)Actualiza CreatorEarningPreview con métricas del mes en curso
earnings-closure0 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.


ÁreaEstado
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