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.
Que hay
Section titled “Que hay”Filtro de fechas
Section titled “Filtro de fechas”Dos inputs de fecha (desde/hasta) que filtran tanto KPIs como tabla. Al cambiar fechas, la paginacion se resetea a pagina 1.
KPIs (4 tarjetas) con drill-down
Section titled “KPIs (4 tarjetas) con drill-down”| KPI | Color | Icono | Datos | Click |
|---|---|---|---|---|
| Ingresos totales | Verde | DollarSign | Sum de pricePaid en el rango | Filtra tabla a active / completed |
| Reembolsos totales | Rojo | TrendingDown | Sum de amountRefunded | Filtra tabla a cancelled con reembolso |
| Ingresos netos | Azul | TrendingUp | Ingresos - Reembolsos | Limpia filtro activo |
| Transacciones | Morado | Receipt | Count de enrollments | Limpia 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(KpiCardrecibeactive={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 de pagos
Section titled “Tabla de pagos”Tabla responsive (headers ocultos en movil) con columnas:
| Columna | Datos |
|---|---|
| Fecha | createdAt formateado DD MMM YYYY |
| Alumno | Nombre + email |
| Producto | Nombre del servicio |
| Importe | pricePaid formateado como moneda |
| Estado | Badge de estado |
| Acciones | Boton de reembolso (condicional) |
Paginacion: 20 items por pagina con botones anterior/siguiente.
Reembolsos
Section titled “Reembolsos”Boton de reembolso visible solo si:
status === 'active'sessionsCompleted === 0sessionsScheduled === 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'.
Filtros avanzados
Section titled “Filtros avanzados”Dropdowns adicionales junto al date range para filtrar la tabla y los KPIs:
| Filtro | Tipo | Comportamiento |
|---|---|---|
| Alumno | Dropdown | Filtra por studentId |
| Servicio | Dropdown | Filtra por serviceId |
| Estado | Dropdown | active, cancelled, refunded |
| Importe minimo | Input numerico | minAmount en query params |
| Importe maximo | Input numerico | maxAmount 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.
Reembolsos parciales
Section titled “Reembolsos parciales”El dialog de reembolso incluye seleccion de tipo:
- Reembolso completo: devuelve
pricePaidintegro (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.
Export CSV y Excel
Section titled “Export CSV y Excel”Boton “Exportar” con dropdown que ofrece dos formatos:
| Formato | Icono | Archivo | Generacion |
|---|---|---|---|
| CSV | FileText | payments.csv | Server-side via /teacher/payments/export. Headers en espanol: Fecha, Alumno, Email, Oferta, Importe, Moneda, Estado, Reembolso |
| Excel (.xlsx) | FileSpreadsheet | payments.xlsx | Client-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).
Codigos de descuento
Section titled “Codigos de descuento”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:
| Tipo | Campo | Ejemplo |
|---|---|---|
percentage | discountValue (0-100) | 20% de descuento |
fixed_amount | discountValue (centimos) + currency | 5.00 EUR de descuento |
Configuracion por codigo:
| Campo | Descripcion |
|---|---|
code | Texto del codigo (max 50 chars, unique por profesor) |
discountType | percentage o fixed_amount |
discountValue | Valor del descuento |
currency | Moneda (solo para fixed_amount) |
applicableServiceIds | JSONB array de IDs de servicios. Vacio = aplica a todos |
maxUses | Maximo de usos totales (null = ilimitado) |
maxUsesPerStudent | Maximo de usos por alumno (null = ilimitado) |
validFrom | Fecha de inicio de validez (opcional) |
validUntil | Fecha de fin de validez (opcional) |
isActive | Toggle 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:
- Alumno introduce codigo en el checkout
- Frontend valida via endpoint publico
- Backend ajusta
unit_amountantes decreateCheckoutSession() - Se registra en
discount_code_usesy en el enrollment (discountCodeId+discountAmount)
Que falta
Section titled “Que falta”| Feature | Descripcion | Estado | Implementado |
|---|---|---|---|
| Drill-down en KPIs | Click 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 filtro | ✅ | v2 |
| Export Excel | Boton “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 nombrada | ✅ | v2 |
| Codigos de descuento | CRUD de codigos promocionales con porcentaje/importe fijo, servicios aplicables, limites de uso, rango de fechas y stats. Descuento server-side antes de Stripe | ✅ | Batch 4 |
Que falla
Section titled “Que falla”| Bug | Descripcion | Estado | Corregido |
|---|---|---|---|
| Moneda hardcodeada en KPIs | Los KPIs formateaban todos los importes como EUR independientemente de la moneda del enrollment. Ahora usan defaultCurrency del profesor | ✅ | Batch 4 |
| Reembolso sin ConfirmDialog | El 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 reembolso | Los errores de reembolso mostraban un toast generico. Ahora muestran mensajes i18n diferenciados segun el tipo de error | ✅ | Batch 4 |
Que cambiaria
Section titled “Que cambiaria”| Mejora | Descripcion | Dificultad | Estado | Implementado |
|---|---|---|---|---|
| ConfirmDialog para reembolsos | Reembolsos ahora usan ConfirmDialog con variant danger | Facil | ✅ | Batch 4 |
| Dashboard de moneda multiple | Los KPIs usan defaultCurrency del profesor. Si hay pagos en multiples monedas, se podria extender con KPIs separados por moneda | Medio | ✅ | Batch 4 (parcial) |
Referencia tecnica
Section titled “Referencia tecnica”Archivos clave
Section titled “Archivos clave”| Archivo | Proposito |
|---|---|
apps/web/src/routes/teacher/payments.lazy.tsx | Pagina completa |
apps/api/src/services/teacher/payment-service.ts | Servicio (KPIs, lista, CSV, reembolso) |
apps/api/src/routes/teacher/payments.ts | Rutas HTTP |
apps/api/src/services/billing/discount-code-service.ts | Servicio de codigos de descuento |
apps/api/src/routes/teacher/discount-codes.ts | Rutas HTTP de codigos de descuento |
| Endpoint | Metodo | Proposito |
|---|---|---|
/teacher/payments | GET | Lista paginada (20/pagina). Query params: studentId, serviceId, status, minAmount, maxAmount, startDate, endDate, page, limit |
/teacher/payments/kpis | GET | KPIs de ingresos/reembolsos. Acepta los mismos filtros de fecha, alumno, servicio y estado |
/teacher/payments/export | GET | CSV de pagos (server-side) |
/teacher/payments/:enrollmentId/refund | POST | Reembolso via Stripe. Body: { amount?: number } (en centimos). Si se omite amount, reembolsa el total |
/teacher/discount-codes | GET | Lista de codigos de descuento del profesor |
/teacher/discount-codes | POST | Crear codigo de descuento |
/teacher/discount-codes/:id | GET/PATCH | Detalle y edicion de codigo |
/teacher/discount-codes/:id | DELETE | Soft-delete de codigo |
/teacher/discount-codes/:id/stats | GET | Estadisticas de uso del codigo |
/public/:slug/validate-discount | POST | Validar codigo (rate-limited 10/min) |
Queries (TanStack Query)
Section titled “Queries (TanStack Query)”useQuery({ queryKey: ['teacher-payments-kpis', startDate, endDate], queryFn: ... })useQuery({ queryKey: ['teacher-payments', startDate, endDate, page], queryFn: ... })Estado local relevante
Section titled “Estado local relevante”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.