Skip to content

Pagos

Ruta: /teacher/payments · Atajo: g p · Sidebar: Pagos

Historial de pagos del profesor con KPIs de ingresos clicables (drill-down), tabla de transacciones, reembolsos via Stripe y exportacion CSV y Excel.

Payments page

Dos inputs de fecha (desde/hasta) que filtran tanto KPIs como tabla. Al cambiar fechas, la paginacion se resetea a pagina 1.

KPIColorIconoDatosClick
Ingresos totalesVerdeDollarSignSum de pricePaid en el rangoFiltra tabla a active / completed
Reembolsos totalesRojoTrendingDownSum de amountRefundedFiltra tabla a cancelled con reembolso
Ingresos netosAzulTrendingUpIngresos - ReembolsosLimpia filtro activo
TransaccionesMoradoReceiptCount de enrollmentsLimpia filtro activo

Cada tarjeta es clicable. Al hacer click:

  • Se aplica un filtro local sobre los pagos ya cargados (client-side, sin nueva peticion al API).
  • La tarjeta activa se marca con estilo active (KpiCard recibe active={true}).
  • Un toast informa del filtro aplicado.
  • Hacer click en la misma tarjeta activa limpia el filtro (toggle).

El filtrado es client-side sobre payments usando useMemo: filtra el array local por statusFilter antes de renderizar la tabla.

Tabla responsive (headers ocultos en movil) con columnas:

ColumnaDatos
FechacreatedAt formateado DD MMM YYYY
AlumnoNombre + email
ProductoNombre del servicio
ImportepricePaid formateado como moneda
EstadoBadge de estado
AccionesBoton de reembolso (condicional)

Paginacion: 20 items por pagina con botones anterior/siguiente.

Boton de reembolso visible solo si:

  • status === 'active'
  • sessionsCompleted === 0
  • sessionsScheduled === 0

Flujo de confirmacion en 2 pasos: click → botones confirmar/cancelar.

Backend: Llama a Stripe createRefund(), actualiza enrollment a cancelled con amountRefunded = pricePaid y cancellationReason = 'refunded'.

Dropdowns adicionales junto al date range para filtrar la tabla y los KPIs:

FiltroTipoComportamiento
AlumnoDropdownFiltra por studentId
ServicioDropdownFiltra por serviceId
EstadoDropdownactive, cancelled, refunded
Importe minimoInput numericominAmount en query params
Importe maximoInput numericomaxAmount en query params

Boton “Limpiar filtros” resetea todos los filtros y la paginacion. El filtrado es server-side: todos los parametros se envian como query params al backend.

El dialog de reembolso incluye seleccion de tipo:

  • Reembolso completo: devuelve pricePaid integro (comportamiento anterior).
  • Reembolso parcial: input de cantidad personalizada. Validado entre 0.01 y pricePaid.

Backend: El endpoint acepta amount opcional en el body. Si se omite, reembolsa el total. Stripe createRefund recibe amount en centimos. amountRefunded en el enrollment refleja el importe real devuelto.

Boton “Exportar” con dropdown que ofrece dos formatos:

FormatoIconoArchivoGeneracion
CSVFileTextpayments.csvServer-side via /teacher/payments/export. Headers en espanol: Fecha, Alumno, Email, Oferta, Importe, Moneda, Estado, Reembolso
Excel (.xlsx)FileSpreadsheetpayments.xlsxClient-side usando xlsx (SheetJS). Opera sobre filteredPayments ya en memoria

El export Excel incluye:

  • Headers internacionalizados (via i18n).
  • Columnas de importe y reembolso formateadas como numeros con formato #,##0.00 (no como texto).
  • Anchos de columna predefinidos (!cols) para legibilidad.
  • Hoja nombrada Payments.
  • Valores de importe convertidos de centimos a unidades (divididos entre 100).

Ruta: /teacher/discount-codes · Sidebar: acceso desde Pagos

Gestion de codigos promocionales que aplican descuento server-side antes de crear la sesion de checkout en Stripe (no usa Stripe Coupons).

CRUD completo:

  • Crear codigo con nombre, tipo de descuento y valor
  • Editar configuracion de un codigo existente
  • Soft-delete (restaurable)

Tipos de descuento:

TipoCampoEjemplo
percentagediscountValue (0-100)20% de descuento
fixed_amountdiscountValue (centimos) + currency5.00 EUR de descuento

Configuracion por codigo:

CampoDescripcion
codeTexto del codigo (max 50 chars, unique por profesor)
discountTypepercentage o fixed_amount
discountValueValor del descuento
currencyMoneda (solo para fixed_amount)
applicableServiceIdsJSONB array de IDs de servicios. Vacio = aplica a todos
maxUsesMaximo de usos totales (null = ilimitado)
maxUsesPerStudentMaximo de usos por alumno (null = ilimitado)
validFromFecha de inicio de validez (opcional)
validUntilFecha de fin de validez (opcional)
isActiveToggle activo/inactivo

Validacion (server-side): DiscountCodeService.validate() comprueba: activo + no eliminado, rango de fechas (validFrom/validUntil), usos totales vs maxUses, usos por alumno vs maxUsesPerStudent, servicios aplicables.

Endpoint publico: POST /public/:slug/validate-discount (rate-limited 10/min) permite validar un codigo antes del checkout.

Estadisticas: Vista de stats por codigo con usesCount y detalle de usos en discount_code_uses.

Flujo de aplicacion:

  1. Alumno introduce codigo en el checkout
  2. Frontend valida via endpoint publico
  3. Backend ajusta unit_amount antes de createCheckoutSession()
  4. Se registra en discount_code_uses y en el enrollment (discountCodeId + discountAmount)

FeatureDescripcionEstadoImplementado
Drill-down en KPIsClick en cualquier KPI filtra la tabla client-side. “Ingresos” muestra activos/completados, “Reembolsos” muestra cancelados con devolucion. Toggle: volver a clicar la misma tarjeta limpia el filtrov2
Export ExcelBoton “Exportar” tiene dropdown con CSV (server-side) y Excel (client-side via SheetJS). El .xlsx incluye anchos de columna, formato numerico en importes y hoja nombradav2
Codigos de descuentoCRUD de codigos promocionales con porcentaje/importe fijo, servicios aplicables, limites de uso, rango de fechas y stats. Descuento server-side antes de StripeBatch 4

BugDescripcionEstadoCorregido
Moneda hardcodeada en KPIsLos KPIs formateaban todos los importes como EUR independientemente de la moneda del enrollment. Ahora usan defaultCurrency del profesorBatch 4
Reembolso sin ConfirmDialogEl flujo de confirmacion usaba botones inline en vez del componente ConfirmDialog con variant danger. Ahora usa ConfirmDialog con variant="danger"Batch 4
Error generico en reembolsoLos errores de reembolso mostraban un toast generico. Ahora muestran mensajes i18n diferenciados segun el tipo de errorBatch 4

MejoraDescripcionDificultadEstadoImplementado
ConfirmDialog para reembolsosReembolsos ahora usan ConfirmDialog con variant dangerFacilBatch 4
Dashboard de moneda multipleLos KPIs usan defaultCurrency del profesor. Si hay pagos en multiples monedas, se podria extender con KPIs separados por monedaMedioBatch 4 (parcial)

ArchivoProposito
apps/web/src/routes/teacher/payments.lazy.tsxPagina completa
apps/api/src/services/teacher/payment-service.tsServicio (KPIs, lista, CSV, reembolso)
apps/api/src/routes/teacher/payments.tsRutas HTTP
apps/api/src/services/billing/discount-code-service.tsServicio de codigos de descuento
apps/api/src/routes/teacher/discount-codes.tsRutas HTTP de codigos de descuento
EndpointMetodoProposito
/teacher/paymentsGETLista paginada (20/pagina). Query params: studentId, serviceId, status, minAmount, maxAmount, startDate, endDate, page, limit
/teacher/payments/kpisGETKPIs de ingresos/reembolsos. Acepta los mismos filtros de fecha, alumno, servicio y estado
/teacher/payments/exportGETCSV de pagos (server-side)
/teacher/payments/:enrollmentId/refundPOSTReembolso via Stripe. Body: { amount?: number } (en centimos). Si se omite amount, reembolsa el total
/teacher/discount-codesGETLista de codigos de descuento del profesor
/teacher/discount-codesPOSTCrear codigo de descuento
/teacher/discount-codes/:idGET/PATCHDetalle y edicion de codigo
/teacher/discount-codes/:idDELETESoft-delete de codigo
/teacher/discount-codes/:id/statsGETEstadisticas de uso del codigo
/public/:slug/validate-discountPOSTValidar codigo (rate-limited 10/min)
useQuery({ queryKey: ['teacher-payments-kpis', startDate, endDate], queryFn: ... })
useQuery({ queryKey: ['teacher-payments', startDate, endDate, page], queryFn: ... })
const [statusFilter, setStatusFilter] = useState<'active' | 'refunded' | null>(null);
// Derived: filteredPayments aplicado via useMemo sobre payments[]
const filteredPayments = useMemo(() => {
if (!statusFilter) return payments;
if (statusFilter === 'active') return payments.filter(p => p.status === 'active' || p.status === 'completed');
if (statusFilter === 'refunded') return payments.filter(p => p.status === 'cancelled' && (p.amountRefunded ?? 0) > 0);
return payments;
}, [payments, statusFilter]);

El export Excel opera sobre filteredPayments, por lo que si hay un drill-down activo, el .xlsx solo contiene los pagos visibles.