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.

Los KPIs incluyen desglose fiscal completo:

KPI adicionalDatos
Comisiones StripeSum de stripeFee de todos los enrollments
Impuestos recaudadosCalculado a partir de taxRate del profesor
Base imponibleIngresos 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:

ColumnaDatosSortable
FechacreatedAt formateadoSi
AlumnoNombre + emailSi
ProductoNombre del servicioSi
ImportepricePaid formateado como monedaSi
EstadoBadge de estadoSi
AccionesVer 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:

CampoDatos
ID de pagoPAY-00001 (paymentNumber)
FechaFecha completa
AlumnoNombre + email
ServicioNombre del servicio
Importe totalpricePaid (negrita)
Comision StripestripeFee o “Sin datos”
Impuesto (X%)Calculado si taxRate > 0
Base imponibleImporte - impuesto
ReembolsoImporte reembolsado (rojo, si aplica)
Neto profesorImporte - comision - reembolso
SesionesCompletadas / programadas
DescuentoCodigo + 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:

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-sidev2
Export ExcelDropdown con CSV (server-side) y Excel (client-side via SheetJS)v2
Codigos de descuentoCRUD completo con validacion y statsBatch 4
TanStack TableTabla con sorting, filtering y busqueda global via @tanstack/react-tablev3
Desglose fiscalKPIs con comisiones Stripe, impuestos y base imponible. Sheet de detalle por pagov3
Tax settingsteachers.taxRate configurable para calculo de impuestos en desglosev3

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/app/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.