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.

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

Los KPIs incluyen desglose fiscal completo:

KPI adicional Datos
Comisiones Stripe Sum de stripeFee de todos los enrollments
Impuestos recaudados Calculado a partir de taxRate del profesor
Base imponible Ingresos netos - impuestos

El calculo fiscal usa teachers.taxRate (porcentaje, ej: 21 para IVA 21%):

base = total / (1 + taxRate / 100)
tax = total - base

Tabla construida con TanStack Table (@tanstack/react-table) con sorting, filtering y columnas:

Columna Datos Sortable
Fecha createdAt formateado Si
Alumno Nombre + email Si
Producto Nombre del servicio Si
Importe pricePaid formateado como moneda Si
Estado Badge de estado Si
Acciones Ver detalle, reembolso (condicional) No

Busqueda: Input de busqueda global que filtra por nombre de alumno o servicio. Paginacion: 20 items por pagina con botones anterior/siguiente.

Click en el icono “ojo” de cualquier fila abre un Sheet lateral con desglose completo:

Campo Datos
ID de pago PAY-00001 (paymentNumber)
Fecha Fecha completa
Alumno Nombre + email
Servicio Nombre del servicio
Importe total pricePaid (negrita)
Comision Stripe stripeFee o “Sin datos”
Impuesto (X%) Calculado si taxRate > 0
Base imponible Importe - impuesto
Reembolso Importe reembolsado (rojo, si aplica)
Neto profesor Importe - comision - reembolso
Sesiones Completadas / programadas
Descuento Codigo + importe (si aplica)

Acciones en el Sheet:

  • Enlace a transaccion en Stripe Dashboard
  • Boton de reembolso (si elegible)

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:

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.

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:

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

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:

  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)

Feature Descripcion Estado Implementado
Drill-down en KPIs Click en cualquier KPI filtra la tabla client-side ✅ v2
Export Excel Dropdown con CSV (server-side) y Excel (client-side via SheetJS) ✅ v2
Codigos de descuento CRUD completo con validacion y stats ✅ Batch 4
TanStack Table Tabla con sorting, filtering y busqueda global via @tanstack/react-table ✅ v3
Desglose fiscal KPIs con comisiones Stripe, impuestos y base imponible. Sheet de detalle por pago ✅ v3
Tax settings teachers.taxRate configurable para calculo de impuestos en desglose ✅ v3

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

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)

Archivo Proposito
apps/app/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)
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.