Ir al contenido

Transfer report tokenizado

hop o dir generan un informe de transferencia desde el perfil del jugador (/dashboard/plantel/[id]) eligiendo nota, links de video y días de expiración. El sistema crea un row en transfer_reports con token opaco. El destinatario abre /transfer/[token] sin login, ve los datos del jugador, y cada visita incrementa atómicamente view_count.

  • Problema que resuelve: mandar a representantes y clubes interesados un informe profesional del jugador (datos físicos, posición, métricas, video) sin tener que generar un PDF manualmente ni darle login al destinatario.
  • Casos de uso típicos: scouting outbound (mostrar un jugador propio a un club interesado), gestión de representantes (compartir performance sin abrir el dashboard).
  • Planes que lo incluyen: la UI de /upgrade lista “Informes de transferencias” como exclusivo de Profesional y Enterprise (Esencial muestra el feature con ok: false). No hay un feature flag transfer_reports en lib/plans.ts — el gating es visual en la página de planes, no enforzado en código. Ver Planes para la matriz real.
  • Diferenciador: link tokenizado con expiración configurable y contador de vistas atómico — el club ve cuántos viewers abrieron el link.

Solo hop y dir pueden generar y revocar informes desde la server action createTransferReport. Hay validación de rol server-side en actions-transfer.ts.

  1. Plantel → click en el jugador → tab “Transferencia”.
  2. Modal pide:
    • Nota (texto libre, opcional).
    • Links de video (array de URLs, opcional).
    • Días de expiración (1-365, default 30).
  3. createTransferReport(playerId, { note, videoLinks, expiryDays }) inserta una row en transfer_reports con token opaco (generado por trigger de DB) y expires_at calculado.
  4. La UI muestra el link absoluto ${origin}/transfer/${token} con botón “Copiar”.
  1. Abre /transfer/[token] en cualquier dispositivo, sin login.
  2. Si el report fue revocado (is_active = false) o está expirado (expires_at < now), ve un 404.
  3. Si está válido, ve:
    • Foto, nombre, posición, dorsal, nacionalidad, fecha de nacimiento, altura, peso, pie preferido.
    • Nota libre del cuerpo técnico.
    • Links de video clickeables.
  4. El view_count se incrementa atómicamente vía RPC increment_transfer_view_count (fire-and-forget). Antes del fix, dos viewers concurrentes perdían visitas — el RPC ahora usa SET view_count = view_count + 1.
  1. Plantel → jugador → tab “Transferencia” lista los últimos 5 reports activos del jugador con: token, fecha de creación, contador de vistas, fecha de expiración.
  2. Botón “Revocar” baja is_active = false → el link devuelve 404 inmediatamente.
  • expiryDays clampea a [1, 365] server-side.
  • El token se genera por trigger de DB — el cliente nunca lo fabrica.
  • Token inválido o report inactivo: la página devuelve 404 y no expone datos del jugador.
  • robots: 'noindex, nofollow' está hardcodeado en el metadata de la página → los buscadores no indexan los links.
  • Sin sesión: la página usa cliente admin (service role) para bypassear RLS, ya que el visitante no está autenticado.

Player surface: N/A. El informe es para terceros (clubes, representantes). El jugador no ve los informes generados sobre él desde el portal.

  • transfer_reports: id, token, player_id, created_by, note, video_links (text[]), expires_at, view_count, is_active, created_at.
  • players: lectura de campos del jugador para renderizar el informe.
  • RPC increment_transfer_view_count(p_token text): incremento atómico del contador.
  • Supabase admin client (service role) para bypassear RLS en la ruta pública.
  • Sentry captura errores de la action de creación.
  • No hay export PDF del informe — el link es web-only. El staff puede usar la captura del navegador para un PDF estático.
  • Sin watermarking del viewer — no se rastrea quién abrió el link, solo cuántas veces se abrió. Para identificar viewers, habría que generar tokens distintos por destinatario.
  • Sin auto-renovación de la expiración. Una vez vencido, el staff tiene que generar un report nuevo (queda token distinto).