openapi: 3.1.0
info:
  title: Convray API
  version: 0.67.0
  license:
    name: Proprietary
    identifier: LicenseRef-Proprietary
  x-convray-contract-status: active-apc-core-casino-team
  x-convray-changelog:
    - version: 0.67.0
      date: 2026-10-03
      summary: >-
        Packs de promo codes de Bonos: recientes primero, búsqueda por palabras y marca de descartado (migración 20261003200000). GET /data/bonuses/promo/packs suma sort (recent por defecto, que ordena por id descendente, o used), status (active por defecto, discarded o all) y busca en q por palabras: todas deben estar en el nombre, en cualquier orden, sin distinguir mayúsculas ni tildes, o el id exacto. Cada pack suma last_used_at, discarded, discarded_at y discard_note, y kpis suma discarded; los KPIs excluyen los packs descartados. Nuevos PUT y DELETE /data/bonuses/promo/packs/{id}/discard (admin de Data o super_admin de plataforma, idempotentes): marcan y desmarcan un pack como descartado, con nota opcional. El dinero no cambia: un pack descartado con canjes sigue contando en lo cobrado. GET /data/bonuses/review suma los hallazgos pack_discardable (pack sin canjes con campaña terminada o creado hace más de 7 días) y pack_discarded_with_redemptions (descartado con canjes que generaron bono).
    - version: 0.66.0
      date: 2026-10-03
      summary: >-
        Efectividad real de los depósitos en los indicadores de Data (Depósitos · Indicadores, corte 1, sin migración). GET /data/deposits/insights suma el bloque real_effectiveness, calculado sobre la tabla cruda de depósitos con los mismos filtros de rango, método, dispositivo y jugador (el filtro status se ignora, como en el resto de los indicadores). Cuenta como no cobrado el depósito abierto (initialized, pending, pay_pending o in_process) que sigue abierto pasados 10 minutos (atascado; justo a los 10 minutos ya es atascado): la efectividad informada es paid / (paid + failed + declined) y la real es paid / (paid + failed + declined + atascados), ambas como fracción 0 a 1 con 4 decimales o null sin denominador. El bloque trae as_of (instante del servidor, RFC 3339), stuck_minutes (10), floor_rate (piso de la alerta de efectividad por método; 0,5 si el casino no tiene ajustes), totals (intentos, estados, abiertos jóvenes, atascados, montos pagado, fallido, rechazado, atascado y no cobrado, y las dos tasas), stuck_now y stale_open (abiertos de hoy entre 10 minutos y 48 horas, y de más de 48 horas, con cuenta y monto; no dependen del rango de fechas, sí de los demás filtros), by_method (por método de pago, ordenado por intentos; los depósitos sin método salen como Sin método; incluye stuck_now y alert_open cuando hay un episodio de alerta abierto), by_hour (por método y hora local de Santiago, solo con intentos) y daily (un elemento por día del rango, con ceros donde no hubo intentos, y alert_opened si empezó un episodio de alerta ese día). El bloque se recalcula con una caché propia de 30 segundos, sin pasar por la caché larga del resto de los indicadores, así que atascados y as_of nunca quedan más viejos que eso; la cabecera X-Cache es HIT solo si toda la respuesta salió de caché. Cambio aditivo, sin operaciones nuevas.
    - version: 0.65.0
      date: 2026-10-02
      summary: >-
        Búsqueda de un promo code en el módulo Bonos de Data. Nuevo GET /data/bonuses/promo/code?code= (rol mínimo viewer): compara el código sin espacios al borde y sin distinguir mayúsculas contra los canjes guardados y el código de los packs Single, y devuelve status (redeemed, not_redeemed o not_found), la vigencia según la campaña de cada pack (vigente, vencido, programado o sin_dato), los canjes distintos, los jugadores y una página de canjes con el pack, el Player ID, la fecha y el bono que generó (etiqueta, monto y cobrado). Un código de un pack Multiple o External que nadie canjeó responde not_found: sus códigos sin usar no se leen en Data. Sin operaciones modificadas.
    - version: 0.64.0
      date: 2026-09-30
      summary: >-
        Interruptor de las API con tokens en Data (migración 20260930300000). Apagar una integración es una pausa: la API responde 503 con code integration_paused a las llamadas hechas con sus tokens y los tokens se conservan (no se revocan); al encender vuelven a funcionar sin emitir nada. La llamada en pausa se audita y no consume cupo. Nuevos GET y PUT /data/support/integration (una sola pausa para toda la API de soporte del tenant), GET /data/retention/integrations (estado de juego-octubre, juego-codigos y qa-retencion) y PUT /data/retention/integrations/{integration}, para owner o admin de Data; los PUT exigen sesión web. Cada estado trae enabled, updated_at y updated_by (null mientras nadie la haya cambiado). Los listados y la emisión de tokens de soporte y de retención suman created_by (quien emitió) y revoked_by (quien revocó o rotó; sin dato en las revocaciones anteriores a este cambio). Los endpoints /support/players/* documentan el 503 nuevo y los de /retention/* lo suman a su 503.
    - version: 0.63.0
      date: 2026-09-30
      summary: >-
        Cierre del día en Data. GET /data/resumen suma en cada fila de daily los retiros sin resolver creados ese día: withdraw_pending_amount_minor y withdraw_pending_count (estados pending, pay_pending, in_process e initialized; no entran en withdraw_amount_minor ni en net_cash_minor, que solo cuentan lo pagado) y net_cash_if_pending_paid_minor (net_cash_minor menos el monto pendiente: el net cash del día si todos se pagan; si se cancelan, el net cash es net_cash_minor). El CSV table=daily agrega al final las columnas retiros_pendientes_clp, q_retiros_pendientes y net_cash_si_se_pagan_clp. GET /data/retention/audit acepta offset (0 a 100000) para paginar con limit, y ordena por created_at e id descendentes. Cambio aditivo, sin operaciones nuevas.
    - version: 0.62.0
      date: 2026-09-29
      summary: >-
        Pantalla de la API de retención en Data. Nuevo GET /data/retention/summary (owner o admin de Data): tokens vivos por integración, llamadas de hoy (día civil de Chile) y de ayer hasta la misma hora, llamadas con error y del servidor, p95 del tiempo de respuesta de hoy, data_through con la misma regla que /retention/health y, por token, las llamadas de cada una de las últimas 24 horas y su último uso. GET /data/retention/audit suma los filtros de servidor token (uuid) y result (ok o error), valida la forma de endpoint, interpreta from y to en formato AAAA-MM-DD como las 00:00 de ese día en hora de Chile (antes, UTC; to sigue siendo exclusivo y debe ser posterior a from) y devuelve totals con los conteos por resultado para las pestañas. Códigos de premio automáticos del juego de retención (PRD-59, migraciones 20260930200000 a 20260930200300): nuevos GET /retention/promo/packs (packs Multiple y External del patrón del token con codes_count, codes_synced y complete; sin códigos) y GET /retention/promo/codes (feed por cursor cifrado de cada código una sola vez, con used, used_by_player_ref y used_at; tope de 50.000 filas por token y día UTC), solo para un token de la integración nueva juego-codigos (scope retention.codes), que responde 403 en deposits/*, players/* y promo/redemptions; un token retention.read responde 403 en promo/packs y promo/codes. POST /data/retention/tokens acepta integration juego-codigos, que exige al menos un patrón de pack e ignora deposits_from (el token guarda el instante de emisión); la auditoría suma los endpoints promo_packs y promo_codes.
    - version: 0.61.0
      date: 2026-09-28
      summary: >-
        Integraciones de plataforma y alertas por Telegram (PRD-56 corte 1, migración 20260928230000), solo super_admin: GET /integrations (tarjetas de SendGrid, Twilio SMS y Telegram con interruptor, configuración, last4 del secreto y última prueba; el secreto nunca se devuelve), PUT /integrations/{code} (configuración y secreto de solo escritura, cifrado), POST /integrations/{code}/test (prueba con el proveedor; Telegram guarda el usuario del bot), POST /integrations/{code}/enable y /disable (activar exige una prueba exitosa en los últimos 30 minutos; apagar es inmediato), DELETE /integrations/{code}/secret y GET /integrations/audit. Equipos de alertas: GET/POST /alert-teams, PATCH/DELETE /alert-teams/{id}, POST /alert-teams/{id}/link-code (enlace t.me de un solo uso por 15 minutos para vincular un grupo) y POST /alert-teams/{id}/test. Rutas tipo de alerta × equipo: GET/PUT /alert-routes. Historial de envíos: GET /alert-deliveries. Las alertas de pagos VIP, efectividad por método de pago, calidad de datos y Potencial VIP se envían al grupo de cada equipo sin datos personales, con enlace a Convray.
    - version: 0.59.0
      date: 2026-09-28
      summary: >-
        Historial en la efectividad de pagos: GET /data/payment-methods/health suma history con la efectividad por día civil de Santiago (últimos 30 días, hoy incluido) y por mes calendario (últimos 12 meses, el en curso hasta hoy) del total y de cada método, desde el rollup diario de depósitos (pagados sobre pagados, fallidos y rechazados; rate null sin intentos). Cambio aditivo, sin operaciones nuevas.
    - version: 0.58.0
      date: 2026-09-27
      summary: >-
        Segundo factor por correo al iniciar sesión (con AUTH_EMAIL_OTP=enforce, obligatorio para todas las cuentas). POST /auth/sessions puede responder 202 con status email_otp_required, challenge_token, expires_in, new_code_after y email_hint en lugar de la sesión: el código de 6 dígitos llega por correo y vence en 10 minutos. Nuevos POST /auth/sessions/email-otp (confirma el código, admite 5 intentos, emite la sesión y, con trust_device, la cookie convray_trusted_device por 30 días, que deja de servir al cambiar la contraseña) y POST /auth/sessions/email-otp/new-code (un minuto entre envíos, máximo 3 por desafío). Las invitaciones de equipo se envían por correo al crearse o regenerarse y la respuesta agrega email_sent; el enlace de un solo uso sigue presente como respaldo.
    - version: 0.57.0
      date: 2026-09-27
      summary: >-
        Bonos por vertical en Data (PRD-52, migración 20260927120000): cada bono entregado tiene su vertical guardada (casino, sport, cash o unknown; los FreeBet de deporte son sport, los FreeBet de casino y los giros casino, el saldo real cash y los Wager la del producto de su definición en Centrivo) y cada campaña la suya (además mixed cuando entregó bonos de casino y de deporte). GET /data/bonuses/summary, /data/bonuses/campaigns, /data/bonuses/rollover y /data/bonuses/rollover/bonus/{bonusId} aceptan vertical=all|casino|sport (filtro por bono; cash y unknown solo en all). El resumen suma by_vertical y series (regalado por día, semana o mes y vertical con granularity=day|week|month; day hasta 92 días, week hasta 1092 y month hasta 3653); DataBonusCampaignRow suma vertical y vertical_grants; el rollover suma by_vertical y la vertical de cada campaña y bono; el bono suma su vertical y siblings (los bonos con depósito de la misma campaña); y la ficha del jugador suma by_vertical con sus tipos y la vertical de cada bono. El reporte bonuses del conector lee además las definiciones Wager de Centrivo (solo lectura). Cambio aditivo, sin operaciones nuevas.
    - version: 0.56.0
      date: 2026-09-27
      summary: >-
        Campaña Sportsbook en el módulo Bonos de Data (PRD-52, migración 20260927110000): DataBonusPurpose suma campana_sportsbook, el propósito de toda campaña con al menos un FreeBet de deporte (bonusType 2 de Centrivo), que gana sobre las reglas de nombre y pierde solo ante la marca manual. Aparece en el resumen por propósito, en el listado y la ficha de campañas, en el filtro purpose de GET /data/bonuses/campaigns y en los bonos de la ficha del jugador. Cambio aditivo en un enum, sin operaciones ni campos nuevos.
    - version: 0.55.0
      date: 2026-09-26
      summary: >-
        Módulo Bonos de Data en solo lectura (PRD-52 ola 4): GET /data/bonuses/summary (regalado, cobrado a dinero real, bonos y jugadores por etiqueta, por propósito y por familia, conteos de pestañas y las campañas con más regalado), GET /data/bonuses/campaigns y GET /data/bonuses/campaigns/{kind}/{id} (regalías cross-platform y campañas normales con su ficha: embudo, depósitos de los receptores antes y después del inicio y receptores por cobrado), GET /data/bonuses/rollover y GET /data/bonuses/rollover/bonus/{bonusId} (bonos que activó un depósito, con el rollover exigido y pendiente), GET /data/bonuses/promo/packs y GET /data/bonuses/promo/packs/{id}/uses (packs de promo codes y sus usos con el bono que generó cada uno), GET /data/bonuses/review (hallazgos para revisar) y GET /data/players/{ref}/profile/bonuses (sección Bonos de la ficha del jugador, histórico o mes en curso). Los bonos de prueba o staff no suman salvo include_tests=true y cada bono entregado cuenta una sola vez. Sin PII: solo el Player ID de Centrivo. Mismo gate de Data (rol mínimo viewer).
    - version: 0.54.1
      date: 2026-09-26
      summary: >-
        El reporte bonuses amplía lo que lee de Centrivo (PRD-52 olas 2 y 3), sin cambiar la forma de GET /data/connector/centrivo ni de POST /data/connector/centrivo/run: además de las campañas cross-platform, los bonos entregados y los jugadores de prueba, lee las campañas normales con su disparador, el depósito que activó cada bono (monto e ID de la transacción) con el turnover exigido, y los packs de promo codes con sus usos por jugador, sin correos ni nombres de jugadores. Un bono entregado se guarda una sola vez aunque Centrivo lo muestre en varias vistas (Statistics de la campaña, Statistics del bono y usos del código). Cambio solo descriptivo, sin operaciones ni campos nuevos; nada se modifica en Centrivo.
    - version: 0.54.0
      date: 2026-09-26
      summary: >-
        Regalías desde Centrivo en solo lectura (PRD-52 corte 1): el conector suma el reporte bonuses, al final de la lista. GET /data/connector/centrivo agrega en reports la fila bonuses (dataset_code bonuses, cadencia por intervalo), que en cada corrida lee las campañas cross-platform, los bonos entregados por jugador y la lista de jugadores de prueba de Centrivo; no baja archivo, así que file_name, file_size e import_batch_id de su última corrida vienen en null. POST /data/connector/centrivo/run acepta report bonuses. Cambio aditivo, sin operaciones nuevas; nada se modifica en Centrivo.
    - version: 0.53.0
      date: 2026-09-26
      summary: >-
        "Eliminar todas" en la campana y la bandeja de notificaciones: nuevo POST /notifications/dismiss-all (?tenant= como read-all) que elimina todas las notificaciones visibles de la cuenta en el tenant, leídas o no, y devuelve {"dismissed": n}. Borrado lógico por dismissed_at (migración 20260926140000): las eliminadas dejan de listarse y de contar como no leídas en GET /notifications, no se marcan como leídas y no se borran del registro.
    - version: 0.52.0
      date: 2026-09-25
      summary: >-
        Conversaciones de Ray personales y con tope: GET /assistant/conversations lista solo las conversaciones activas PROPIAS y agrega conversation_limit (4); POST /assistant/conversations responde 409 assistant_conversation_limit al superar el tope (eliminar = archivar con PATCH archived=true libera un cupo y conserva la conversación para auditoría).
    - version: 0.51.0
      date: 2026-09-25
      summary: >-
        Ray exporta a CSV y queda registrado. Nuevos GET /assistant/conversations/{id}/messages/{messageId}/data.csv (fuente, parámetros y cifras sin formato de cada consulta de una respuesta) y GET /assistant/export.csv (export completo de un listado de Data preparado por Ray). Ambos quedan en Data · Exportaciones a nombre del usuario; GET /data/exports agrega via (screen | assistant) para marcar lo hecho con Ray (migración 20260925220000).
    - version: 0.50.0
      date: 2026-09-25
      summary: >-
        Agrega la vista Efectividad de pagos de Data (PRD-55): GET /data/payment-methods/health
        (efectividad por método de pago en la última ventana, base de los días cerrados, estado
        alert/ok/low_volume/muted, parámetros de la regla y barras por hora UTC de 1 a 72 horas),
        GET /data/payment-method-alerts (episodios de la alerta de efectividad por método con
        open_count), PATCH /data/payment-method-alerts/{id} (marcar revisada con nota, admin, sin
        cambiar el estado) y GET/PUT /data/payment-method-alert-settings (interruptor, umbrales,
        métodos silenciados y canales campana/Work/correo, admin). La efectividad es pagados /
        (pagados + fallidos + rechazados); un vigilante propio abre, escala y cierra los episodios
        cada 5 minutos, a lo sumo uno abierto por método.
    - version: 0.49.0
      date: 2026-09-25
      summary: >-
        GET /data/players/vip-recovery agrega group (depósitos pagados y depositantes del grupo VIP completo en el mes evaluado y los dos anteriores, en total y por nivel) y days_in_month. Cambio aditivo: Ray lo usa para comparar depósitos VIP entre meses en una sola llamada.
    - version: 0.48.0
      date: 2026-09-25
      summary: >-
        Totales por jugador deja de estar atado a los periodos de 24 h cargados de Centrivo: se agregan
        GET /data/player-totals/range/summary y GET /data/player-totals/range/list, que toman un rango
        civil arbitrario (from/to, America/Santiago) y los mismos filtros globales que Jugadores
        (segmento tag, estado flag, compania isp, partner y deposito minimo dep_min/dep_from/dep_to).
        Depositado, retirado, net cash, apostado y GGR por vertical (con y sin bono) se computan desde
        data_player_activity_daily y son exactos para cualquier rango. El NGR y los impuestos son
        exactos SOLO si el rango se cubre exactamente con periodos cargados (ngr_available); si no,
        ngr_minor y taxes_minor son null (nunca se estima el NGR). El listado por rango exporta CSV y
        respeta la misma regla del NGR por fila.
    - version: 0.47.0
      date: 2026-09-25
      summary: >-
        Segmentación de Jugadores por depósitos y exclusiones (owner 2026-09-25). GET /data/players/list
        y GET /data/players/summary aceptan dep_min (entero CLP >= 0) sobre su PROPIA ventana
        dep_from/dep_to (días civiles America/Santiago, independiente del rango de registro): un jugador
        entra si la suma de sus depósitos PAGADOS en la ventana alcanza dep_min. summary también acepta
        state/status, tag y flag para que players_total y los KPIs reflejen el filtro (mismas exclusiones
        que el listado: no_staff/no_partner_tag/no_vip incl. Experiencia, no_self_excluded/no_blocked).
        El listado agrega deposited_window_minor/deposits_window_count/deposit_window_last_at por fila y
        el orden sort=deposited_window:desc|net_cash:desc|ltv:desc (default deposited_window:desc con la
        ventana activa). El export CSV (format=csv) usa separador ';' y las columnas player_id;vip;
        depositado_en_ventana_clp;depositos_en_ventana;ultimo_deposito;net_cash_historico_clp;
        compania_internet;registrado (+ nombre;apellido con PII), ordenado por depositado en la ventana.
    - version: 0.46.0
      date: 2026-09-25
      summary: >-
        Rankings de jugadores (GET /data/players/rankings): Top N configurable (limit hasta 200; el
        selector de la web manda 50/100/150/200, default 50) y los mismos filtros globales que Jugadores
        y Totales. Además de tag/flag/isp ya existentes, ahora acepta partner (socio/BTAG: tokens
        none/organic/any o slug) y "Depositó al menos $X" con su propia ventana (dep_min con dep_from/
        dep_to). Los filtros del camino agregado usan un IN NO correlacionado (la columna Player ID va
        calificada); el EXISTS correlacionado colgaba Rankings > 26 s.
    - version: 0.45.0
      date: 2026-09-25
      summary: >-
        Los resúmenes/rankings agregados de apuestas ahora ACEPTAN los filtros por jugador tag/flag/isp
        y filtran por jugador en vez de responder 422 (un 422 tumbaba la vista de Deportes cuando la
        FilterBar traía tag/flag). GET /data/sport-bets/summary suma solo las apuestas de los jugadores
        que cumplen el filtro (desde el ledger por apuesta, porque el rollup diario no tiene jugador);
        GET /data/casino-bets/summary y GET /data/casino-bets/games filtran por jugador desde el agregado
        jugador×juego×día. La columna Player ID va calificada y el filtro es un IN no correlacionado.
    - version: 0.44.0
      date: 2026-09-25
      summary: >-
        Tabla de Jugadores (GET /data/players/list): las filas suman deposits_paid_total_minor y
        withdrawals_paid_total_minor (depósitos y retiros PAGADOS históricos, columnas "Depósitos" y
        "Retiros") y ltv_minor (LTV = NGR histórico por jugador, suma sin solape de los períodos
        maximales, igual regla que /player-totals/ltv). Se cargan en una consulta por página (sin N+1).
        El filtro isp ya existente aplica también en esta tabla desde la web.
    - version: 0.43.0
      date: 2026-09-25
      summary: >-
        ISP por jugador visible (PRD-53 Fase 2): las filas por jugador de Data (jugadores, Totales,
        LTV, rankings, riesgo, recuperación VIP, transacciones) y la ficha suman player_isp (compañía
        de internet vigente: operator, kind, last_seen_at, operators_count; la IP no se guarda). Nuevo
        filtro isp (lista OR de operadores más sin_dato) en esos listados. Net cash honesto: las filas y
        la ficha suman deposits_loaded para distinguir el $0 real del $0 por historial de depósitos no
        cargado.
    - version: 0.42.0
      date: 2026-09-24
      summary: >-
        GET /data/player-totals/summary acepta los filtros tag/flag (DataTagFilter/DataFlagFilter): los
        KPIs de Totales por jugador suman solo a los jugadores filtrados, igual que el listado (que
        ahora también los documenta).
    - version: 0.41.0
      date: 2026-09-24
      summary: >-
        Las filas del LTV histórico (GET /player-totals/ltv) suman player_flags (badges VIP,
        autoexcluido, bloqueado…), igual que el listado de Totales por jugador. Corrige el filtro
        tag/flag de GET /player-totals/list, que no filtraba (correlación sin calificar).
    - version: 0.40.0
      date: 2026-09-24
      summary: >-
        Suma self_exclusion_date_status (synced|no_history|pending) a player_flags (listados por
        jugador de Data) y a la ficha del jugador (PRD-50, arreglo 2026-09-24). Distingue "sin fecha en
        Centrivo" (el jugador no tiene historial de juego responsable) de "pendiente de sincronizar",
        para que el badge Autoexcluido, la columna de Riesgo y la ficha muestren el texto correcto.
    - version: 0.39.0
      date: 2026-09-24
      summary: >-
        GET /assistant/status suma reason (PRD-51 iteración 2): null si Ray responde; si no, el primer
        motivo por el que no lo hace (disabled, missing_provider, missing_model, missing_key o
        key_unreadable), para que la pantalla de configuración y el widget muestren el estado en español.
    - version: 0.38.0
      date: 2026-09-24
      summary: >-
        Agrega la configuración de Ray (PRD-51), solo para super_admin: GET/PUT /assistant/settings
        (activación, proveedor, modelo, precios y topes; la llave de API es de solo escritura y se
        guarda cifrada), DELETE /assistant/settings/api-key, POST /assistant/settings/test (prueba de
        conexión), GET /assistant/settings/audit (registro de cambios), GET /assistant/budgets (gasto
        del mes por plataforma, departamento y usuario) y PUT /assistant/budgets/departments/{code} y
        /assistant/budgets/users/{userId} (topes propios o compartidos). /assistant/status suma el gasto
        y el tope de plataforma y de departamento, y el error de tope indica cuál se alcanzó (scope).
    - version: 0.37.0
      date: 2026-09-24
      summary: >-
        Agrega la FECHA de autoexclusión (PRD-50): self_excluded_at, self_exclusion_ends_at,
        self_exclusion_indefinite y self_exclusion_applied_by a player_flags (listados por jugador de
        Data) y a la ficha del jugador. Sólo se pueblan si el jugador está autoexcluido hoy y ya se
        sincronizó su fecha desde el historial de juego responsable de Centrivo; sin el comentario ni
        el nombre del operador (PII).
    - version: 0.36.0
      date: 2026-09-24
      summary: >-
        Work admite varios tableros por área (ruling 2026-09-24): POST /work/spaces/{id}/boards
        crea un tablero con plantilla de columnas (general/short) y PATCH /work/boards/{id} lo
        renombra o archiva; WorkBoard suma sort_order y archived. Las Alertas VIP pueden llegar a
        VARIOS destinos de Work: VipAlertSettings cambia work_board_id/work_column_id por la lista
        work_destinations (tablero + columna); la alerta abre una tarjeta en cada destino. Se agrega
        GET /data/work-targets para poblar los selectores Área → Tablero → Columna desde el host de
        Data (que no alcanza /work/*).
    - version: 0.35.0
      date: 2026-09-24
      summary: >-
        Ruling del owner (2026-09-24): "Experiencia VIP" SUMA a VIP en todos los indicadores. vip es
        true también para Experiencia VIP en player_flags (conteos, KPIs, filtro tag=vip, Recuperación
        VIP y Alertas VIP de pagos), y vip_level suma el valor "experience" (precedencia
        Diamante > Zafiro > Experiencia). ex_vip pasa a ser "tuvo cualquier etiqueta VIP y hoy no".
    - version: 0.34.0
      date: 2026-09-24
      summary: >-
        Agrega el endpoint transversal de solo marcas GET /players/flags (PRD-50 Fase 6): dado un
        conjunto de Player IDs devuelve sus badges (VIP/Ex-VIP/Experiencia VIP/Partner/Staff más
        self_excluded/blocked/attention) para pintarlos junto al Player ID en cualquier producto
        (APP/DATA/CRM/WORK) sin abrir Data ni exponer PII. Sin puerta de producto; lo autoriza una
        cuenta de empresa del tenant. Además suma net_cash_total_minor (net cash histórico del
        jugador: depósitos pagados − retiros pagados de toda su vida, CLP) a las filas por jugador de
        Players, Totales, Rankings, Riesgo y Recuperación VIP.
    - version: 0.33.0
      date: 2026-09-24
      summary: >-
        Agrega la vista Alertas VIP de Data (PRD-50 Fase 5): GET /data/vip-payment-alerts
        (lista con banderas del jugador), PATCH /data/vip-payment-alerts/{id} (marcar revisada
        con nota, admin) y GET/PUT /data/vip-alert-settings (umbrales y canales in-app/Work/correo,
        admin). Las alertas las abre el conector tras importar depósitos y retiros.
    - version: 0.32.0
      date: 2026-09-23
      summary: >-
        Agrega la vista Recuperación VIP (GET /data/players/vip-recovery) con Apagados, Bajando
        ritmo y Net Cash negativo del mes (exportable a CSV, excluye autoexcluidos y bloqueados), y
        suma los filtros tag/flag a Rankings, Riesgo y el listado de Totales por jugador (PRD-50
        Fases 3 y 4).
    - version: 0.31.0
      date: 2026-09-23
      summary: >-
        Agrega player_flags (etiquetas VIP/Ex-VIP/Experiencia VIP/Partner/Staff de Centrivo
        más self_excluded/blocked/attention) a los listados por jugador de Data, extiende la
        ficha del jugador y el contexto de transacción con las mismas etiquetas, y suma los
        filtros tag/flag a depósitos, retiros, apuestas de casino y deportes, y el listado de
        jugadores (PRD-50).
    - version: 0.30.2
      date: 2026-08-14
      summary: Conserva la sesión al volver a la landing, evita carreras de refresh y marca toda respuesta API como no almacenable.
    - version: 0.30.1
      date: 2026-08-14
      summary: Entrega invitaciones Account Manager mediante fragmento de URL de un solo uso y contraseña definida por la persona invitada.
    - version: 0.30.0
      date: 2026-08-14
      summary: Agrega equipo casino multiusuario y Account Manager operacional de solo lectura.
    - version: 0.29.0
      date: 2026-08-13
      summary: Cierra por defecto el registro público y conserva login y alta controlada para el piloto.
    - version: 0.28.0
      date: 2026-08-13
      summary: Agrega partner administrado exclusivo y casino principal de solo lectura.
    - version: 0.27.0
      date: 2026-07-25
      summary: >-
        Las Wager Races pasan al modelo de casino: cuelgan de una conexión de casino
        externa, el premio va en dinero del casino y Convray deja de acreditar puntos
        por ellas.
    - version: 0.23.1
      date: 2026-07-22
      summary: Limita APC-4 a destinos versionados, links opacos, clicks durables y redirect seguro.
  description: |
    Contrato operativo de identidad partner global, programa afiliado, ofertas
    CPA, RevShare sobre NGR e Hybrid versionadas, aceptación vinculante, deals
    partner-casino y tracking opaco para Affiliate Platform Core.

    El registro público está gobernado por una política de despliegue fail-closed.
    En modo `open` crea inmediatamente la identidad, el workspace partner global,
    la membresía owner y la sesión inicial; en `closed` o `invite_only` responde
    403 antes de validar el body o crear entidades. La extensión creator histórica
    es opcional y no cambia el tipo partner. La autoridad de cada request privado
    se resuelve en el servidor desde una sesión vigente y nunca desde un tenant_id
    enviado por el cliente. Los access tokens administrativos declaran
    `aud=convray-admin`, están ligados a una sesión server-side y dejan de ser
    válidos cuando esa sesión se revoca.

    Todas las respuestas bajo `/api/v1` incluyen `Cache-Control: private,
    no-store`, `Pragma: no-cache` y `X-Content-Type-Options: nosniff` para evitar
    persistencia accidental de datos operativos en caches compartidas o del
    navegador.

    APC-3 convierte una versión publicada de oferta en una aplicación idempotente,
    conserva snapshots contractuales y permite provisioning L0/L1 con lifecycle
    seguro. APC-4 incorpora exclusivamente destinos HTTPS versionados, links
    opacos ligados a deals activos, clicks durables previos al redirect y visitor
    keys pseudónimos con retención limitada. El cálculo financiero comienza en
    APC-6.
servers:
  - url: "{protocol}://{host}:{port}/api/v1"
    description: Entorno local canónico de desarrollo
    variables:
      protocol:
        default: http
        enum:
          - http
      host:
        default: localhost
      port:
        default: "8080"
tags:
  - name: Identity
    description: Identidad partner global con alta pública gobernada por política de despliegue.
  - name: Session
    description: Creación, rotación, contexto, selección y revocación de sesión.
  - name: YouTube
    description: Conexión OAuth oficial y tenant-scoped del canal del streamer.
  - name: Kick
    description: Conexión tenant-scoped y webhooks firmados del canal Kick.
  - name: Community
    description: Comunidad, puntos y preferencias tenant-scoped del streamer.
  - name: Overlay
    description: Actividad pública de Convray seleccionada por cada streamer.
  - name: Giveaways
    description: Sorteos tenant-scoped, entradas viewer, cierre automático y selección owner auditable.
  - name: Store
    description: Catálogo y canjes tenant-scoped con fulfillment sandbox atómico e idempotente.
  - name: MiniGames
    description: Mini-juegos tenant-scoped operados exclusivamente con Puntos Convray sin valor monetario.
  - name: Platform
    description: Operaciones globales exclusivas de SuperAdmin con grant explícito y motivo auditado.
  - name: Casino
    description: Onboarding, programa afiliado, ofertas CPA/RevShare/Hybrid y Casino Landing B2B del tenant casino.
  - name: Deals
    description: Aplicaciones, aceptación vinculante y relaciones comerciales partner-casino.
  - name: Analytics
    description: Analítica first-party del Streamer Site y agregados autoritativos tenant-scoped.
  - name: Tracking
    description: Destinos versionados, links opacos y redirect público con click durable.
  - name: CRM
    description: Reporte por jugador y gráficos de las campañas de voz (voice_nexor) del CRM, sólo para cuentas de empresa.
  - name: Data
    description: Ingesta, calidad, lectura de dashboards y analítica del producto Data, detrás de la puerta de producto 'data'.
  - name: Work
    description: Tableros de trabajo por área con solicitudes cruzadas, tarjetas y posteos, detrás de la puerta de producto 'work'.
  - name: Tickets
    description: Tickets internos transversales con comentarios, disponibles en cualquier puerta.
  - name: Assistant
    description: Asistente interno de solo lectura para cuentas de empresa del casino.
  - name: Integrations
    description: Proveedores de plataforma (SendGrid, Twilio SMS, bot de Telegram) y alertas a equipos por Telegram. Solo super_admin.
  - name: Notifications
    description: Notificaciones in-app del shell para la sesión vigente.
  - name: Support
    description: Administración y API de solo lectura de soporte con token de servicio dedicado.
  - name: Retention
    description: API de retención de solo lectura para el juego Cajas Halloween (token de máquina cvj_), con la verificación de correo sin PII, y su administración en Data. Límites de la superficie, además de los de cada operación, 120 solicitudes por minuto y 20.000 por día por token, y como máximo 8 solicitudes en curso a la vez por token y 12 en toda la superficie; al excederlos responde 429 con Retry-After.
x-convray-problem-catalog:
  product_subscription_required:
    lifecycle: future-product-response
    http_status: 402
    media_type: application/problem+json
    schema:
      allOf:
        - $ref: "#/components/schemas/ProblemBase"
        - type: object
          properties:
            status:
              type: integer
              const: 402
            code:
              type: string
              const: subscription_required
    example:
      type: https://api.convray.com/problems/subscription-required
      title: Suscripción requerida
      status: 402
      detail: Activa la suscripción del tenant para usar este módulo.
      instance: /api/v1/example-product-resource
      code: subscription_required
      request_id: req_01
paths:
  /health:
    get:
      tags:
        - Platform
      operationId: getHealthLiveness
      summary: Liveness del proceso sin consultar dependencias
      x-convray-runtime-status: active-apc-core-casino-team
      description: |
        Responde 200 mientras el proceso esté vivo. No consulta la base ni
        ninguna dependencia a propósito: un incidente de base no debe reiniciar
        en cascada todos los contenedores, sólo debe dejarlos drenar.
      security: []
      responses:
        "200":
          description: El proceso está vivo
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required:
                  - status
                  - service
                properties:
                  status:
                    type: string
                    const: ok
                  service:
                    type: string
                    const: convray-api
  /health/ready:
    get:
      tags:
        - Platform
      operationId: getHealthReadiness
      summary: Readiness que verifica las dependencias críticas
      x-convray-runtime-status: active-apc-core-casino-team
      description: |
        Hace Ping con timeout corto al pool principal de PostgreSQL y al pool de
        auditoría de denegaciones. Responde 200 sólo si ambos contestan y 503
        problem+json si alguno falla, nombrando la dependencia caída en `errors`
        sin exponer el error interno.
      security: []
      responses:
        "200":
          description: Todas las dependencias respondieron a tiempo
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required:
                  - status
                  - service
                properties:
                  status:
                    type: string
                    const: ready
                  service:
                    type: string
                    const: convray-api
        "503":
          description: Una dependencia crítica no está disponible
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
  /partner-registrations:
    post:
      tags:
        - Identity
      operationId: createPartnerRegistration
      summary: Crea una cuenta partner cuando el alta pública está habilitada
      x-convray-runtime-status: gated-apc-1
      x-convray-registration-modes:
        - open
        - closed
        - invite_only
      description: |
        Sólo en modo `open`, crea atómicamente la identidad, el workspace partner
        global, la membresía owner y la sesión inicial. `creator_enabled` habilita
        la extensión creator histórica sin cambiar el tipo partner ni la autoridad
        del core. La configuración por defecto es `closed`.

        En modos `closed` o `invite_only`, responde
        `public_registration_closed` antes de decodificar o validar el body y sin
        crear usuario, tenant, membresía o sesión. El intento queda auditado. El
        login y las sesiones de cuentas existentes no cambian.

        Un payload que intente forzar el valor `casino` mediante exactamente uno
        de los aliases protegidos `role`, `account_type` o `tenant_kind` se
        rechaza con 403 antes de la validación general del payload. No se crea
        ninguna entidad parcial. Cualquier otro campo extra responde 422.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PartnerRegistrationInput"
      responses:
        "201":
          description: Cuenta partner y sesión creadas
          headers:
            Cache-Control:
              $ref: "#/components/headers/NoStore"
            Location:
              description: Contexto de sesión creado
              schema:
                type: string
                const: /api/v1/me
            Set-Cookie:
              $ref: "#/components/headers/RefreshCookie"
            X-Request-Id:
              $ref: "#/components/headers/RequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AuthResponse"
        "403":
          $ref: "#/components/responses/RegistrationForbidden"
        "409":
          $ref: "#/components/responses/PartnerRegistrationConflict"
        "422":
          $ref: "#/components/responses/ValidationFailed"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
  /auth/sessions:
    post:
      tags:
        - Session
      operationId: createSession
      summary: Inicia una sesión administrativa con email y contraseña
      x-convray-runtime-status: active-apc-1
      description: |
        Autentica sin revelar si el email existe. El servidor selecciona una
        asignación platform activa antes que una membresía tenant; una sesión
        casino ni un partner genérico requieren entitlement creator. Una
        autoridad platform nunca se obtiene de una membresía tenant.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/LoginInput"
      responses:
        "200":
          description: Sesión administrativa creada (2FA apagado o dispositivo de confianza vigente)
          headers:
            Set-Cookie:
              $ref: "#/components/headers/RefreshCookie"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AuthResponse"
        "202":
          description: |
            Contraseña válida; falta el código enviado por correo. No emite sesión ni cookies:
            confirma con POST /auth/sessions/email-otp. Si llega la cookie convray_trusted_device
            vigente del mismo usuario (y su contraseña no cambió), responde 200 sin pedir código.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EmailOTPChallenge"
        "401":
          $ref: "#/components/responses/InvalidCredentials"
        "403":
          $ref: "#/components/responses/CSRFRejected"
        "422":
          $ref: "#/components/responses/ValidationFailed"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
  /auth/sessions/email-otp:
    post:
      tags:
        - Session
      operationId: verifySessionEmailOTP
      summary: Confirma el código de inicio de sesión enviado por correo
      x-convray-runtime-status: active-auth-email-otp
      description: |
        Confirma el código de 6 dígitos del desafío y emite la sesión administrativa. Cada
        desafío admite 5 intentos y vence a los 10 minutos; es de un solo uso. Con
        trust_device=true agrega la cookie convray_trusted_device (30 días), que evita el
        código en los próximos inicios de sesión del mismo usuario hasta que cambie su contraseña.
        Errores: 401 email-otp-invalid (código incorrecto), 410 email-otp-expired (vencido,
        usado o sin intentos: volver a iniciar sesión).
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/EmailOTPVerifyInput"
      responses:
        "200":
          description: Código confirmado; sesión administrativa creada
          headers:
            Set-Cookie:
              $ref: "#/components/headers/RefreshCookie"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AuthResponse"
        "401":
          $ref: "#/components/responses/InvalidCredentials"
        "403":
          $ref: "#/components/responses/CSRFRejected"
        "410":
          $ref: "#/components/responses/EmailOTPExpired"
        "422":
          $ref: "#/components/responses/ValidationFailed"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
  /auth/sessions/email-otp/new-code:
    post:
      tags:
        - Session
      operationId: requestSessionEmailOTPCode
      summary: Reenvía el código de inicio de sesión del desafío vigente
      x-convray-runtime-status: active-auth-email-otp
      description: |
        Envía un código nuevo para el mismo desafío e invalida el anterior. Un minuto entre
        envíos (429 email-otp-cooldown con Retry-After) y máximo 3 envíos por desafío
        (429 email-otp-rate-limited). 503 email-otp-undeliverable si el proveedor de correo falla.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/EmailOTPNewCodeInput"
      responses:
        "202":
          description: Código reenviado
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EmailOTPChallenge"
        "403":
          $ref: "#/components/responses/CSRFRejected"
        "410":
          $ref: "#/components/responses/EmailOTPExpired"
        "422":
          $ref: "#/components/responses/ValidationFailed"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
  /auth/sessions/refresh:
    post:
      tags:
        - Session
      operationId: refreshSession
      summary: Rota la credencial refresh de una sesión administrativa vigente
      x-convray-runtime-status: active-m4
      description: |
        Rota la credencial opaca y emite un access token con
        `aud=convray-admin`, ligado a la misma sesión server-side. Una credencial
        revocada, expirada o reproducida responde 401 sin emitir `Set-Cookie`;
        así una respuesta obsoleta no puede borrar una rotación más reciente de
        otra pestaña.
      security:
        - adminRefreshCookie: []
      responses:
        "200":
          description: Credenciales administrativas rotadas
          headers:
            Set-Cookie:
              $ref: "#/components/headers/RefreshCookie"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AuthResponse"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/CSRFRejected"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
  /auth/sessions/current:
    patch:
      tags:
        - Session
      operationId: selectActiveMembership
      summary: Selecciona el tenant activo desde una membresía vigente
      x-convray-runtime-status: active-apc-1
      description: |
        Acepta únicamente un membership_id de la lista activa del usuario. Un
        tenant_id, streamer_id o casino_id adicional no cambia la
        autoridad. Al cambiar la membresía, el servidor rota la sesión antes de
        responder y las credenciales anteriores dejan de ser válidas.

        La operación exige simultáneamente bearer y cookie refresh vigentes,
        crea una sesión nueva y revoca las credenciales anteriores. Un partner
        genérico no recibe entitlement creator.
      security:
        - adminBearer: []
          adminRefreshCookie: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ActiveMembershipSelectionInput"
      responses:
        "200":
          description: Membresía activa seleccionada y credenciales rotadas
          headers:
            Set-Cookie:
              $ref: "#/components/headers/RefreshCookie"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AuthResponse"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
    delete:
      tags:
        - Session
      operationId: deleteCurrentSession
      summary: Revoca la sesión administrativa actual en el servidor
      x-convray-runtime-status: active-m4
      description: |
        Revoca la sesión server-side y expira la cookie. Puede autenticarse con
        la cookie refresh administrativa. La operación es idempotente cuando la
        cookie no existe. Reutilizar cualquier credencial revocada responde 401
        en refresh o en un recurso protegido posterior.
      security:
        - adminRefreshCookie: []
      responses:
        "204":
          description: Sesión revocada
          headers:
            Set-Cookie:
              $ref: "#/components/headers/ExpiredRefreshCookie"
        "403":
          $ref: "#/components/responses/CSRFRejected"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
  /me:
    get:
      tags:
        - Session
      operationId: getCurrentSessionContext
      summary: Devuelve la autoridad administrativa activa
      x-convray-runtime-status: active-m4
      description: |
        Revalida una credencial administrativa con `aud=convray-admin`, la
        sesión server-side y exactamente una rama de autoridad. Un token Viewer
        con otra audiencia no autentica este endpoint y responde 401
        `invalid_session`; no se convierte en una sesión administrativa.

        Devuelve la identidad y autoridad activa. Para `tenant_member` incluye
        el tenant partner o casino y sus membresías activas; para
        `platform_admin` los campos tenant son null porque la sesión global no
        inventa una membresía.
      security:
        - adminBearer: []
      responses:
        "200":
          description: Contexto administrativo vigente
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RuntimeUser"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
  /partner/profile:
    get:
      tags:
        - Identity
      operationId: getPartnerProfile
      summary: Lee el perfil comercial del partner activo
      x-convray-runtime-status: active-apc-1
      security:
        - adminBearer: []
      responses:
        "200":
          description: Perfil propio y canales declarados
          headers:
            ETag:
              description: Versión fuerte del agregado partner
              schema:
                type: string
                pattern: ^"[1-9][0-9]*"$
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerProfile"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
    patch:
      tags:
        - Identity
      operationId: updatePartnerProfile
      summary: Actualiza campos comerciales y reemplaza canales declarados
      x-convray-runtime-status: active-apc-1
      description: |
        Solo owner. `expected_version` evita sobrescrituras concurrentes. Un
        array `channels` presente reemplaza la colección completa; verificación,
        status, términos y capacidad creator no son editables desde este recurso.
      security:
        - adminBearer: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PartnerProfileUpdateInput"
      responses:
        "200":
          description: Perfil actualizado y evidencia persistida atómicamente
          headers:
            ETag:
              schema:
                type: string
                pattern: ^"[1-9][0-9]*"$
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerProfile"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          description: Sin permiso owner u Origin rechazado
          content:
            application/problem+json:
              schema:
                oneOf:
                  - $ref: "#/components/schemas/PermissionDeniedProblem"
                  - $ref: "#/components/schemas/CSRFRejectedProblem"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "409":
          $ref: "#/components/responses/VersionConflict"
        "422":
          $ref: "#/components/responses/ValidationFailed"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
  /platform/casinos:
    post:
      tags:
        - Platform
        - Casino
      operationId: onboardCasino
      summary: Crea el agregado casino después de registrar un contrato activo
      x-convray-runtime-status: active-phase-5
      description: |
        Operación atómica exclusiva de SuperAdmin con grant global `tenant.write`.
        Crea contrato, tenant tipado, Casino Landing draft e invitación owner. El
        token se devuelve una sola vez y solo su SHA-256 queda persistido.
      security:
        - adminBearer: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CasinoOnboardingInput"
      responses:
        "201":
          description: Casino, contrato, landing e invitación creados
          headers:
            Location:
              schema:
                type: string
                const: /api/v1/casino/dashboard
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CasinoOnboardingResult"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "409":
          $ref: "#/components/responses/VersionConflict"
        "422":
          $ref: "#/components/responses/ValidationFailed"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
  /casino-invitation-acceptances:
    post:
      tags:
        - Casino
      operationId: acceptCasinoInvitation
      summary: Acepta una invitación casino de un solo uso
      x-convray-runtime-status: active-phase-5
      description: >-
        El token y la contraseña viajan exclusivamente en el body. La web oficial
        puede recibir el token en el fragmento #token de una URL de invitación;
        ese fragmento no se envía por HTTP, se consume localmente y se elimina de
        la barra del navegador antes de ejecutar esta operación.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CasinoInvitationAcceptanceInput"
      responses:
        "201":
          description: Identidad y membresía casino activadas
          headers:
            Location:
              schema:
                type: string
                const: /api/v1/auth/sessions
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CasinoInvitationAcceptance"
        "401":
          $ref: "#/components/responses/InvalidCredentials"
        "409":
          $ref: "#/components/responses/VersionConflict"
        "410":
          description: Invitación vencida
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "422":
          $ref: "#/components/responses/ValidationFailed"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
  /deals:
    get:
      tags:
        - Deals
      operationId: listDeals
      summary: Lista la proyección allowlisted de deals del tenant efectivo
      x-convray-runtime-status: active-phase-5
      security:
        - adminBearer: []
      responses:
        "200":
          description: Solo relaciones donde participa el tenant efectivo
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DealList"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
  /casino/dashboard:
    get:
      tags:
        - Casino
      operationId: getCasinoDashboard
      summary: Devuelve el resumen privado del tenant casino efectivo
      x-convray-runtime-status: active-phase-5
      security:
        - adminBearer: []
      responses:
        "200":
          description: Identidad casino, landing privada y deals relacionados
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CasinoDashboard"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
  /casino/team:
    get:
      tags:
        - Casino
      operationId: getCasinoTeam
      summary: Lista miembros e invitaciones del casino efectivo
      x-convray-runtime-status: active-casino-team
      description: Solo Casino Admin puede consultar correos y estados del equipo. Los tokens nunca forman parte del listado.
      security:
        - adminBearer: []
      responses:
        "200":
          description: Equipo aislado al tenant de la sesión
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CasinoTeam"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
  /casino/team/invitations:
    post:
      tags:
        - Casino
      operationId: createCasinoTeamInvitation
      summary: Invita un Account Manager al casino efectivo
      x-convray-runtime-status: active-casino-team
      description: El servidor fija el rol member. El token opaco se devuelve una sola vez y solo su SHA-256 queda persistido.
      security:
        - adminBearer: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CasinoTeamInvitationCreateInput"
      responses:
        "201":
          description: Invitación creada
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CasinoTeamInvitation"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "409":
          $ref: "#/components/responses/VersionConflict"
        "422":
          $ref: "#/components/responses/ValidationFailed"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
  /casino/team/invitations/{invitation_id}:
    parameters:
      - name: invitation_id
        in: path
        required: true
        schema:
          type: string
          format: uuid
    patch:
      tags:
        - Casino
      operationId: revokeCasinoTeamInvitation
      summary: Revoca una invitación pendiente
      x-convray-runtime-status: active-casino-team
      security:
        - adminBearer: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CasinoTeamMutationInput"
      responses:
        "200":
          description: Invitación revocada
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CasinoTeamInvitation"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "409":
          $ref: "#/components/responses/VersionConflict"
        "422":
          $ref: "#/components/responses/ValidationFailed"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
  /casino/team/members/{membership_id}:
    parameters:
      - name: membership_id
        in: path
        required: true
        schema:
          type: string
          format: uuid
    patch:
      tags:
        - Casino
      operationId: updateCasinoTeamMember
      summary: Suspende, reactiva o revoca un Account Manager
      x-convray-runtime-status: active-casino-team
      description: No permite modificar owners. Cada cambio incrementa la autoridad de la membresía e invalida sus sesiones anteriores.
      security:
        - adminBearer: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CasinoTeamMemberUpdateInput"
      responses:
        "200":
          description: Membresía actualizada
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CasinoTeamMember"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "409":
          $ref: "#/components/responses/VersionConflict"
        "422":
          $ref: "#/components/responses/ValidationFailed"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
  /casino/landing/settings:
    get:
      tags:
        - Casino
      operationId: getCasinoLandingSettings
      summary: Lee el borrador privado de la Casino Landing
      x-convray-runtime-status: active-phase-5
      security:
        - adminBearer: []
      responses:
        "200":
          description: Borrador privado
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CasinoLandingSettings"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
    patch:
      tags:
        - Casino
      operationId: updateCasinoLandingSettings
      summary: Guarda, publica o despublica la Casino Landing
      x-convray-runtime-status: active-phase-5
      security:
        - adminBearer: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CasinoLandingUpdateInput"
      responses:
        "200":
          description: Borrador actualizado; publicar reemplaza el snapshot atómicamente
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CasinoLandingSettings"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "409":
          $ref: "#/components/responses/VersionConflict"
        "422":
          $ref: "#/components/responses/ValidationFailed"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
  /casino/programs:
    get:
      tags:
        - Casino
      operationId: listAffiliatePrograms
      summary: Lista los programas del casino efectivo
      x-convray-runtime-status: active-apc-2
      security:
        - adminBearer: []
      responses:
        "200":
          description: Catálogo privado limitado a 100 programas
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AffiliateProgramList"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
    post:
      tags:
        - Casino
      operationId: createAffiliateProgram
      summary: Crea un programa afiliado para el casino efectivo
      x-convray-runtime-status: active-apc-2
      description: El tenant y el actor se derivan de la sesión; el body no acepta campos de autoridad.
      security:
        - adminBearer: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AffiliateProgramCreateInput"
      responses:
        "201":
          description: Programa creado
          headers:
            Location:
              schema:
                type: string
                pattern: ^/api/v1/casino/programs/[0-9a-f-]{36}$
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AffiliateProgram"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "409":
          $ref: "#/components/responses/VersionConflict"
        "422":
          $ref: "#/components/responses/ValidationFailed"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
  /casino/programs/{program_id}:
    parameters:
      - name: program_id
        in: path
        required: true
        schema:
          type: string
          format: uuid
    patch:
      tags:
        - Casino
      operationId: updateAffiliateProgram
      summary: Actualiza por versión optimista un programa propio
      x-convray-runtime-status: active-apc-2
      security:
        - adminBearer: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AffiliateProgramUpdateInput"
      responses:
        "200":
          description: Programa actualizado
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AffiliateProgram"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "409":
          $ref: "#/components/responses/VersionConflict"
        "422":
          $ref: "#/components/responses/ValidationFailed"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
  /casino/offers:
    get:
      tags:
        - Casino
      operationId: listCasinoOffers
      summary: Lista ofertas y su historial inmutable del casino efectivo
      x-convray-runtime-status: active-apc-2
      security:
        - adminBearer: []
      responses:
        "200":
          description: Hasta 100 ofertas privadas con versiones descendentes
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OfferList"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
    post:
      tags:
        - Casino
      operationId: createCasinoOffer
      summary: Crea la identidad estable de una oferta
      x-convray-runtime-status: active-apc-2
      security:
        - adminBearer: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/OfferCreateInput"
      responses:
        "201":
          description: Oferta draft sin versiones
          headers:
            Location:
              schema:
                type: string
                pattern: ^/api/v1/casino/offers/[0-9a-f-]{36}$
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Offer"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "409":
          $ref: "#/components/responses/VersionConflict"
        "422":
          $ref: "#/components/responses/ValidationFailed"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
  /casino/offers/{offer_id}/versions:
    parameters:
      - name: offer_id
        in: path
        required: true
        schema:
          type: string
          format: uuid
    post:
      tags:
        - Casino
      operationId: createOfferVersion
      summary: Crea la siguiente versión inmutable de una oferta
      x-convray-runtime-status: active-apc-2.1
      security:
        - adminBearer: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/OfferVersionCreateInput"
            examples:
              cpa:
                summary: CPA fijo por adquisición
                value:
                  commercial_model: cpa
                  cpa_amount_minor: 2500
                  criteria_summary: Primer depósito confirmado por el casino
                  terms_version: offer-v1
                  terms_url: https://casino.example/cpa-terms
                  valid_from: 2026-07-21T00:00:00Z
              revshare:
                summary: RevShare contractual sobre NGR
                value:
                  commercial_model: revshare
                  revshare_bps: 3000
                  criteria_summary: 30% del NGR atribuible al partner
                  terms_version: offer-v1
                  terms_url: https://casino.example/revshare-terms
                  valid_from: 2026-07-21T00:00:00Z
              hybrid:
                summary: CPA más RevShare contractual sobre NGR
                value:
                  commercial_model: hybrid
                  cpa_amount_minor: 1500
                  revshare_bps: 2000
                  criteria_summary: CPA inicial más 20% del NGR atribuible
                  terms_version: offer-v1
                  terms_url: https://casino.example/hybrid-terms
                  valid_from: 2026-07-21T00:00:00Z
      responses:
        "201":
          description: Versión consecutiva creada; la moneda se deriva del programa
          headers:
            Location:
              schema:
                type: string
                pattern: ^/api/v1/casino/offer-versions/[0-9a-f-]{36}$
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OfferVersion"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "409":
          $ref: "#/components/responses/VersionConflict"
        "422":
          $ref: "#/components/responses/ValidationFailed"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
  /casino/offer-versions/{version_id}/publications:
    parameters:
      - name: version_id
        in: path
        required: true
        schema:
          type: string
          format: uuid
    post:
      tags:
        - Casino
      operationId: publishOfferVersion
      summary: Publica una versión exactamente una vez
      x-convray-runtime-status: active-apc-2.1
      description: Requiere que el programa esté published y actualiza el puntero estable de la oferta.
      security:
        - adminBearer: []
      responses:
        "201":
          description: Publicación inmutable creada
          headers:
            Location:
              schema:
                type: string
                pattern: ^/api/v1/casino/offer-versions/[0-9a-f-]{36}/publications$
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Offer"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "409":
          $ref: "#/components/responses/VersionConflict"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
  /partner/programs:
    get:
      tags:
        - Deals
      operationId: getPartnerProgramCandidate
      summary: Resuelve una oferta publicada antes de aplicar
      x-convray-runtime-status: active-apc-3
      description: |
        Devuelve snapshots contractuales y elegibilidad para el partner efectivo.
        Consultar no crea una aplicación ni acepta términos.
      security:
        - adminBearer: []
      parameters:
        - name: casino
          in: query
          required: true
          schema:
            type: string
            minLength: 3
            maxLength: 63
            pattern: ^[a-z0-9]+(?:-[a-z0-9]+)*$
        - name: offer
          in: query
          required: true
          schema:
            type: string
            minLength: 3
            maxLength: 63
            pattern: ^[a-z0-9]+(?:-[a-z0-9]+)*$
      responses:
        "200":
          description: Oferta y elegibilidad vigentes para el partner
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerProgramCandidate"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "409":
          $ref: "#/components/responses/VersionConflict"
        "422":
          $ref: "#/components/responses/ValidationFailed"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
  /partner/deals:
    get:
      tags:
        - Deals
      operationId: listPartnerDeals
      summary: Lista los deals del partner efectivo
      x-convray-runtime-status: active-apc-3
      security:
        - adminBearer: []
      parameters:
        - $ref: "#/components/parameters/RelationshipLimit"
        - $ref: "#/components/parameters/RelationshipCursor"
        - $ref: "#/components/parameters/RelationshipStatus"
      responses:
        "200":
          description: Página de deals aislada al partner derivado de la sesión
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RelationshipDealPage"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "422":
          $ref: "#/components/responses/ValidationFailed"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
    post:
      tags:
        - Deals
      operationId: createPartnerDeal
      summary: Acepta términos y crea una aplicación idempotente
      x-convray-runtime-status: active-apc-3
      description: |
        Partner, actor, timestamps y estado inicial se derivan en el servidor. La
        misma clave y payload devuelve el deal existente sin duplicar evidencia.
        Un partner `managed_exclusive` solo puede crear el deal con su
        `home_casino_tenant_id`; el cliente no puede cambiar esa autoridad.
      security:
        - adminBearer: []
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PartnerDealCreateInput"
      responses:
        "201":
          description: Aplicación creada o replay idempotente confirmado
          headers:
            Location:
              schema:
                type: string
                pattern: ^/api/v1/partner/deals/[0-9a-f-]{36}$
            ETag:
              schema:
                type: string
                pattern: ^"[1-9][0-9]*"$
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RelationshipDeal"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          description: Actor sin permiso o partner no activo
          content:
            application/problem+json:
              schema:
                oneOf:
                  - $ref: "#/components/schemas/PermissionDeniedProblem"
                  - $ref: "#/components/schemas/PartnerNotActiveProblem"
                  - $ref: "#/components/schemas/ManagedExclusiveCasinoProblem"
              examples:
                permissionDenied:
                  value:
                    type: https://api.convray.com/problems/permission-denied
                    title: Acción no permitida
                    status: 403
                    detail: No tienes permiso para realizar esta acción.
                    instance: /api/v1/partner/deals
                    code: permission_denied
                    request_id: req_01
                partnerNotActive:
                  value:
                    type: https://api.convray.com/problems/partner-not-active
                    title: Partner no activo
                    status: 403
                    detail: El partner debe estar activo para aplicar a una oferta.
                    instance: /api/v1/partner/deals
                    code: partner_not_active
                    request_id: req_01
                managedExclusiveCasino:
                  value:
                    type: https://api.convray.com/problems/managed-exclusive-casino
                    title: Casino administrado
                    status: 403
                    detail: Este partner opera exclusivamente con el casino asignado por su organización.
                    instance: /api/v1/partner/deals
                    code: managed_exclusive_casino
                    request_id: req_01
        "409":
          $ref: "#/components/responses/VersionConflict"
        "422":
          $ref: "#/components/responses/RelationshipRuleViolation"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
  /partner/deals/{deal_id}:
    parameters:
      - $ref: "#/components/parameters/RelationshipDealId"
    get:
      tags:
        - Deals
      operationId: getPartnerDeal
      summary: Lee un deal propio del partner
      x-convray-runtime-status: active-apc-3
      security:
        - adminBearer: []
      responses:
        "200":
          description: Deal con snapshots y acciones permitidas por el servidor
          headers:
            ETag:
              schema:
                type: string
                pattern: ^"[1-9][0-9]*"$
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RelationshipDeal"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
    patch:
      tags:
        - Deals
      operationId: cancelPartnerDeal
      summary: Cancela una aplicación propia aún submitted
      x-convray-runtime-status: active-apc-3
      security:
        - adminBearer: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PartnerDealCancelInput"
      responses:
        "200":
          description: Aplicación cancelada con versión incrementada
          headers:
            ETag:
              schema:
                type: string
                pattern: ^"[1-9][0-9]*"$
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RelationshipDeal"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "409":
          $ref: "#/components/responses/VersionConflict"
        "422":
          $ref: "#/components/responses/ValidationFailed"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
  /casino/deals:
    get:
      tags:
        - Deals
      operationId: listCasinoPartnerDeals
      summary: Lista aplicaciones y relaciones del casino efectivo
      x-convray-runtime-status: active-apc-3
      security:
        - adminBearer: []
      parameters:
        - $ref: "#/components/parameters/RelationshipLimit"
        - $ref: "#/components/parameters/RelationshipCursor"
        - $ref: "#/components/parameters/RelationshipStatus"
      responses:
        "200":
          description: Página de deals aislada al casino derivado de la sesión
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RelationshipDealPage"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "422":
          $ref: "#/components/responses/ValidationFailed"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
  /casino/deals/{deal_id}:
    parameters:
      - $ref: "#/components/parameters/RelationshipDealId"
    get:
      tags:
        - Deals
      operationId: getCasinoPartnerDeal
      summary: Lee un deal del casino efectivo
      x-convray-runtime-status: active-apc-3
      security:
        - adminBearer: []
      responses:
        "200":
          description: Deal con snapshots y acciones según rol vigente
          headers:
            ETag:
              schema:
                type: string
                pattern: ^"[1-9][0-9]*"$
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RelationshipDeal"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
    patch:
      tags:
        - Deals
      operationId: transitionCasinoPartnerDeal
      summary: Transiciona y provisiona un deal como Casino Admin
      x-convray-runtime-status: active-apc-3
      description: |
        Usa expected_version. Activar requiere mapping L0/L1 no secreto; Casino
        Account Manager conserva acceso de lectura pero no puede mutar.
      security:
        - adminBearer: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CasinoDealTransitionInput"
      responses:
        "200":
          description: Deal transicionado con versión incrementada
          headers:
            ETag:
              schema:
                type: string
                pattern: ^"[1-9][0-9]*"$
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RelationshipDeal"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "409":
          $ref: "#/components/responses/VersionConflict"
        "422":
          $ref: "#/components/responses/ValidationFailed"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
  /casino/tracking-destinations:
    get:
      tags:
        - Tracking
      operationId: listCasinoTrackingDestinations
      summary: Lista destinos estables del casino efectivo
      x-convray-runtime-status: active-apc-4
      description: Casino Admin y Casino Account Manager tienen lectura. La sesión resuelve el casino; no se acepta autoridad tenant en query o body.
      security:
        - adminBearer: []
      parameters:
        - $ref: "#/components/parameters/TrackingLimit"
        - $ref: "#/components/parameters/TrackingCursor"
        - $ref: "#/components/parameters/TrackingDestinationStatus"
        - $ref: "#/components/parameters/TrackingProgramId"
      responses:
        "200":
          description: Página de destinos con su versión publicada actual
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TrackingDestinationPage"
              example:
                items:
                  - id: 2d11815c-7df6-44c8-b49f-0935966cfb1b
                    program_id: a17a19b8-e8fe-489f-ab30-a495c8b92b10
                    code: latam-main
                    description: Landing principal de adquisición
                    status: published
                    current_published_version_id: 036c7863-58f2-45a4-af89-e1a637126f04
                    version: 2
                    created_at: 2026-07-21T12:00:00Z
                    updated_at: 2026-07-21T12:30:00Z
                next_cursor: null
                limit: 25
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "422":
          $ref: "#/components/responses/ValidationFailed"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
    post:
      tags:
        - Tracking
      operationId: createCasinoTrackingDestination
      summary: Crea un destino estable y su primera versión inmutable
      x-convray-runtime-status: active-apc-4
      description: Solo Casino Admin. El mismo Idempotency-Key y digest devuelve el resultado original; reutilizar la key con otro digest responde 409.
      security:
        - adminBearer: []
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TrackingDestinationCreateInput"
            example:
              program_id: a17a19b8-e8fe-489f-ab30-a495c8b92b10
              code: latam-main
              description: Landing principal de adquisición
              initial_version:
                target_url: https://offers.casino.example/register
                allowed_hosts:
                  - offers.casino.example
                parameter_template:
                  click_id: "{click_id}"
                  affiliate_ref: "{provider_account}"
                valid_from: 2026-07-21T12:00:00Z
                valid_until: null
      responses:
        "201":
          description: Identidad draft y primera versión inmutable creadas
          headers:
            Location:
              schema:
                type: string
                pattern: ^/api/v1/casino/tracking-destinations/[0-9a-f-]{36}$
            ETag:
              schema:
                type: string
                pattern: ^"[1-9][0-9]*"$
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TrackingDestinationDetail"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionOrCSRFRejected"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "409":
          $ref: "#/components/responses/IdempotencyConflict"
        "422":
          $ref: "#/components/responses/ValidationFailed"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
  /casino/tracking-destinations/{destination_id}:
    parameters:
      - $ref: "#/components/parameters/TrackingDestinationId"
    get:
      tags:
        - Tracking
      operationId: getCasinoTrackingDestination
      summary: Lee un destino estable y su versión publicada actual
      x-convray-runtime-status: active-apc-4
      description: Casino Admin y Casino Account Manager tienen lectura. Un destino ajeno se oculta como 404.
      security:
        - adminBearer: []
      responses:
        "200":
          description: Destino y versión publicada allowlisted
          headers:
            ETag:
              schema:
                type: string
                pattern: ^"[1-9][0-9]*"$
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TrackingDestinationDetail"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
    patch:
      tags:
        - Tracking
      operationId: transitionCasinoTrackingDestination
      summary: Cambia estado o versión publicada del destino
      x-convray-runtime-status: active-apc-4
      description: Solo Casino Admin. Publicar cambia atómicamente el puntero a una versión inmutable propia; pausar, reactivar o archivar conserva todo el historial.
      security:
        - adminBearer: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TrackingDestinationTransitionInput"
            example:
              status: published
              published_version_id: 036c7863-58f2-45a4-af89-e1a637126f04
              reason: Publicación aprobada para la campaña regional.
              expected_version: 1
      responses:
        "200":
          description: Destino transicionado con versión optimista incrementada
          headers:
            ETag:
              schema:
                type: string
                pattern: ^"[1-9][0-9]*"$
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TrackingDestinationDetail"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionOrCSRFRejected"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "409":
          $ref: "#/components/responses/VersionConflict"
        "422":
          $ref: "#/components/responses/ValidationFailed"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
  /casino/tracking-destinations/{destination_id}/versions:
    parameters:
      - $ref: "#/components/parameters/TrackingDestinationId"
    get:
      tags:
        - Tracking
      operationId: listCasinoTrackingDestinationVersions
      summary: Lista versiones inmutables de un destino
      x-convray-runtime-status: active-apc-4
      description: Casino Admin y Casino Account Manager tienen lectura. Las versiones se ordenan por version_number descendente.
      security:
        - adminBearer: []
      parameters:
        - $ref: "#/components/parameters/TrackingLimit"
        - $ref: "#/components/parameters/TrackingCursor"
      responses:
        "200":
          description: Página de versiones inmutables
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TrackingDestinationVersionPage"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
    post:
      tags:
        - Tracking
      operationId: createCasinoTrackingDestinationVersion
      summary: Crea una nueva versión inmutable todavía no publicada
      x-convray-runtime-status: active-apc-4
      description: Solo Casino Admin. Crear una versión no cambia el puntero publicado. El replay idempotente no crea otra versión.
      security:
        - adminBearer: []
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TrackingDestinationVersionCreateInput"
            example:
              target_url: https://offers.casino.example/register-v2
              allowed_hosts:
                - offers.casino.example
              parameter_template:
                click_id: "{click_id}"
                affiliate_ref: "{provider_account}"
              valid_from: 2026-08-01T00:00:00Z
              valid_until: null
      responses:
        "201":
          description: Siguiente versión inmutable creada
          headers:
            Location:
              schema:
                type: string
                pattern: ^/api/v1/casino/tracking-destinations/[0-9a-f-]{36}/versions$
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TrackingDestinationVersion"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionOrCSRFRejected"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "409":
          $ref: "#/components/responses/IdempotencyConflict"
        "422":
          $ref: "#/components/responses/ValidationFailed"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
  /partner/deals/{deal_id}/tracking-destinations:
    parameters:
      - $ref: "#/components/parameters/RelationshipDealId"
    get:
      tags:
        - Tracking
      operationId: listPartnerDealTrackingDestinations
      summary: Lista destinos publicados disponibles para crear links del deal
      x-convray-runtime-status: active-apc-4
      description: |
        El partner owner que opera links puede leer identidades de destino del
        programa y casino de un deal propio active. Solo incluye destinos
        published cuya versión actual exista y esté vigente. Un deal ajeno,
        inexistente o no active se oculta como 404. La proyección no expone URL
        target, hosts, templates ni datos de otros tenants.
      security:
        - adminBearer: []
      parameters:
        - $ref: "#/components/parameters/TrackingLimit"
        - $ref: "#/components/parameters/TrackingCursor"
      responses:
        "200":
          description: Página de identidades de destino elegibles para el deal
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TrackingDestinationPage"
              example:
                items:
                  - id: 2d11815c-7df6-44c8-b49f-0935966cfb1b
                    program_id: a17a19b8-e8fe-489f-ab30-a495c8b92b10
                    code: latam-main
                    description: Landing principal de adquisición
                    status: published
                    current_published_version_id: 036c7863-58f2-45a4-af89-e1a637126f04
                    version: 2
                    created_at: 2026-07-21T12:00:00Z
                    updated_at: 2026-07-21T12:30:00Z
                next_cursor: null
                limit: 25
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
  /partner/deals/{deal_id}/tracking-links:
    parameters:
      - $ref: "#/components/parameters/RelationshipDealId"
    post:
      tags:
        - Tracking
      operationId: createPartnerTrackingLink
      summary: Crea un link opaco para un deal activo propio
      x-convray-runtime-status: active-apc-4
      description: Partner owner. El servidor resuelve partner, casino y destino; un deal ajeno se oculta como 404. El replay idempotente no emite otro token.
      security:
        - adminBearer: []
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TrackingLinkCreateInput"
            example:
              destination_id: 2d11815c-7df6-44c8-b49f-0935966cfb1b
              label: Perfil principal
      responses:
        "201":
          description: Link opaco creado
          headers:
            Location:
              schema:
                type: string
                pattern: ^/api/v1/partner/tracking-links/[0-9a-f-]{36}$
            ETag:
              schema:
                type: string
                pattern: ^"[1-9][0-9]*"$
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TrackingLink"
              example:
                id: ce38f6a9-a261-447c-ac4e-850afeb09072
                deal_id: 541941f5-b6e5-4d18-8f76-9c81889661fc
                destination_id: 2d11815c-7df6-44c8-b49f-0935966cfb1b
                token: AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8
                public_url: https://go.convray.com/r/AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8
                label: Perfil principal
                resolved_destination_version_id: 036c7863-58f2-45a4-af89-e1a637126f04
                status: active
                version: 1
                created_at: 2026-07-21T13:00:00Z
                updated_at: 2026-07-21T13:00:00Z
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionOrCSRFRejected"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "409":
          $ref: "#/components/responses/IdempotencyConflict"
        "422":
          $ref: "#/components/responses/ValidationFailed"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
  /partner/tracking-links:
    get:
      tags:
        - Tracking
      operationId: listPartnerTrackingLinks
      summary: Lista links opacos propios del partner
      x-convray-runtime-status: active-apc-4
      security:
        - adminBearer: []
      parameters:
        - $ref: "#/components/parameters/TrackingLimit"
        - $ref: "#/components/parameters/TrackingCursor"
        - $ref: "#/components/parameters/TrackingDealIdQuery"
        - $ref: "#/components/parameters/TrackingLinkStatus"
      responses:
        "200":
          description: Página de links del partner derivado de la sesión
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TrackingLinkPage"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "422":
          $ref: "#/components/responses/ValidationFailed"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
  /partner/tracking-links/{link_id}:
    parameters:
      - $ref: "#/components/parameters/TrackingLinkId"
    get:
      tags:
        - Tracking
      operationId: getPartnerTrackingLink
      summary: Lee un link opaco propio
      x-convray-runtime-status: active-apc-4
      security:
        - adminBearer: []
      responses:
        "200":
          description: Link propio
          headers:
            ETag:
              schema:
                type: string
                pattern: ^"[1-9][0-9]*"$
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TrackingLink"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
    patch:
      tags:
        - Tracking
      operationId: transitionPartnerTrackingLink
      summary: Pausa, reactiva o revoca un link propio
      x-convray-runtime-status: active-apc-4
      description: Partner owner. Toda transición exige reason de 10 a 500 caracteres y expected_version; revoked es terminal.
      security:
        - adminBearer: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TrackingLinkTransitionInput"
            example:
              action: pause
              reason: Campaña pausada temporalmente por revisión.
              expected_version: 1
      responses:
        "200":
          description: Link transicionado y auditado atómicamente
          headers:
            ETag:
              schema:
                type: string
                pattern: ^"[1-9][0-9]*"$
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TrackingLink"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionOrCSRFRejected"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "409":
          $ref: "#/components/responses/VersionConflict"
        "422":
          $ref: "#/components/responses/ValidationFailed"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
  /casino/tracking-links:
    get:
      tags:
        - Tracking
      operationId: listCasinoTrackingLinks
      summary: Lista links de los deals del casino efectivo
      x-convray-runtime-status: active-apc-4
      description: Casino Admin y Casino Account Manager tienen lectura. Los filtros nunca amplían el tenant derivado de la sesión.
      security:
        - adminBearer: []
      parameters:
        - $ref: "#/components/parameters/TrackingLimit"
        - $ref: "#/components/parameters/TrackingCursor"
        - $ref: "#/components/parameters/TrackingDealIdQuery"
        - $ref: "#/components/parameters/TrackingLinkStatus"
      responses:
        "200":
          description: Página de links aislada al casino
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TrackingLinkPage"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "422":
          $ref: "#/components/responses/ValidationFailed"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
  /casino/tracking-links/{link_id}:
    parameters:
      - $ref: "#/components/parameters/TrackingLinkId"
    get:
      tags:
        - Tracking
      operationId: getCasinoTrackingLink
      summary: Lee un link de un deal del casino efectivo
      x-convray-runtime-status: active-apc-4
      description: Casino Admin y Casino Account Manager tienen lectura. Un link de otro casino se oculta como 404.
      security:
        - adminBearer: []
      responses:
        "200":
          description: Link del casino
          headers:
            ETag:
              schema:
                type: string
                pattern: ^"[1-9][0-9]*"$
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TrackingLink"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
  /r/{opaque_token}:
    parameters:
      - $ref: "#/components/parameters/OpaqueTrackingToken"
    get:
      tags:
        - Tracking
      operationId: redirectOpaqueTrackingLink
      summary: Persiste el click y redirige mediante el link opaco
      x-convray-runtime-status: active-apc-4
      description: |
        Handler backend directo. Confirma el click y la versión de destino resuelta
        con redirect_issued=true antes de escribir el 302. Puede emitir una cookie
        first-party pseudónima. Convray no solicita ni sigue el destino externo y
        no documenta un outcome remoto.
      servers:
        - url: https://go.convray.com
          description: Backend directo de producción mediante HTTPS, sin el prefijo /api/v1
      security: []
      responses:
        "302":
          description: Click durable confirmado; redirect temporal emitido
          headers:
            Location:
              description: Destino HTTPS inmediato validado contra la versión allowlisted
              required: true
              schema:
                type: string
                format: uri
                maxLength: 2048
                pattern: ^https://
              example: https://offers.casino.example/register?click_id=01K0CLICK
            Set-Cookie:
              $ref: "#/components/headers/TrackingVisitorCookie"
        "404":
          $ref: "#/components/responses/TrackingLinkNotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/TrackingRedirectUnavailable"
  /public/casinos/{casino_slug}/affiliate-program:
    parameters:
      - name: casino_slug
        in: path
        required: true
        schema:
          type: string
          pattern: ^[a-z0-9]+(?:-[a-z0-9]+)*$
    get:
      tags:
        - Casino
      operationId: getPublicAffiliateProgram
      summary: Devuelve el programa y contenido B2B del snapshot público
      x-convray-runtime-status: active-apc-2
      security: []
      responses:
        "200":
          description: Programa allowlisted sin UUID, tenant, actor ni auditoría
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PublicAffiliateProgram"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
  /public/casinos/{casino_slug}/offers:
    parameters:
      - name: casino_slug
        in: path
        required: true
        schema:
          type: string
          pattern: ^[a-z0-9]+(?:-[a-z0-9]+)*$
    get:
      tags:
        - Casino
      operationId: listPublicCasinoOffers
      summary: Lista ofertas CPA, RevShare e Hybrid vigentes del snapshot público
      x-convray-runtime-status: active-apc-2.1
      security: []
      responses:
        "200":
          description: Ofertas allowlisted sin identificadores internos
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PublicOfferList"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
  /integrations/youtube:
    get:
      tags:
        - YouTube
      operationId: getYouTubeIntegration
      summary: Consulta la conexión YouTube del tenant streamer activo
      x-convray-runtime-status: active-youtube-v1
      security:
        - adminBearer: []
          adminRefreshCookie: []
      responses:
        "200":
          description: Estado aislado de la integración; no expone credenciales OAuth
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/YouTubeIntegrationStatus"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/YouTubeForbidden"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
    delete:
      tags:
        - YouTube
      operationId: disconnectYouTubeIntegration
      summary: Desconecta el canal del tenant streamer activo
      x-convray-runtime-status: active-youtube-v1
      description: |
        Elimina localmente las credenciales cifradas y detiene futuras
        sincronizaciones. La revocación remota se intenta después de confirmar
        el cambio local y no bloquea el resto del dashboard.
      security:
        - adminBearer: []
      responses:
        "200":
          description: Conexión eliminada de forma idempotente
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/YouTubeIntegrationStatus"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/YouTubeForbidden"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
  /integrations/youtube/authorizations:
    post:
      tags:
        - YouTube
      operationId: createYouTubeAuthorization
      summary: Inicia la autorización oficial de Google con PKCE
      x-convray-runtime-status: active-youtube-v1
      description: |
        Solo el owner del tenant streamer activo puede iniciar el flujo. El
        servidor persiste un `state` de un solo uso y cifra el verifier PKCE;
        ninguno de estos secretos se devuelve salvo la URL oficial de Google.
      security:
        - adminBearer: []
      responses:
        "201":
          description: Autorización efímera creada
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/YouTubeAuthorizationStart"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/YouTubeForbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/YouTubeUnavailable"
  /integrations/youtube/sync:
    post:
      tags:
        - YouTube
      operationId: syncYouTubeCatalog
      summary: Actualiza manualmente el catálogo YouTube del tenant streamer activo
      x-convray-runtime-status: active-youtube-v1
      description: |
        Renueva el token si corresponde, consulta la playlist de subidas y
        reemplaza únicamente el catálogo público del tenant autorizado. La misma
        operación también es ejecutada por el worker periódico con lease.
      security:
        - adminBearer: []
      responses:
        "200":
          description: Catálogo actualizado o error del proveedor registrado
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/YouTubeIntegrationStatus"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/YouTubeForbidden"
        "409":
          description: Canal no conectado o sincronización ya en curso
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "502":
          description: YouTube no respondió o devolvió datos inválidos
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "503":
          $ref: "#/components/responses/YouTubeUnavailable"
  /integrations/youtube/callback:
    get:
      tags:
        - YouTube
      operationId: completeYouTubeAuthorization
      summary: Completa el callback OAuth de Google
      x-convray-runtime-status: active-youtube-v1
      description: |
        Consume una vez el `state`, intercambia el código en el backend,
        resuelve el canal mediante `channels.list(mine=true)`, importa su catálogo
        inicial y vuelve al dashboard del tenant original. Nunca recibe un
        tenant_id del cliente.
      security: []
      parameters:
        - name: state
          in: query
          required: true
          schema:
            type: string
            minLength: 43
            maxLength: 43
        - name: code
          in: query
          schema:
            type: string
        - name: error
          in: query
          schema:
            type: string
      responses:
        "303":
          description: Regreso al dashboard tenant-scoped con el resultado en query string
          headers:
            Location:
              schema:
                type: string
                format: uri
        "400":
          description: State inválido, vencido o reproducido
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "429":
          $ref: "#/components/responses/RateLimited"
        "502":
          description: Google no pudo completar el intercambio o resolver el canal
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "503":
          $ref: "#/components/responses/YouTubeUnavailable"
  /integrations/kick:
    get:
      tags:
        - Kick
      operationId: getKickIntegration
      summary: Consulta el canal Kick del tenant streamer activo
      x-convray-runtime-status: active-kick-v1
      security:
        - adminBearer: []
      responses:
        "200":
          description: Estado tenant-scoped sin access ni refresh tokens del owner
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/KickIntegrationStatus"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/KickForbidden"
        "503":
          $ref: "#/components/responses/KickUnavailable"
    delete:
      tags:
        - Kick
      operationId: disconnectKickIntegration
      summary: Elimina las suscripciones webhook y desconecta el canal
      x-convray-runtime-status: active-kick-v1
      security:
        - adminBearer: []
      responses:
        "200":
          description: Canal y regla automática de chat desconectados
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/KickIntegrationStatus"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/KickForbidden"
        "502":
          description: Kick no confirmó la eliminación de las suscripciones
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "503":
          $ref: "#/components/responses/KickUnavailable"
  /integrations/kick/authorizations:
    post:
      tags:
        - Kick
      operationId: createKickChannelAuthorization
      summary: Inicia OAuth PKCE para demostrar propiedad del canal
      x-convray-runtime-status: active-kick-v1
      description: |
        Solicita únicamente `user:read` y `channel:read`. El access token del
        owner se usa para resolver el canal y se descarta. Las suscripciones se
        crean después con un App Access Token; no se persisten tokens personales.
      security:
        - adminBearer: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/KickAuthorizationInput"
      responses:
        "201":
          description: Autorización efímera creada
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/KickAuthorizationStart"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/KickForbidden"
        "422":
          $ref: "#/components/responses/ValidationFailed"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/KickUnavailable"
  /integrations/kick/callback:
    get:
      tags:
        - Kick
      operationId: completeKickChannelAuthorization
      summary: Completa OAuth, resuelve el canal y crea suscripciones webhook
      x-convray-runtime-status: active-kick-v1
      security: []
      parameters:
        - name: state
          in: query
          required: true
          schema:
            type: string
            minLength: 43
            maxLength: 43
        - name: code
          in: query
          schema:
            type: string
        - name: error
          in: query
          schema:
            type: string
      responses:
        "303":
          description: Regreso al dashboard del tenant original con el resultado
          headers:
            Location:
              schema:
                type: string
                format: uri
        "400":
          description: State inválido, vencido o reproducido
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "409":
          description: El canal ya pertenece a otro tenant
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "429":
          $ref: "#/components/responses/RateLimited"
        "502":
          description: Kick no completó el intercambio, canal o suscripciones
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "503":
          $ref: "#/components/responses/KickUnavailable"
  /integrations/kick/webhook:
    post:
      tags:
        - Kick
      operationId: receiveKickWebhook
      summary: Recibe un evento Kick firmado e idempotente
      x-convray-runtime-status: active-kick-v1
      description: |
        Verifica RSA-SHA256 sobre message-id, timestamp RFC3339 y body crudo.
        `Kick-Event-Message-Id` es la evidencia e idempotency key. Un chat solo
        acredita puntos si el subject Kick está vinculado y el viewer confirmó
        una suscripción comunitaria activa en el mismo tenant.
      security: []
      parameters:
        - name: Kick-Event-Message-Id
          in: header
          required: true
          schema:
            type: string
            minLength: 8
            maxLength: 128
        - name: Kick-Event-Subscription-Id
          in: header
          required: true
          schema:
            type: string
            minLength: 8
            maxLength: 128
        - name: Kick-Event-Signature
          in: header
          required: true
          schema:
            type: string
            format: byte
        - name: Kick-Event-Message-Timestamp
          in: header
          required: true
          schema:
            type: string
            format: date-time
        - name: Kick-Event-Type
          in: header
          required: true
          schema:
            $ref: "#/components/schemas/KickEventType"
        - name: Kick-Event-Version
          in: header
          required: true
          schema:
            type: string
            const: "1"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "204":
          description: Evento válido procesado o duplicado reconocido
        "401":
          description: Firma, timestamp, suscripción o evidencia inválidos
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "413":
          description: Body mayor a 64 KiB
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "503":
          $ref: "#/components/responses/KickUnavailable"
  /mini-games:
    get:
      tags:
        - MiniGames
      operationId: getMiniGameOverview
      summary: Obtiene configuraciones de mini-juegos del tenant streamer activo
      x-convray-runtime-status: active-flip-dice-plinko-roulette-sandbox
      security:
        - adminBearer: []
      responses:
        "200":
          description: Configuraciones sandbox aisladas por tenant
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MiniGameOverview"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
  /mini-games/{game}/configurations:
    post:
      tags:
        - MiniGames
      operationId: createMiniGameConfiguration
      summary: Crea un borrador de configuración sandbox para un juego compatible
      x-convray-runtime-status: active-flip-dice-plinko-roulette-sandbox
      description: Solo configura costos enteros en Puntos Convray; no acepta dinero, criptoactivos, depósitos ni retiros.
      security:
        - adminBearer: []
      parameters:
        - $ref: "#/components/parameters/MiniGameKey"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/MiniGameConfigurationCreateInput"
      responses:
        "200":
          description: Replay idempotente del borrador existente
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MiniGameConfiguration"
        "201":
          description: Borrador creado y auditado
          headers:
            Location:
              schema:
                type: string
                pattern: ^/api/v1/mini-games/(flip|dice|plinko|roulette)/configurations/[0-9a-f-]{36}$
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MiniGameConfiguration"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/CSRFRejected"
        "409":
          $ref: "#/components/responses/VersionConflict"
        "422":
          $ref: "#/components/responses/ValidationFailed"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
  /mini-games/{game}/configurations/{configurationId}/publication:
    post:
      tags:
        - MiniGames
      operationId: publishMiniGameConfiguration
      summary: Publica una configuración sandbox
      x-convray-runtime-status: active-flip-dice-plinko-roulette-sandbox
      security:
        - adminBearer: []
      parameters:
        - $ref: "#/components/parameters/MiniGameKey"
        - $ref: "#/components/parameters/MiniGameConfigurationId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/MiniGameConfigurationPublicationInput"
      responses:
        "200":
          description: Configuración activa o replay de la versión publicada
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MiniGameConfiguration"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/CSRFRejected"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "409":
          $ref: "#/components/responses/VersionConflict"
        "422":
          $ref: "#/components/responses/ValidationFailed"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
  /mini-games/{game}/configurations/{configurationId}/suspension:
    post:
      tags:
        - MiniGames
      operationId: suspendMiniGameConfiguration
      summary: Suspende una configuración activa
      x-convray-runtime-status: active-flip-dice-plinko-roulette-sandbox
      security:
        - adminBearer: []
      parameters:
        - $ref: "#/components/parameters/MiniGameKey"
        - $ref: "#/components/parameters/MiniGameConfigurationId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/MiniGameConfigurationSuspensionInput"
      responses:
        "200":
          description: Configuración suspendida sin alterar rondas ya confirmadas
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MiniGameConfiguration"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/CSRFRejected"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "409":
          $ref: "#/components/responses/VersionConflict"
        "422":
          $ref: "#/components/responses/ValidationFailed"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
  /viewer/mini-games:
    get:
      tags:
        - MiniGames
      operationId: getViewerMiniGameCatalog
      summary: Obtiene un juego y el saldo privado del viewer en una comunidad
      x-convray-runtime-status: active-flip-dice-plinko-roulette-sandbox
      security:
        - viewerSessionCookie: []
      parameters:
        - $ref: "#/components/parameters/StreamerSlugQuery"
        - $ref: "#/components/parameters/MiniGameKeyQuery"
      responses:
        "200":
          description: Catálogo tenant-scoped; nunca expone saldo de otra comunidad
          headers:
            Cache-Control:
              schema:
                type: string
                const: private, no-store
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ViewerMiniGameCatalog"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /viewer/mini-games/{game}/rounds:
    post:
      tags:
        - MiniGames
      operationId: prepareMiniGameRound
      summary: Prepara una ronda y entrega su commitment previo
      x-convray-runtime-status: active-flip-dice-plinko-roulette-sandbox
      security:
        - viewerSessionCookie: []
      parameters:
        - $ref: "#/components/parameters/MiniGameKey"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/MiniGameRoundPrepareInput"
      responses:
        "200":
          description: Replay idempotente de la ronda preparada
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MiniGameRoundPrepareResult"
        "201":
          description: Ronda preparada por primera vez sin debitar puntos
          headers:
            Location:
              schema:
                type: string
                pattern: ^/api/v1/viewer/mini-games/rounds/[0-9a-f-]{36}$
            Cache-Control:
              schema:
                type: string
                const: private, no-store
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MiniGameRoundPrepareResult"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/CSRFRejected"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "409":
          $ref: "#/components/responses/VersionConflict"
        "422":
          $ref: "#/components/responses/ValidationFailed"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/FairnessUnavailable"
  /viewer/mini-games/{game}/rounds/{roundId}/plays:
    post:
      tags:
        - MiniGames
      operationId: playMiniGameRound
      summary: Liquida una ronda con puntos y proof verificable
      x-convray-runtime-status: active-flip-dice-plinko-roulette-sandbox
      description: Débito, premio bruto cuando corresponda, saldo, límites, auditoría y outbox se confirman o revierten juntos. Plinko siempre entrega el premio bruto de su bucket, incluso cuando representa una pérdida neta.
      security:
        - viewerSessionCookie: []
      parameters:
        - $ref: "#/components/parameters/MiniGameKey"
        - $ref: "#/components/parameters/MiniGameRoundId"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/MiniGameRoundPlayInput"
      responses:
        "200":
          description: Settlement inicial o replay, indicado por `replayed`
          headers:
            Cache-Control:
              schema:
                type: string
                const: private, no-store
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MiniGameRoundPlayResult"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/CSRFRejected"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "409":
          $ref: "#/components/responses/VersionConflict"
        "422":
          $ref: "#/components/responses/ValidationFailed"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/FairnessUnavailable"
  /viewer/mini-games/rounds/{roundId}:
    get:
      tags:
        - MiniGames
      operationId: getViewerMiniGameRound
      summary: Obtiene una ronda settled propia con su proof
      x-convray-runtime-status: active-flip-dice-plinko-roulette-sandbox
      security:
        - viewerSessionCookie: []
      parameters:
        - $ref: "#/components/parameters/MiniGameRoundId"
        - $ref: "#/components/parameters/StreamerSlugQuery"
      responses:
        "200":
          description: Ronda settled del viewer y tenant efectivos
          headers:
            Cache-Control:
              schema:
                type: string
                const: private, no-store
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MiniGameSettledRound"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "409":
          $ref: "#/components/responses/VersionConflict"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /store:
    get:
      tags:
        - Store
      operationId: getStoreOverview
      summary: Obtiene catálogo y órdenes de Tienda del tenant streamer activo
      x-convray-runtime-status: active-store-sandbox
      security:
        - adminBearer: []
      responses:
        "200":
          description: Hasta cien recompensas y cien órdenes aisladas por tenant
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/StoreOverview"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
  /store/rewards:
    post:
      tags:
        - Store
      operationId: createStoreReward
      summary: Crea un borrador de recompensa sandbox
      x-convray-runtime-status: active-store-sandbox
      description: |
        La etapa sandbox acepta exclusivamente fulfillment simulado. No admite
        dinero, retiros, transferencias, criptoactivos ni beneficios externos.
      security:
        - adminBearer: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/StoreRewardCreateInput"
      responses:
        "201":
          description: Borrador creado con stock reservado para el tenant
          headers:
            Location:
              schema:
                type: string
                pattern: ^/api/v1/store/rewards/[0-9a-f-]{36}$
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/StoreReward"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/CSRFRejected"
        "422":
          $ref: "#/components/responses/ValidationFailed"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
  /store/rewards/{rewardId}/publication:
    post:
      tags:
        - Store
      operationId: publishStoreReward
      summary: Publica una recompensa sandbox vigente en el Streamer Site
      x-convray-runtime-status: active-store-sandbox
      security:
        - adminBearer: []
      parameters:
        - name: rewardId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required:
                - expected_version
              properties:
                expected_version:
                  type: integer
                  minimum: 1
      responses:
        "200":
          description: Recompensa publicada con versión optimista
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/StoreReward"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/CSRFRejected"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "409":
          description: Versión desactualizada, estado no publicable o vigencia vencida
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "422":
          $ref: "#/components/responses/ValidationFailed"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
  /viewer/store/orders:
    get:
      tags:
        - Store
      operationId: listViewerStoreOrders
      summary: Lista los canjes privados del viewer en una comunidad
      x-convray-runtime-status: active-store-sandbox
      security:
        - viewerSessionCookie: []
      parameters:
        - name: streamer_slug
          in: query
          required: true
          schema:
            type: string
            pattern: ^[a-z0-9][a-z0-9-]{2,39}$
      responses:
        "200":
          description: Hasta cien órdenes propias, sin órdenes de otros viewers o tenants
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/StoreOrderCollection"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /viewer/store/redemptions:
    post:
      tags:
        - Store
      operationId: redeemStoreReward
      summary: Debita puntos, reserva stock y entrega una orden sandbox atómicamente
      x-convray-runtime-status: active-store-sandbox
      description: |
        Requiere sesión viewer, membresía activa, stock, saldo y términos vigentes.
        `Idempotency-Key` devuelve la orden original sin repetir débito ni entrega.
      security:
        - viewerSessionCookie: []
      parameters:
        - name: Idempotency-Key
          in: header
          required: true
          schema:
            type: string
            minLength: 8
            maxLength: 128
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/StoreRedemptionInput"
      responses:
        "200":
          description: Replay idempotente de una orden existente
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/StoreRedemptionResult"
        "201":
          description: Canje, débito y entrega simulada confirmados por primera vez
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/StoreRedemptionResult"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "409":
          description: Saldo, stock, límite, términos, vigencia o idempotencia incompatibles
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "422":
          $ref: "#/components/responses/ValidationFailed"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
  /giveaways:
    get:
      tags:
        - Giveaways
      operationId: listGiveaways
      summary: Lista los sorteos del tenant streamer activo
      x-convray-runtime-status: active-giveaway-foundation
      security:
        - adminBearer: []
      responses:
        "200":
          description: Hasta cien sorteos aislados por tenant
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/GiveawayCollection"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
    post:
      tags:
        - Giveaways
      operationId: createGiveaway
      summary: Crea un borrador de sorteo con premio en puntos
      x-convray-runtime-status: active-giveaway-foundation
      description: |
        Crea únicamente un borrador dentro del programa de puntos del tenant
        activo. Esta primera vertical no acepta dinero, criptoactivos, códigos,
        bienes físicos ni proveedores externos.
      security:
        - adminBearer: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/GiveawayCreateInput"
      responses:
        "201":
          description: Borrador creado y auditado
          headers:
            Location:
              schema:
                type: string
                pattern: ^/api/v1/giveaways/[0-9a-f-]{36}$
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Giveaway"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/CSRFRejected"
        "422":
          $ref: "#/components/responses/ValidationFailed"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
  /giveaways/{giveawayId}/publication:
    post:
      tags:
        - Giveaways
      operationId: publishGiveaway
      summary: Publica un borrador vigente en el Streamer Site
      x-convray-runtime-status: active-giveaway-foundation
      security:
        - adminBearer: []
      parameters:
        - name: giveawayId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required:
                - expected_version
              properties:
                expected_version:
                  type: integer
                  minimum: 1
      responses:
        "200":
          description: Sorteo publicado con versión optimista
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Giveaway"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/CSRFRejected"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "409":
          description: Versión desactualizada, estado no publicable o vigencia vencida
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "422":
          $ref: "#/components/responses/ValidationFailed"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
  /giveaways/{giveawayId}/draw:
    post:
      tags:
        - Giveaways
      operationId: drawGiveaway
      summary: Selecciona al azar los ganadores de un sorteo cerrado
      x-convray-runtime-status: active-giveaway-draw
      description: |
        Solo el owner del tenant puede ejecutar la selección después del cierre.
        Usa el snapshot inmutable de participantes, entropía del sistema operativo
        y HMAC-SHA256 versionado. Repetir la operación devuelve el mismo resultado.
      security:
        - adminBearer: []
      parameters:
        - name: giveawayId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Resultado final o replay idempotente de la selección existente
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/GiveawayDrawResult"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/CSRFRejected"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "409":
          description: El sorteo todavía no cerró o el snapshot no está disponible
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "422":
          $ref: "#/components/responses/ValidationFailed"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
  /viewer/giveaway-entries:
    get:
      tags:
        - Giveaways
      operationId: listViewerGiveawayEntries
      summary: Lista entradas y resultados privados del viewer en el Streamer Site actual
      x-convray-runtime-status: active-giveaway-entries
      security:
        - viewerSessionCookie: []
      parameters:
        - name: streamer_slug
          in: query
          required: true
          schema:
            type: string
            pattern: ^[a-z0-9][a-z0-9-]{2,39}$
      responses:
        "200":
          description: Hasta cien entradas y resultados propios, sin datos de otros viewers o tenants
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/GiveawayEntryCollection"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
    post:
      tags:
        - Giveaways
      operationId: enterGiveaway
      summary: Registra una entrada y debita puntos de forma atómica
      x-convray-runtime-status: active-giveaway-entries
      description: |
        Exige sesión viewer, membresía activa, período abierto, bases vigentes,
        confirmación de edad. `Idempotency-Key` se aplica por sorteo y
        viewer; un replay devuelve la entrada original sin un segundo débito.
      security:
        - viewerSessionCookie: []
      parameters:
        - name: Idempotency-Key
          in: header
          required: true
          schema:
            type: string
            minLength: 8
            maxLength: 128
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/GiveawayEntryCreateInput"
      responses:
        "200":
          description: Replay idempotente de una entrada existente
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/GiveawayEntryResult"
        "201":
          description: Entrada y débito confirmados por primera vez
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/GiveawayEntryResult"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "409":
          description: Período cerrado, saldo insuficiente, límite, bases cambiadas o conflicto de idempotencia
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "422":
          $ref: "#/components/responses/ValidationFailed"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
  /community/overlay-settings:
    patch:
      tags:
        - Community
        - Overlay
      operationId: updateCommunityOverlaySettings
      summary: Elige qué actividad Convray muestra el overlay del tenant activo
      x-convray-runtime-status: active-overlay-v1
      security:
        - adminBearer: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/OverlaySettingsUpdate"
      responses:
        "200":
          description: Preferencias guardadas con versión optimista
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OverlaySettings"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/CSRFRejected"
        "409":
          description: La versión de configuración quedó desactualizada
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "422":
          $ref: "#/components/responses/ValidationFailed"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
  /community/point-rules/{ruleID}:
    patch:
      tags:
        - Community
      operationId: updateCommunityPointRule
      summary: Ajusta y activa una regla de acumulación de puntos del tenant activo
      x-convray-runtime-status: active-community-v1
      security:
        - adminBearer: []
      parameters:
        - name: ruleID
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PointRuleUpdate"
      responses:
        "200":
          description: Regla guardada con versión optimista
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PointRule"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/CSRFRejected"
        "404":
          description: La regla no existe dentro de esta comunidad
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "409":
          description: La versión quedó desactualizada o la fuente no puede entregar evidencia
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "422":
          $ref: "#/components/responses/ValidationFailed"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
  /go/{referralToken}:
    get:
      tags:
        - Community
      operationId: redirectCasinoReferral
      summary: Cuenta el click y manda al casino del streamer
      description: >-
        Link público del Streamer Site. El click se registra en la misma transacción
        que emite el redirect: si no se pudo contar, la persona no sale, porque un
        contador que a veces no suma no sirve para negociar con un casino.


        El visitante recibe una cookie con material aleatorio; lo que se guarda es su
        HMAC, no la cookie, y caduca a los 90 días. Sirve para separar clicks de
        personas, no para identificar a nadie.
      x-convray-runtime-status: active-community-v1
      security: []
      parameters:
        - name: referralToken
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{43}$"
      responses:
        "302":
          description: Click contado y redirect emitido
          headers:
            Location:
              schema:
                type: string
                format: uri
            Set-Cookie:
              description: Sólo en el primer click del visitante.
              schema:
                type: string
        "404":
          description: No hay un casino activo detrás de este link
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "503":
          description: No se pudo confirmar un redirect seguro
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
  /casino-ingest/wagers:
    post:
      tags:
        - Community
      operationId: ingestCasinoWagers
      summary: Recibe un lote de wager firmado por un casino conectado
      description: >-
        No lleva sesión: el casino se autentica firmando el cuerpo. La firma es
        `HMAC-SHA256(secreto, "<connection_id>.<timestamp>.<cuerpo crudo>")` en
        hexadecimal, y el sello de tiempo debe caer dentro de los cinco minutos
        para que una request capturada no se pueda reenviar.


        Los eventos se leen del cuerpo firmado, así que alterar el JSON después de
        firmar invalida el lote entero. La idempotencia es por `external_event_id`
        dentro de la conexión: reenviar el mismo lote no vuelve a sumar y responde
        cuántos ya estaban.


        Un lote se acepta entero o se rechaza entero.
      x-convray-runtime-status: active-community-v1
      security: []
      parameters:
        - name: Convray-Connection-Id
          in: header
          required: true
          schema:
            type: string
            format: uuid
        - name: Convray-Timestamp
          in: header
          required: true
          description: Momento de la firma en RFC3339.
          schema:
            type: string
            format: date-time
        - name: Convray-Signature
          in: header
          required: true
          description: HMAC-SHA256 en hexadecimal.
          schema:
            type: string
            pattern: "^[0-9a-fA-F]{64}$"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CasinoWagerBatch"
      responses:
        "202":
          description: Lote aceptado; informa cuántos eventos eran nuevos y cuántos ya estaban
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CasinoWagerIngestResult"
        "401":
          description: La firma, el sello de tiempo o la conexión no son válidos
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "413":
          description: El lote supera el tamaño admitido
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "422":
          $ref: "#/components/responses/ValidationFailed"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
  /community/casino-connections:
    get:
      tags:
        - Community
      operationId: listCommunityCasinoConnections
      summary: Lista los casinos externos conectados por el tenant activo
      x-convray-runtime-status: active-community-v1
      security:
        - adminBearer: []
      responses:
        "200":
          description: Conexiones con su origen, estado y pista del secreto
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required:
                  - connections
                properties:
                  connections:
                    type: array
                    items:
                      $ref: "#/components/schemas/CasinoConnection"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
    post:
      tags:
        - Community
      operationId: createCommunityCasinoConnection
      summary: Conecta un casino externo y emite su secreto de ingesta
      description: >-
        El secreto en claro viaja una única vez, en esta respuesta. Después queda
        cifrado en reposo y ni el panel puede volver a leerlo.
      x-convray-runtime-status: active-community-v1
      security:
        - adminBearer: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CasinoConnectionCreate"
      responses:
        "201":
          description: Conexión creada, con el secreto de ingesta en claro
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CreatedCasinoConnection"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/CSRFRejected"
        "422":
          $ref: "#/components/responses/ValidationFailed"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
  /community/casino-connections/{connectionID}:
    patch:
      tags:
        - Community
      operationId: updateCommunityCasinoConnection
      summary: Renombra o pausa una conexión de casino
      description: >-
        La identidad y el secreto de la conexión son inmutables: apuntar a otro casino
        es crear una conexión nueva.
      x-convray-runtime-status: active-community-v1
      security:
        - adminBearer: []
      parameters:
        - name: connectionID
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CasinoConnectionUpdate"
      responses:
        "200":
          description: Conexión actualizada
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CasinoConnection"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/CSRFRejected"
        "404":
          description: La conexión no existe dentro de esta comunidad
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "409":
          description: La versión quedó desactualizada
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "422":
          $ref: "#/components/responses/ValidationFailed"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
  /community/casino-metrics:
    get:
      tags:
        - Community
      operationId: listCommunityCasinoMetrics
      summary: Clicks, visitantes y volumen apostado por casino conectado
      description: >-
        Lo que el streamer le muestra a un casino: cuánta gente le mandó y cuánto
        apostó esa gente.


        El único total agregado es el de clicks. El volumen apostado se liquida en la
        moneda de cada conexión, y el pseudónimo del visitante está atado a su
        conexión para que no se pueda cruzar entre casinos, así que ni el importe ni
        los visitantes se suman entre conexiones sin perder sentido.
      x-convray-runtime-status: active-community-v1
      security:
        - adminBearer: []
      parameters:
        - name: window_days
          in: query
          required: false
          description: Ventana en días. Cualquier otro valor cae en 30.
          schema:
            type: integer
            enum: [7, 30, 90]
            default: 30
      responses:
        "200":
          description: Métricas por conexión dentro de la ventana pedida
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CasinoMetrics"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
  /community/casino-connections/{connectionID}/referral:
    put:
      tags:
        - Community
      operationId: configureCommunityCasinoReferral
      summary: Configura el link medido hacia un casino conectado
      description: >-
        El destino no lo inventa Convray: es el link de afiliado que el streamer ya
        tiene con ese casino. Convray lo envuelve en un token propio, cuenta el
        click y redirige.


        El enlace se valida como HTTPS al puerto estándar y se comprueba que no
        resuelva a una red interna. El token se conserva entre cambios de destino
        para no romper los links ya publicados.
      x-convray-runtime-status: active-community-v1
      security:
        - adminBearer: []
      parameters:
        - name: connectionID
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CasinoReferralConfigure"
      responses:
        "200":
          description: Conexión con su link medido configurado
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CasinoConnection"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/CSRFRejected"
        "404":
          description: La conexión no existe dentro de esta comunidad
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "409":
          description: La versión quedó desactualizada
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "422":
          $ref: "#/components/responses/ValidationFailed"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
  /community/wager-races:
    get:
      tags:
        - Community
      operationId: listCommunityWagerRaces
      summary: Lista las carreras por volumen apostado del tenant activo
      x-convray-runtime-status: active-community-v1
      security:
        - adminBearer: []
      responses:
        "200":
          description: Carreras con premios y clasificación congelada si ya cerraron
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required:
                  - races
                properties:
                  races:
                    type: array
                    items:
                      $ref: "#/components/schemas/WagerRace"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
    post:
      tags:
        - Community
      operationId: createCommunityWagerRace
      summary: Crea una carrera en borrador con su ventana y su tabla de premios
      x-convray-runtime-status: active-community-v1
      security:
        - adminBearer: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/WagerRaceCreate"
      responses:
        "201":
          description: Carrera creada en borrador
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WagerRace"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/CSRFRejected"
        "422":
          $ref: "#/components/responses/ValidationFailed"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
  /community/wager-races/{raceID}:
    patch:
      tags:
        - Community
      operationId: updateCommunityWagerRace
      summary: Ajusta o activa una carrera que todavía no cerró
      description: >-
        El cierre no se pide desde acá: Convray cierra la carrera al vencer su ventana,
        congela la clasificación y acredita los premios. Una carrera cerrada es inmutable.
      x-convray-runtime-status: active-community-v1
      security:
        - adminBearer: []
      parameters:
        - name: raceID
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/WagerRaceUpdate"
      responses:
        "200":
          description: Carrera guardada con versión optimista
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WagerRace"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/CSRFRejected"
        "404":
          description: La carrera no existe dentro de esta comunidad
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "409":
          description: La versión quedó desactualizada o la carrera ya cerró
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "422":
          $ref: "#/components/responses/ValidationFailed"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
  /community/chat-code-settings:
    patch:
      tags:
        - Community
      operationId: updateCommunityChatCodeSettings
      summary: Habilita o acota la emisión de códigos desde el chat del canal
      x-convray-runtime-status: active-community-v1
      security:
        - adminBearer: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ChatCodeSettingsUpdate"
      responses:
        "200":
          description: Interruptor y techo guardados
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ChatCodeSettings"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/CSRFRejected"
        "422":
          $ref: "#/components/responses/ValidationFailed"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
  /community/redemption-codes:
    get:
      tags:
        - Community
      operationId: listCommunityRedemptionCodes
      summary: Lista los códigos de comunidad del tenant activo
      x-convray-runtime-status: active-community-v1
      security:
        - adminBearer: []
      responses:
        "200":
          description: Códigos emitidos por este streamer
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required:
                  - codes
                properties:
                  codes:
                    type: array
                    items:
                      $ref: "#/components/schemas/RedemptionCode"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
    post:
      tags:
        - Community
      operationId: createCommunityRedemptionCode
      summary: Emite un código canjeable una sola vez por viewer
      x-convray-runtime-status: active-community-v1
      security:
        - adminBearer: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RedemptionCodeCreate"
      responses:
        "201":
          description: Código emitido
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RedemptionCode"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/CSRFRejected"
        "409":
          description: Ya existe un código con ese texto en la comunidad
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "422":
          $ref: "#/components/responses/ValidationFailed"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
  /community/redemption-codes/{codeID}:
    patch:
      tags:
        - Community
      operationId: updateCommunityRedemptionCode
      summary: Ajusta la vigencia, el cupo o el estado de un código
      x-convray-runtime-status: active-community-v1
      security:
        - adminBearer: []
      parameters:
        - name: codeID
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RedemptionCodeUpdate"
      responses:
        "200":
          description: Código guardado con versión optimista
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RedemptionCode"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/CSRFRejected"
        "404":
          description: El código no existe dentro de esta comunidad
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "409":
          description: La versión quedó desactualizada
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "422":
          $ref: "#/components/responses/ValidationFailed"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
  /viewer/redemption-codes/redemptions:
    post:
      tags:
        - Community
      operationId: redeemCommunityCode
      summary: Canjea un código de comunidad con la sesión de viewer
      description: >-
        El canje es idempotente por viewer y por código. Reintentar devuelve 200 con
        replayed en true y el mismo saldo, sin acreditar de nuevo.
      x-convray-runtime-status: active-community-v1
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RedemptionCodeRedeem"
      responses:
        "201":
          description: Código acreditado
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RedemptionCodeRedeemResult"
        "200":
          description: El código ya había sido canjeado por este viewer
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RedemptionCodeRedeemResult"
        "401":
          description: La sesión de viewer no existe, expiró o fue revocada
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "403":
          $ref: "#/components/responses/CSRFRejected"
        "404":
          description: El código no existe, está deshabilitado, venció o agotó su cupo
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "409":
          description: El viewer no está suscrito a la comunidad
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "422":
          $ref: "#/components/responses/ValidationFailed"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
  /public/streamer-sites/{slug}:
    get:
      tags:
        - Community
        - Giveaways
        - Store
        - MiniGames
      operationId: getPublicStreamerSite
      summary: Resuelve el Streamer Site publicado, sus códigos de slots, sorteos y catálogo de Tienda
      x-convray-runtime-status: active-streamer-site-v1
      security: []
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
            pattern: ^[a-z0-9]+(?:-[a-z0-9]+)*$
      responses:
        "200":
          description: Snapshot público sin IDs tenant, membresías, secretos ni borradores
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PublicStreamerSite"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
  /public/streamer-sites/{slug}/events:
    post:
      tags:
        - Analytics
      operationId: recordPublicStreamerSiteEvent
      summary: Registra una impresión o interacción first-party idempotente
      description: |
        Solo acepta eventos desde el Origin web autorizado. El servidor emite
        una cookie HttpOnly aleatoria y persiste exclusivamente un digest
        tenant-scoped; no persiste IP, User-Agent, referrer ni el valor crudo
        de la cookie. `event_id` es la clave idempotente dentro del tenant.
      x-convray-runtime-status: active-streamer-site-analytics-v1
      security: []
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
            pattern: ^[a-z0-9]+(?:-[a-z0-9]+)*$
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/StreamerSiteEventInput"
            examples:
              pageView:
                value:
                  event_id: 11111111-1111-4111-8111-111111111111
                  event_type: page_view
                  surface: home
              click:
                value:
                  event_id: 22222222-2222-4222-8222-222222222222
                  event_type: click
                  surface: home
                  target_type: social
                  target_key: kick
      responses:
        "202":
          description: Evento aceptado o replay idempotente confirmado
          headers:
            Set-Cookie:
              $ref: "#/components/headers/AnalyticsVisitorCookie"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/StreamerSiteEventReceipt"
        "403":
          description: Origin no autorizado
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "409":
          $ref: "#/components/responses/VersionConflict"
        "422":
          $ref: "#/components/responses/ValidationFailed"
        "429":
          $ref: "#/components/responses/RateLimited"
  /streamer-site/analytics:
    get:
      tags:
        - Analytics
      operationId: getStreamerSiteAnalytics
      summary: Obtiene métricas first-party y actividad autoritativa del tenant streamer activo
      description: |
        Los plays se agregan desde `mini_game_rounds` settled. Entradas de
        sorteos y canjes se agregan desde sus ledgers operativos; nunca desde
        clicks reportados por el navegador. Una tasa sin denominador es null.
      x-convray-runtime-status: active-streamer-site-analytics-v1
      security:
        - adminBearer: []
      parameters:
        - name: period
          in: query
          required: false
          schema:
            type: string
            enum:
              - 7d
              - 30d
              - month
            default: 30d
      responses:
        "200":
          description: Reporte aislado al tenant derivado de la sesión
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/StreamerSiteAnalytics"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "422":
          $ref: "#/components/responses/ValidationFailed"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
  /public/streamer-sites/{slug}/overlay-events:
    get:
      tags:
        - Overlay
      operationId: getPublicConvrayOverlayEvents
      summary: Devuelve actividad pública y sanitizada de Convray para OBS
      x-convray-runtime-status: active-overlay-v1
      security: []
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
            pattern: ^[a-z0-9]+(?:-[a-z0-9]+)*$
        - name: since
          in: query
          required: false
          schema:
            type: string
            format: date-time
      responses:
        "200":
          description: Hasta 20 eventos habilitados por el streamer; nunca incluye IDs privados ni payloads de proveedores
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ConvrayOverlayFeed"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /crm/campaigns/{id}/voice-recipients:
    get:
      tags:
        - CRM
      operationId: listCRMVoiceCampaignRecipients
      summary: Detalle por jugador de una campaña de voz (voice_nexor)
      description: |
        Tabla por jugador del reporte de una campaña voice_nexor (Reactivación Nexor F2-C8). Sólo cuentas
        de empresa del casino: el producto CRM ya bloquea a un partner con 404. Nombre y apellido se
        descifran en memoria desde el snapshot cifrado de Data y NUNCA se persisten ni se logean. Los montos
        van en pesos enteros. `format=csv` devuelve el mismo detalle como descarga. Sólo aplica a campañas
        del canal voice_nexor; una campaña de email responde 422.
      x-convray-runtime-status: active-nexor-f2-c8
      security:
        - adminBearer: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: tenant
          in: query
          required: false
          description: Slug del tenant objetivo (super_admin de plataforma).
          schema:
            type: string
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 200
            default: 200
        - name: offset
          in: query
          required: false
          schema:
            type: integer
            minimum: 0
            default: 0
        - name: cursor
          in: query
          required: false
          description: Alias de offset (entero) para paginar.
          schema:
            type: string
        - name: status
          in: query
          required: false
          description: Filtra por nexor_status (estado mapeado).
          schema:
            type: string
        - name: group
          in: query
          required: false
          schema:
            type: string
            enum: [send, control]
        - name: deposited
          in: query
          required: false
          schema:
            type: boolean
        - name: format
          in: query
          required: false
          schema:
            type: string
            enum: [csv]
      responses:
        "200":
          description: Página del detalle por jugador (o CSV si format=csv)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CRMVoiceRecipientPage"
            text/csv:
              schema:
                type: string
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
  /crm/campaigns/{id}/voice-breakdown:
    get:
      tags:
        - CRM
      operationId: getCRMVoiceCampaignBreakdown
      summary: Agregados para los gráficos del reporte de voz (sin PII)
      description: |
        Distribución de estados finales del grupo send y reactivados por día civil (America/Santiago) de una
        campaña voice_nexor. Sin PII. Sólo aplica al canal voice_nexor; email responde 422.
      x-convray-runtime-status: active-nexor-f2-c8
      security:
        - adminBearer: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: tenant
          in: query
          required: false
          schema:
            type: string
      responses:
        "200":
          description: Agregados de estados y reactivados por día
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CRMVoiceBreakdown"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /data/imports:
    get:
      tags:
        - Data
      operationId: listDataImports
      summary: Lista los lotes de importación del tenant
      x-convray-runtime-status: active-data-v1
      description: >-
        Devuelve los lotes de importación (batches) del tenant, sin la lista de
        rechazos (esa se pide en el detalle). Superficie Data: acepta JWT o token
        personal cvr_. Detrás de la puerta de producto 'data': si el header
        X-Convray-Product no coincide, responde 404.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: tenant
          in: query
          required: false
          description: Slug del tenant cuando la sesión abarca varios.
          schema:
            type: string
        - name: dataset
          in: query
          required: false
          description: Filtra por código de dataset (por ejemplo deposits).
          schema:
            type: string
        - name: limit
          in: query
          required: false
          schema:
            type: integer
      responses:
        "200":
          description: Lotes del tenant
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataImportListResponse"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
    post:
      tags:
        - Data
      operationId: createDataImport
      summary: Sube un archivo para importar (asíncrono) o calcula su preview
      x-convray-runtime-status: active-data-v1
      description: >-
        Recibe un archivo CSV o ZIP en el campo multipart 'file' y encola un lote
        de importación asíncrono (responde 202 con el batch en estado received o
        duplicate). Con ?preview=1 calcula el bloque de calidad SIN encolar ni
        guardar (responde 200 con quality). Para el dataset player_period_totals
        los campos period_from y period_to (YYYY-MM-DD) son obligatorios porque el
        CSV no trae fecha. Superficie Data: acepta JWT o token personal cvr_.
        Detrás de la puerta de producto 'data': si el header X-Convray-Product no
        coincide, responde 404.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: tenant
          in: query
          required: false
          schema:
            type: string
        - name: preview
          in: query
          required: false
          description: Con valor 1 calcula solo el bloque de calidad, sin importar.
          schema:
            type: string
            enum:
              - "1"
        - name: force
          in: query
          required: false
          description: Con valor 1 saltea el bloqueo de "período ya cubierto".
          schema:
            type: string
            enum:
              - "1"
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required:
                - file
              properties:
                file:
                  type: string
                  format: binary
                  description: Archivo CSV o ZIP a importar.
                dataset:
                  type: string
                  description: Código del dataset; por defecto deposits.
                period_from:
                  type: string
                  format: date
                  description: Inicio del período (obligatorio en player_period_totals).
                period_to:
                  type: string
                  format: date
                  description: Fin del período (obligatorio en player_period_totals).
      responses:
        "200":
          description: Bloque de calidad calculado (modo preview)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataImportPreviewResponse"
        "202":
          description: Lote encolado (received o duplicate)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataImportResponse"
        "400":
          description: Archivo faltante o inválido en el campo 'file'
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "409":
          description: >-
            El período ya está cubierto (código already_covered). El cuerpo
            problem+json incluye además el bloque quality para mostrar el aviso y
            el botón "Subir de todos modos" (force=1).
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/DataImportQualityBlockProblem"
        "413":
          description: El archivo supera el límite permitido
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "422":
          description: >-
            Datos inválidos (columnas faltantes con quality embebido, dataset
            desconocido o período requerido).
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/DataImportQualityBlockProblem"
        "503":
          description: Importación deshabilitada (falta DATA_IMPORT_DIR)
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
  /data/imports/{id}:
    get:
      tags:
        - Data
      operationId: getDataImport
      summary: Obtiene el detalle de un lote de importación
      x-convray-runtime-status: active-data-v1
      description: >-
        Devuelve un lote de importación con sus filas rechazadas. Superficie Data:
        acepta JWT o token personal cvr_. Detrás de la puerta de producto 'data':
        si el header X-Convray-Product no coincide, responde 404.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
        - name: tenant
          in: query
          required: false
          schema:
            type: string
      responses:
        "200":
          description: Detalle del lote
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataImportResponse"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
  /data/imports/{id}/retry:
    post:
      tags:
        - Data
      operationId: retryDataImport
      summary: Reintenta un lote de importación fallido
      x-convray-runtime-status: active-data-v1
      description: >-
        Reencola un lote de importación para volver a procesarlo y devuelve el
        batch actualizado. Superficie Data: acepta JWT o token personal cvr_.
        Detrás de la puerta de producto 'data': si el header X-Convray-Product no
        coincide, responde 404.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
        - name: tenant
          in: query
          required: false
          schema:
            type: string
      responses:
        "200":
          description: Lote reencolado
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataImportResponse"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
  /data/datasets:
    get:
      tags:
        - Data
      operationId: listDataDatasets
      summary: Lista los datasets del tenant con su perfil y freshness
      x-convray-runtime-status: active-data-v1
      description: >-
        Devuelve los datasets disponibles del tenant con su catálogo de columnas y
        su estado de frescura. Superficie Data: acepta JWT o token personal cvr_.
        Detrás de la puerta de producto 'data': si el header X-Convray-Product no
        coincide, responde 404.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: tenant
          in: query
          required: false
          schema:
            type: string
      responses:
        "200":
          description: Datasets del tenant
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataDatasetListResponse"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
  /data/datasets/detect:
    post:
      tags:
        - Data
      operationId: detectDataDataset
      summary: Detecta el dataset más probable a partir de un encabezado
      x-convray-runtime-status: active-data-v1
      description: >-
        Recibe el encabezado a analizar (archivo CSV en el campo multipart 'file' o
        un cuerpo JSON con {"header": [...]}) y devuelve el dataset más probable,
        su confianza y las columnas requeridas faltantes. Superficie Data: acepta
        JWT o token personal cvr_. Detrás de la puerta de producto 'data': si el
        header X-Convray-Product no coincide, responde 404.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: tenant
          in: query
          required: false
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/DataDatasetDetectRequest"
          multipart/form-data:
            schema:
              type: object
              properties:
                file:
                  type: string
                  format: binary
                  description: CSV del que se leen las primeras líneas como encabezado.
      responses:
        "200":
          description: Dataset detectado
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataDatasetDetection"
        "400":
          description: Cuerpo o archivo inválido
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
  /data/exports:
    get:
      tags:
        - Data
      operationId: listDataExports
      summary: Lista el historial de exportaciones del tenant
      x-convray-runtime-status: active-data-v1
      description: >-
        Devuelve las últimas exportaciones registradas del tenant. Superficie Data:
        acepta JWT o token personal cvr_. Detrás de la puerta de producto 'data':
        si el header X-Convray-Product no coincide, responde 404.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: tenant
          in: query
          required: false
          schema:
            type: string
        - name: dataset
          in: query
          required: false
          schema:
            type: string
        - name: limit
          in: query
          required: false
          schema:
            type: integer
      responses:
        "200":
          description: Historial de exportaciones
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataExportListResponse"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
    post:
      tags:
        - Data
      operationId: createDataExport
      summary: Registra una exportación realizada en el navegador
      x-convray-runtime-status: active-data-v1
      description: >-
        Registra en el historial una exportación (por ejemplo "Exportar selección")
        que el navegador ya generó. No devuelve cuerpo (201). Superficie Data:
        acepta JWT o token personal cvr_. Detrás de la puerta de producto 'data':
        si el header X-Convray-Product no coincide, responde 404.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: tenant
          in: query
          required: false
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateDataExportRequest"
      responses:
        "201":
          description: Exportación registrada (sin cuerpo)
        "400":
          description: Cuerpo JSON inválido
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /data/exports/kpis:
    get:
      tags:
        - Data
      operationId: getDataExportKpis
      summary: Cifras del encabezado del módulo Exportaciones
      x-convray-runtime-status: active-data-v1
      description: >-
        Exportaciones del mes en curso (día civil America/Santiago) con la variación
        respecto al mes anterior, filas exportadas, exportaciones con datos personales
        y personas distintas que exportaron. Detrás de la puerta de producto 'data'.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: tenant
          in: query
          required: false
          schema:
            type: string
      responses:
        "200":
          description: Cifras de exportaciones
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataExportKpisResponse"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
  /data/exports/{id}:
    get:
      tags:
        - Data
      operationId: getDataExportDetail
      summary: Detalle de una exportación con su historial
      x-convray-runtime-status: active-data-v1
      description: >-
        Una exportación con sus campos completos y su historial de eventos
        (solicitada / generada / descargada / revocada). Detrás de la puerta de
        producto 'data'.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
        - name: tenant
          in: query
          required: false
          schema:
            type: string
      responses:
        "200":
          description: Detalle de la exportación
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataExportDetailResponse"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
  /data/exports/{id}/repeat:
    post:
      tags:
        - Data
      operationId: repeatDataExport
      summary: Repite una exportación con sus filtros guardados
      x-convray-runtime-status: active-data-v1
      description: >-
        Devuelve el dataset y los filtros guardados para RE-EJECUTAR la exportación
        con esos filtros (el cliente re-navega al módulo). Valida el acceso vigente:
        si el usuario ya no puede ver Data, responde 404. Detrás de la puerta 'data'.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
        - name: tenant
          in: query
          required: false
          schema:
            type: string
      responses:
        "200":
          description: Dataset y filtros para re-ejecutar
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataExportRepeatResponse"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
  /data/exports/{id}/revoke:
    post:
      tags:
        - Data
      operationId: revokeDataExport
      summary: Revoca una exportación en el registro
      x-convray-runtime-status: active-data-v1
      description: >-
        Anota que la exportación quedó revocada (evento append-only; el estado
        efectivo pasa a revoked). Como la descarga es directa (sin enlace
        persistente), es una marca de gobierno del registro. Idempotente. Detrás de
        la puerta de producto 'data'.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
        - name: tenant
          in: query
          required: false
          schema:
            type: string
      responses:
        "200":
          description: Ficha actualizada de la exportación
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataExportDetailResponse"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
  /data/quality/alerts:
    get:
      tags:
        - Data
      operationId: listDataQualityAlerts
      summary: Lista las alertas de calidad de datos
      x-convray-runtime-status: active-data-v1
      description: >-
        Devuelve las alertas del centro de calidad de datos con sus totales y si el
        usuario puede administrarlas. Superficie Data: acepta JWT o token personal
        cvr_. Detrás de la puerta de producto 'data': si el header
        X-Convray-Product no coincide, responde 404.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: tenant
          in: query
          required: false
          schema:
            type: string
        - name: dataset
          in: query
          required: false
          schema:
            type: string
        - name: status
          in: query
          required: false
          description: Filtra por estado (open, resolved, dismissed).
          schema:
            type: string
        - name: severity
          in: query
          required: false
          description: Filtra por gravedad (info, warning, critical).
          schema:
            type: string
        - name: limit
          in: query
          required: false
          schema:
            type: integer
      responses:
        "200":
          description: Alertas de calidad
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataQualityAlertListResponse"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
  /data/quality/alerts/{id}:
    patch:
      tags:
        - Data
      operationId: patchDataQualityAlert
      summary: Cambia el estado de una alerta de calidad
      x-convray-runtime-status: active-data-v1
      description: >-
        Actualiza el estado de una alerta (open, resolved o dismissed) y devuelve
        la alerta actualizada. Superficie Data: acepta JWT o token personal cvr_.
        Detrás de la puerta de producto 'data': si el header X-Convray-Product no
        coincide, responde 404.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
        - name: tenant
          in: query
          required: false
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PatchDataQualityAlertRequest"
      responses:
        "200":
          description: Alerta actualizada
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataQualityAlertResponse"
        "400":
          description: Cuerpo JSON inválido
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /players/flags:
    get:
      tags:
        - Data
      operationId: listPlayerFlags
      summary: Marcas (badges) de jugadores por Player ID
      x-convray-runtime-status: active-data-v1
      description: >-
        Endpoint TRANSVERSAL de solo marcas (PRD-50 Fase 6): dado un conjunto de Player IDs
        (external_player_ref, separados por coma, máximo 200), devuelve por ref sus PlayerFlags
        para pintar los badges del jugador en cualquier producto (APP/DATA/CRM/WORK) sin abrir
        Data ni exponer PII. Va detrás de JWT y SIN puerta de producto: se sirve por el mismo
        origen de cualquier subdominio del producto. Lo autoriza una función DEFINER que exige
        una cuenta de empresa del tenant y filtra por tenant; un partner del portal recibe 404.
      security:
        - adminBearer: []
      parameters:
        - name: tenant
          in: query
          required: false
          description: >-
            Slug del tenant. Obligatorio para el super_admin; el miembro resuelve el suyo.
          schema:
            type: string
        - name: refs
          in: query
          required: true
          description: >-
            Player IDs (external_player_ref) separados por coma. Máximo 200; deduplica y descarta
            vacíos. Vacío o más de 200 responde 422.
          schema:
            type: string
      responses:
        "200":
          description: Marcas por Player ID (solo las refs con alguna marca).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PlayerFlagsResponse"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /data/vip-payment-alerts:
    get:
      tags:
        - Data
      operationId: listVipPaymentAlerts
      summary: Lista las alertas preventivas de pagos VIP
      x-convray-runtime-status: active-data-v1
      description: >-
        Devuelve las alertas preventivas de pagos VIP (PRD-50 Fase 5) del estado
        pedido, con las banderas del jugador para pintar los badges. Superficie
        Data: acepta JWT o token personal cvr_. Detrás de la puerta de producto
        'data': si el header X-Convray-Product no coincide, responde 404.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: tenant
          in: query
          required: false
          schema:
            type: string
        - name: status
          in: query
          required: false
          description: Filtra por estado (open, reviewed, resolved, all). Por defecto open.
          schema:
            type: string
      responses:
        "200":
          description: Alertas de pagos VIP
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/VipPaymentAlertListResponse"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
  /data/vip-payment-alerts/{id}:
    patch:
      tags:
        - Data
      operationId: reviewVipPaymentAlert
      summary: Marca revisada una alerta de pagos VIP
      x-convray-runtime-status: active-data-v1
      description: >-
        Marca una alerta como revisada con una nota (rol mínimo admin de Data) y
        devuelve la alerta actualizada. Superficie Data: acepta JWT o token
        personal cvr_. Detrás de la puerta de producto 'data': si el header
        X-Convray-Product no coincide, responde 404.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
        - name: tenant
          in: query
          required: false
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ReviewVipPaymentAlertRequest"
      responses:
        "200":
          description: Alerta actualizada
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/VipPaymentAlertResponse"
        "400":
          description: Cuerpo JSON inválido
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /data/vip-alert-settings:
    get:
      tags:
        - Data
      operationId: getVipAlertSettings
      summary: Ajustes de alertas de pagos VIP
      x-convray-runtime-status: active-data-v1
      description: >-
        Devuelve los umbrales y canales de las alertas de pagos VIP del casino
        (rol mínimo admin de Data). Superficie Data: acepta JWT o token personal
        cvr_. Detrás de la puerta de producto 'data': si el header
        X-Convray-Product no coincide, responde 404.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: tenant
          in: query
          required: false
          schema:
            type: string
      responses:
        "200":
          description: Ajustes de alertas VIP
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/VipAlertSettingsResponse"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
    put:
      tags:
        - Data
      operationId: putVipAlertSettings
      summary: Edita los ajustes de alertas de pagos VIP
      x-convray-runtime-status: active-data-v1
      description: >-
        Actualiza los umbrales y canales de las alertas de pagos VIP del casino
        (rol mínimo admin de Data) y devuelve los ajustes resultantes. Superficie
        Data: acepta JWT o token personal cvr_. Detrás de la puerta de producto
        'data': si el header X-Convray-Product no coincide, responde 404.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: tenant
          in: query
          required: false
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PutVipAlertSettingsRequest"
      responses:
        "200":
          description: Ajustes actualizados
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/VipAlertSettingsResponse"
        "400":
          description: Cuerpo JSON inválido
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /data/vip-program/settings:
    get:
      tags:
        - Data
      operationId: getVipProgramSettings
      summary: Ajustes del programa de detección temprana VIP
      x-convray-runtime-status: active-data-v1
      description: >-
        Devuelve los umbrales, la ventana de scoring, el porcentaje de control A/B y la semilla del
        programa de detección temprana de jugadores VIP del casino. Cualquier acceso a Data (rol mínimo
        viewer) puede leerlos. Superficie Data: acepta JWT o token personal cvr_. Detrás de la puerta de
        producto 'data': si el header X-Convray-Product no coincide, responde 404.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: tenant
          in: query
          required: false
          schema:
            type: string
      responses:
        "200":
          description: Ajustes del programa VIP
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/VipProgramSettingsResponse"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
    put:
      tags:
        - Data
      operationId: putVipProgramSettings
      summary: Edita los ajustes del programa de detección temprana VIP
      x-convray-runtime-status: active-data-v1
      description: >-
        Actualiza los umbrales, la ventana de scoring, el porcentaje de control A/B y la semilla del
        programa de detección temprana de jugadores VIP del casino y devuelve los ajustes resultantes.
        Sólo admin de Data o super_admin de plataforma. Superficie Data: acepta JWT o token personal
        cvr_. Detrás de la puerta de producto 'data': si el header X-Convray-Product no coincide,
        responde 404.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: tenant
          in: query
          required: false
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PutVipProgramSettingsRequest"
      responses:
        "200":
          description: Ajustes actualizados
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/VipProgramSettingsResponse"
        "400":
          description: Cuerpo JSON inválido
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /data/work-targets:
    get:
      tags:
        - Data
      operationId: getDataWorkTargets
      summary: Catálogo de tableros de Work para los destinos de Alertas VIP
      x-convray-runtime-status: active-data-v1
      description: >-
        Devuelve las áreas con sus tableros y columnas de Work (rol mínimo admin de
        Data) para poblar los selectores Área → Tablero → Columna de los destinos de
        las Alertas VIP. La página de Ajustes se sirve desde el host de Data y la
        puerta de producto impide llamar a /work/*, por eso el catálogo se sirve por
        esta ruta de Data. Detrás de la puerta de producto 'data': si el header
        X-Convray-Product no coincide, responde 404.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: tenant
          in: query
          required: false
          schema:
            type: string
      responses:
        "200":
          description: Catálogo de destinos de Work
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataWorkTargetsResponse"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
  /data/payment-methods/health:
    get:
      tags:
        - Data
      operationId: getPaymentMethodHealth
      summary: Salud de la efectividad por método de pago
      x-convray-runtime-status: active-data-v1
      description: >-
        Devuelve, por método de pago, la efectividad de los depósitos en la última
        ventana evaluada, la base de los días civiles cerrados, el estado del método
        y las barras por hora UTC de las últimas horas pedidas (PRD-55). La
        efectividad es pagados / (pagados + fallidos + rechazados): los pendientes y
        los cancelados por el operador quedan fuera del denominador y se informan
        aparte. No es el KPI "Conversión de depósitos" (pagados / todos). La ventana
        usa la misma regla de corte que el vigilante: E = min(ahora − asentamiento,
        cobertura de la última corrida exitosa de depósitos del conector) y la
        ventana es [E − window_minutes, E). stale indica que esa cobertura tiene más
        de 2 horas y la vigilancia está en pausa. Rol mínimo viewer de Data.
        Superficie Data: acepta JWT o token personal cvr_. Detrás de la puerta de
        producto 'data': si el header X-Convray-Product no coincide, responde 404.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: tenant
          in: query
          required: false
          schema:
            type: string
        - name: hours
          in: query
          required: false
          description: Horas de barras por método (1 a 72). Por defecto 24. Fuera de rango responde 422.
          schema:
            type: integer
            minimum: 1
            maximum: 72
            default: 24
      responses:
        "200":
          description: Salud por método de pago
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PaymentMethodHealthResponse"
              example:
                generated_at: "2026-09-25T05:10:00Z"
                window:
                  from: "2026-09-25T04:00:00Z"
                  to: "2026-09-25T05:00:00Z"
                  minutes: 60
                data_fresh_until: "2026-09-25T05:03:00Z"
                stale: false
                enabled: true
                params:
                  window_minutes: 60
                  settle_minutes: 10
                  min_attempts: 20
                  floor_pct: 50
                  critical_pct: 35
                  drop_points: 25
                  baseline_days: 7
                  baseline_min_attempts: 100
                  recovery_pct: 65
                  resolve_min_attempts: 5
                  no_traffic_hours: 3
                  muted_methods: []
                methods:
                  - payment_method: BankCard
                    current:
                      paid: 33
                      failed: 70
                      declined: 6
                      cancelled: 0
                      open: 2
                      attempts: 109
                      rate: 30.28
                      failed_without_external_ref: 76
                      players_failed: 29
                    baseline:
                      days: 7
                      rate: 79.4
                      attempts: 5120
                      valid: true
                    state: alert
                    hours:
                      - hour_start: "2026-09-25T03:00:00Z"
                        paid: 90
                        failed: 20
                        declined: 5
                        cancelled: 0
                        open: 1
                        attempts: 115
                        rate: 78.26
                      - hour_start: "2026-09-25T04:00:00Z"
                        paid: 33
                        failed: 70
                        declined: 6
                        cancelled: 0
                        open: 2
                        attempts: 109
                        rate: 30.28
                can_admin: true
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /data/payment-method-alerts:
    get:
      tags:
        - Data
      operationId: listPaymentMethodAlerts
      summary: Lista las alertas de efectividad por método de pago
      x-convray-runtime-status: active-data-v1
      description: >-
        Devuelve hasta 200 episodios de la alerta de efectividad por método de pago
        (PRD-55) del estado pedido, abiertos primero y los más recientes arriba, más
        open_count (total real de abiertos del casino, independiente del filtro) y
        can_admin. Cada episodio es de un casino y un método (a lo sumo uno abierto
        por método), solo con agregados: sin Player ID ni PII. Rol mínimo viewer de
        Data. Superficie Data: acepta JWT o token personal cvr_. Detrás de la puerta
        de producto 'data': si el header X-Convray-Product no coincide, responde 404.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: tenant
          in: query
          required: false
          schema:
            type: string
        - name: status
          in: query
          required: false
          description: Filtra por estado (open, resolved, all). Por defecto open. Otro valor responde 422.
          schema:
            type: string
            enum:
              - open
              - resolved
              - all
            default: open
      responses:
        "200":
          description: Alertas de efectividad por método de pago
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PaymentMethodAlertListResponse"
              example:
                alerts:
                  - id: 7b0c2f51-4a0e-4e7c-9a53-2f0f0c5d3a11
                    payment_method: BankCard
                    rule: approval_drop
                    severity: critical
                    status: open
                    resolution: null
                    started_at: "2026-09-25T04:00:00Z"
                    opened_at: "2026-09-25T05:05:00Z"
                    last_evaluated_at: "2026-09-25T06:10:00Z"
                    resolved_at: null
                    worst_rate: 29.3
                    worst_window_start: "2026-09-25T05:00:00Z"
                    worst_window_end: "2026-09-25T06:00:00Z"
                    detail:
                      window:
                        from: "2026-09-25T05:00:00Z"
                        to: "2026-09-25T06:00:00Z"
                        minutes: 60
                        paid: 27
                        failed: 60
                        declined: 5
                        cancelled: 0
                        open: 1
                        attempts: 92
                        rate: 29.35
                        failed_without_external_ref: 65
                        players_failed: 24
                      baseline:
                        days: 7
                        rate: 79.4
                        attempts: 5120
                        valid: true
                      episode:
                        from: "2026-09-25T04:00:00Z"
                        to: "2026-09-25T06:00:00Z"
                        paid: 60
                        failed: 141
                        attempts: 201
                        players_failed: 41
                        failed_without_external_ref: 141
                        failed_without_external_ref_pct: 100
                      others:
                        - payment_method: Mach
                          rate: 95.2
                          attempts: 42
                      params:
                        window_minutes: 60
                        settle_minutes: 10
                        min_attempts: 20
                        floor_pct: 50
                        critical_pct: 35
                        drop_points: 25
                        baseline_days: 7
                        baseline_min_attempts: 100
                        recovery_pct: 65
                        resolve_min_attempts: 5
                        no_traffic_hours: 3
                        muted_methods: []
                      trigger: floor
                    notified_channels:
                      open:
                        in_app:
                          status: enviado
                          recipients: 3
                        work:
                          status: creadas
                          created: 1
                          destinations:
                            - board_id: 5a8f7c11-3b1d-4f0e-8d2a-9c4b1e7f6a20
                              column_id: 0e6d2c93-7a41-4b5f-9e18-3d7c5a2b1f04
                              status: creada
                              card_id: c4f1a9e2-6b3d-4a7c-8e51-2d9f0b7a3c68
                        email:
                          status: no_enviado
                          detail: el servidor de correo rechazó la autenticación
                          code: 535
                    reviewed_at: null
                    reviewed_by: null
                    review_note: null
                open_count: 1
                can_admin: true
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /data/payment-method-alerts/{id}:
    patch:
      tags:
        - Data
      operationId: reviewPaymentMethodAlert
      summary: Marca revisada una alerta de efectividad por método de pago
      x-convray-runtime-status: active-data-v1
      description: >-
        Marca un episodio como revisado con una nota opcional de hasta 2000
        caracteres (rol mínimo admin de Data) y devuelve el episodio actualizado. No
        cambia el estado: un episodio abierto sigue abierto hasta que el vigilante lo
        cierre al recuperarse el método o por falta de tráfico. Un id inexistente o
        de otro casino responde 404. Superficie Data: acepta JWT o token personal
        cvr_. Detrás de la puerta de producto 'data': si el header X-Convray-Product
        no coincide, responde 404.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: tenant
          in: query
          required: false
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ReviewPaymentMethodAlertRequest"
            example:
              note: El proveedor confirmó la caída de la pasarela de tarjetas entre las 01:00 y las 03:15.
      responses:
        "200":
          description: Alerta actualizada
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PaymentMethodAlertResponse"
        "400":
          description: Cuerpo JSON inválido
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /data/payment-method-alert-settings:
    get:
      tags:
        - Data
      operationId: getPaymentMethodAlertSettings
      summary: Ajustes de la alerta de efectividad por método de pago
      x-convray-runtime-status: active-data-v1
      description: >-
        Devuelve el interruptor, los umbrales, los métodos silenciados y los canales
        de la alerta de efectividad por método de pago del casino (rol mínimo admin
        de Data). Si el casino no tiene ajustes guardados devuelve los valores por
        defecto con updated_at null. Superficie Data: acepta JWT o token personal
        cvr_. Detrás de la puerta de producto 'data': si el header X-Convray-Product
        no coincide, responde 404.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: tenant
          in: query
          required: false
          schema:
            type: string
      responses:
        "200":
          description: Ajustes de la alerta de efectividad
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PaymentMethodAlertSettingsResponse"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
    put:
      tags:
        - Data
      operationId: putPaymentMethodAlertSettings
      summary: Edita los ajustes de la alerta de efectividad por método de pago
      x-convray-runtime-status: active-data-v1
      description: >-
        Valida y guarda los ajustes de la alerta de efectividad por método de pago
        del casino (rol mínimo admin de Data) y devuelve los ajustes resultantes.
        Cada rango fuera del contrato responde 422 con el campo; los destinos de Work
        se validan igual que en las Alertas VIP (UUID, sin duplicados, hasta 20), cada
        correo debe ser una sola dirección válida (hasta 20) y el autor de las tarjetas
        es por defecto quien guarda (un autor inexistente responde 422). Superficie Data: acepta JWT o token
        personal cvr_. Detrás de la puerta de producto 'data': si el header
        X-Convray-Product no coincide, responde 404.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: tenant
          in: query
          required: false
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PutPaymentMethodAlertSettingsRequest"
            example:
              enabled: true
              window_minutes: 60
              settle_minutes: 10
              min_attempts: 20
              floor_pct: 50
              critical_pct: 35
              drop_points: 25
              baseline_days: 7
              baseline_min_attempts: 100
              recovery_pct: 65
              resolve_min_attempts: 5
              no_traffic_hours: 3
              muted_methods: []
              channel_in_app: true
              channel_work: true
              channel_email: false
              work_destinations:
                - board_id: 5a8f7c11-3b1d-4f0e-8d2a-9c4b1e7f6a20
                  column_id: 0e6d2c93-7a41-4b5f-9e18-3d7c5a2b1f04
              work_author_user_id: null
              email_recipients:
                - pagos@casino.example
      responses:
        "200":
          description: Ajustes actualizados
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PaymentMethodAlertSettingsResponse"
        "400":
          description: Cuerpo JSON inválido
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /data/quality/coverage:
    get:
      tags:
        - Data
      operationId: getDataQualityCoverage
      summary: Cobertura de calidad por dataset
      x-convray-runtime-status: active-data-v1
      description: >-
        Devuelve, por dataset, el último cargado, si se cargó hoy, si hay brechas
        abiertas y cuántas alertas abiertas tiene, más el total de alertas
        abiertas. Superficie Data: acepta JWT o token personal cvr_. Detrás de la
        puerta de producto 'data': si el header X-Convray-Product no coincide,
        responde 404.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: tenant
          in: query
          required: false
          schema:
            type: string
      responses:
        "200":
          description: Cobertura por dataset
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataQualityCoverageResponse"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
  /data/quality/owners:
    get:
      tags:
        - Data
      operationId: getDataQualityOwners
      summary: Lista los responsables de cada dataset
      x-convray-runtime-status: active-data-v1
      description: >-
        Devuelve los responsables asignados por dataset, los candidatos elegibles y
        si el usuario puede editarlos. Superficie Data: acepta JWT o token personal
        cvr_. Detrás de la puerta de producto 'data': si el header
        X-Convray-Product no coincide, responde 404.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: tenant
          in: query
          required: false
          schema:
            type: string
      responses:
        "200":
          description: Responsables y candidatos
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataQualityOwnersResponse"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
    put:
      tags:
        - Data
      operationId: putDataQualityOwners
      summary: Asigna los responsables de cada dataset
      x-convray-runtime-status: active-data-v1
      description: >-
        Reemplaza la asignación de responsables por dataset y devuelve el estado
        actualizado (responsables, candidatos y si puede editar). Superficie Data:
        acepta JWT o token personal cvr_. Detrás de la puerta de producto 'data':
        si el header X-Convray-Product no coincide, responde 404.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: tenant
          in: query
          required: false
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PutDataQualityOwnersRequest"
      responses:
        "200":
          description: Responsables actualizados
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataQualityOwnersResponse"
        "400":
          description: Cuerpo JSON inválido
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /data/affiliates/catalog:
    get:
      tags:
        - Data
      operationId: listDataAffiliateCatalog
      summary: Lista el catálogo de afiliados paginado
      x-convray-runtime-status: active-data-v1
      description: >-
        Devuelve el catálogo de afiliados del portal antiguo (solo datos no
        personales) con el partner mapeado y los agregados de Data por afiliado,
        paginado y con búsqueda por usuario o ID. Superficie Data: acepta JWT o
        token personal cvr_. Detrás de la puerta de producto 'data': si el header
        X-Convray-Product no coincide, responde 404.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: tenant
          in: query
          required: false
          schema:
            type: string
        - name: search
          in: query
          required: false
          description: Búsqueda por nombre de usuario o ID de afiliado.
          schema:
            type: string
        - name: limit
          in: query
          required: false
          schema:
            type: integer
        - name: offset
          in: query
          required: false
          schema:
            type: integer
      responses:
        "200":
          description: Catálogo de afiliados
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataAffiliateCatalogResponse"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
    post:
      tags:
        - Data
      operationId: importDataAffiliateCatalog
      summary: Carga el CSV de catálogo de afiliados
      x-convray-runtime-status: active-data-v1
      description: >-
        Recibe el CSV "Afiliados" en el campo multipart 'file' y hace un upsert
        síncrono, descartando toda columna personal. Devuelve el resumen de filas
        leídas, insertadas, actualizadas y rechazadas (con motivos neutros, sin
        PII). Solo owner/admin de Data. Superficie Data: acepta JWT o token
        personal cvr_. Detrás de la puerta de producto 'data': si el header
        X-Convray-Product no coincide, responde 404.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: tenant
          in: query
          required: false
          schema:
            type: string
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required:
                - file
              properties:
                file:
                  type: string
                  format: binary
                  description: CSV "Afiliados" del portal antiguo.
      responses:
        "200":
          description: Resumen de la carga
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataAffiliateCatalogImportResponse"
        "400":
          description: Archivo faltante o inválido en el campo 'file'
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "413":
          description: El archivo supera el límite permitido
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "422":
          description: >-
            El CSV no tiene las columnas requeridas o está vacío (códigos
            affiliate-catalog-headers / affiliate-catalog-empty).
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
# Superficie DATA lectura de dashboard (data-B). Todas las rutas van detrás de
# JWTAuth + product gate `data` (productgate.ProductData). Si el header de producto
# no coincide, el gate responde 404 (ResourceNotFound), NO 403. Seguridad: JWT
# (adminBearer) o token personal cvr_ (dataApiToken).
  /data/overview:
    get:
      tags:
        - Data
      operationId: getDataOverview
      summary: Franja global de KPIs del rango (los cuatro datasets)
      x-convray-runtime-status: active-data-v1
      description: >-
        Combina en una respuesta los KPIs de depósitos, retiros, casino y deportes
        del rango pedido (default: mes en curso en America/Santiago, máximo 366
        días). El product gate `data` responde 404 si el header de producto no
        coincide. Refleja el acierto de caché en el header X-Cache.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: tenant
          in: query
          required: false
          description: Slug del tenant a consultar (plataforma admin puede acotar).
          schema:
            type: string
        - name: from
          in: query
          required: false
          description: Día civil inicial YYYY-MM-DD (America/Santiago).
          schema:
            type: string
            format: date
        - name: to
          in: query
          required: false
          description: Día civil final YYYY-MM-DD (America/Santiago).
          schema:
            type: string
            format: date
      responses:
        "200":
          description: Franja global de KPIs del rango
          headers:
            X-Cache:
              description: HIT o MISS de la caché de proceso.
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataOverview"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /data/resumen:
    get:
      tags:
        - Data
      operationId: getDataResumen
      summary: Módulo Resumen del mes (día a día, mes a mes y proyección)
      x-convray-runtime-status: active-data-v1
      description: >-
        Resumen del mes pedido (default: mes en curso en America/Santiago; un mes
        futuro devuelve 422). Con format=csv&table=daily|monthly|verticals baja la
        tabla indicada como CSV. El product gate `data` responde 404 si el header
        de producto no coincide. Refleja el acierto de caché en X-Cache.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: tenant
          in: query
          required: false
          schema:
            type: string
        - name: month
          in: query
          required: false
          description: Mes YYYY-MM (default mes en curso, America/Santiago).
          schema:
            type: string
        - name: format
          in: query
          required: false
          description: csv para descargar la tabla como CSV.
          schema:
            type: string
            enum:
              - csv
        - name: table
          in: query
          required: false
          description: Tabla a exportar cuando format=csv.
          schema:
            type: string
            enum:
              - daily
              - monthly
              - verticals
      responses:
        "200":
          description: Resumen del mes (JSON) o la tabla pedida (CSV)
          headers:
            X-Cache:
              description: HIT o MISS de la caché de proceso (solo respuesta JSON).
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataResumen"
            text/csv:
              schema:
                type: string
                format: binary
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /data/deposits/summary:
    get:
      tags:
        - Data
      operationId: getDataDepositsSummary
      summary: KPIs, serie diaria y desgloses de depósitos
      x-convray-runtime-status: active-data-v1
      description: >-
        Indicadores del encabezado de Depósitos, serie diaria y desgloses por
        método, dispositivo y estado del rango. Un filtro no aplicable al dataset
        de depósitos devuelve 422. El product gate `data` responde 404 si el header
        de producto no coincide.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: tenant
          in: query
          required: false
          schema:
            type: string
        - name: from
          in: query
          required: false
          schema:
            type: string
            format: date
        - name: to
          in: query
          required: false
          schema:
            type: string
            format: date
        - name: status
          in: query
          required: false
          description: Lista de estados (multivalor).
          schema:
            type: array
            items:
              type: string
        - name: method
          in: query
          required: false
          description: Lista de métodos de pago (multivalor).
          schema:
            type: array
            items:
              type: string
        - name: device
          in: query
          required: false
          description: Lista de tipos de dispositivo (multivalor).
          schema:
            type: array
            items:
              type: string
        - $ref: "#/components/parameters/DataStatusExclude"
        - $ref: "#/components/parameters/DataMethodExclude"
        - $ref: "#/components/parameters/DataMethodTypeExclude"
        - $ref: "#/components/parameters/DataDeviceExclude"
        - $ref: "#/components/parameters/DataCountryExclude"
        - $ref: "#/components/parameters/DataRegionExclude"
      responses:
        "200":
          description: Resumen de depósitos del rango
          headers:
            X-Cache:
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataDepositSummary"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /data/deposits/insights:
    get:
      tags:
        - Data
      operationId: getDataDepositsInsights
      summary: Indicadores avanzados de depósitos (aprobación, cohortes, ARPU)
      x-convray-runtime-status: active-data-v1
      description: >-
        Aprobación por método, fallidos por hora/día, reintentos y recuperación,
        depositantes activos, nuevos vs recurrentes, frecuencia, ARPU, mix,
        promedio diario y efectividad real (`real_effectiveness`: cuenta como no
        cobrado el depósito abierto pasados 10 minutos). Comparte los filtros de
        summary; un filtro no aplicable devuelve 422. El product gate `data`
        responde 404 si el header no coincide. `real_effectiveness` se recalcula
        con una caché propia de 30 segundos.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: tenant
          in: query
          required: false
          schema:
            type: string
        - name: from
          in: query
          required: false
          schema:
            type: string
            format: date
        - name: to
          in: query
          required: false
          schema:
            type: string
            format: date
        - name: status
          in: query
          required: false
          schema:
            type: array
            items:
              type: string
        - name: method
          in: query
          required: false
          schema:
            type: array
            items:
              type: string
        - name: device
          in: query
          required: false
          schema:
            type: array
            items:
              type: string
      responses:
        "200":
          description: Indicadores de depósitos del rango
          headers:
            X-Cache:
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataDepositInsights"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /data/deposits/transactions:
    get:
      tags:
        - Data
      operationId: getDataDepositsTransactions
      summary: Listado paginado de transacciones de depósito
      x-convray-runtime-status: active-data-v1
      description: >-
        Tabla paginada de depósitos con el contrato común de filtros (listas de
        estado/método/dispositivo, región, país, tipo de método, rango de monto,
        depósito automático) más el filtro por ID (ref). Con format=csv baja el
        listado como CSV. El product gate `data` responde 404 si el header no
        coincide; un filtro no aplicable devuelve 422.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: tenant
          in: query
          required: false
          schema:
            type: string
        - name: from
          in: query
          required: false
          schema:
            type: string
            format: date
        - name: to
          in: query
          required: false
          schema:
            type: string
            format: date
        - name: status
          in: query
          required: false
          description: Estado (contrato común, admite multivalor).
          schema:
            type: array
            items:
              type: string
        - name: method
          in: query
          required: false
          schema:
            type: array
            items:
              type: string
        - name: method_type
          in: query
          required: false
          schema:
            type: array
            items:
              type: string
        - name: device
          in: query
          required: false
          schema:
            type: array
            items:
              type: string
        - name: country
          in: query
          required: false
          schema:
            type: array
            items:
              type: string
        - name: region
          in: query
          required: false
          schema:
            type: array
            items:
              type: string
        - name: amount_min
          in: query
          required: false
          description: Monto mínimo en unidad menor (CLP entero).
          schema:
            type: integer
            format: int64
        - name: amount_max
          in: query
          required: false
          schema:
            type: integer
            format: int64
        - name: auto_deposit
          in: query
          required: false
          description: Filtro de depósito automático (yes|no).
          schema:
            type: string
        - $ref: "#/components/parameters/DataTagFilter"
        - $ref: "#/components/parameters/DataFlagFilter"
        - $ref: "#/components/parameters/DataIspFilter"
        - $ref: "#/components/parameters/DataIspExclude"
        - $ref: "#/components/parameters/DataTagExclude"
        - $ref: "#/components/parameters/DataFlagExclude"
        - $ref: "#/components/parameters/DataContactable"
        - name: player
          in: query
          required: false
          description: Referencia externa del jugador.
          schema:
            type: string
        - name: ref
          in: query
          required: false
          description: Filtro por ID exacto (deposit_ref, external_ref o external_player_ref).
          schema:
            type: string
        - name: page
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            default: 1
        - name: page_size
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 200
            default: 50
        - name: sort
          in: query
          required: false
          description: Orden created_at|amount_minor con :asc|:desc.
          schema:
            type: string
        - name: format
          in: query
          required: false
          schema:
            type: string
            enum:
              - csv
        - $ref: "#/components/parameters/DataStatusExclude"
        - $ref: "#/components/parameters/DataMethodExclude"
        - $ref: "#/components/parameters/DataMethodTypeExclude"
        - $ref: "#/components/parameters/DataDeviceExclude"
        - $ref: "#/components/parameters/DataCountryExclude"
        - $ref: "#/components/parameters/DataRegionExclude"
      responses:
        "200":
          description: Página de transacciones (JSON) o CSV
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataDepositTransactionsPage"
            text/csv:
              schema:
                type: string
                format: binary
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /data/deposits/statuses:
    get:
      tags:
        - Data
      operationId: getDataDepositStatuses
      summary: Catálogo de estados de depósito
      x-convray-runtime-status: active-data-v1
      description: >-
        Catálogo estático de estados de depósito (código + etiqueta en español).
        Autoriza por la puerta data viewer sobre ?tenant=. El product gate `data`
        responde 404 si el header de producto no coincide.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: tenant
          in: query
          required: false
          schema:
            type: string
      responses:
        "200":
          description: Catálogo de estados
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataStatusCatalog"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
  /data/deposits/{ref}/context:
    get:
      tags:
        - Data
      operationId: getDataDepositContext
      summary: Ficha de contexto de un depósito
      x-convray-runtime-status: active-data-v1
      description: >-
        Transacción completa de un depósito más el contexto del jugador
        (agregados por estado, medios y dispositivos, primer/último depósito
        pagado, últimos movimientos, net cash y banderas). {ref} es el ID de
        Centrivo (deposit_ref). 404 si la transacción no es del tenant o el header
        de producto no coincide. La PII sólo se expone con rol admin y clave del
        CRM; sin permiso viaja enmascarada y sin bloque pii.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: ref
          in: path
          required: true
          description: ID de Centrivo del depósito (deposit_ref).
          schema:
            type: string
        - name: tenant
          in: query
          required: false
          schema:
            type: string
        - name: from
          in: query
          required: false
          description: Inicio del rango opcional (acota los agregados; requiere to).
          schema:
            type: string
        - name: to
          in: query
          required: false
          description: Fin del rango opcional (acota los agregados; requiere from).
          schema:
            type: string
      responses:
        "200":
          description: Ficha de la transacción y contexto del jugador
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataTransactionContext"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /data/withdrawals/summary:
    get:
      tags:
        - Data
      operationId: getDataWithdrawalsSummary
      summary: KPIs, serie diaria y desgloses de retiros
      x-convray-runtime-status: active-data-v1
      description: >-
        Indicadores del encabezado de Retiros, serie diaria y desgloses por
        método, dispositivo y estado del rango. Un filtro no aplicable devuelve
        422. El product gate `data` responde 404 si el header no coincide.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: tenant
          in: query
          required: false
          schema:
            type: string
        - name: from
          in: query
          required: false
          schema:
            type: string
            format: date
        - name: to
          in: query
          required: false
          schema:
            type: string
            format: date
        - name: status
          in: query
          required: false
          schema:
            type: array
            items:
              type: string
        - name: method
          in: query
          required: false
          schema:
            type: array
            items:
              type: string
        - name: device
          in: query
          required: false
          schema:
            type: array
            items:
              type: string
        - $ref: "#/components/parameters/DataStatusExclude"
        - $ref: "#/components/parameters/DataMethodExclude"
        - $ref: "#/components/parameters/DataMethodTypeExclude"
        - $ref: "#/components/parameters/DataDeviceExclude"
        - $ref: "#/components/parameters/DataCountryExclude"
        - $ref: "#/components/parameters/DataRegionExclude"
      responses:
        "200":
          description: Resumen de retiros del rango
          headers:
            X-Cache:
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataWithdrawalSummary"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /data/withdrawals/transactions:
    get:
      tags:
        - Data
      operationId: getDataWithdrawalsTransactions
      summary: Listado paginado de transacciones de retiro
      x-convray-runtime-status: active-data-v1
      description: >-
        Tabla paginada de retiros con el contrato común de filtros (listas de
        estado/método/dispositivo, región, país, tipo de método, rango de monto)
        más el filtro por ID (ref). Con format=csv baja el listado como CSV. El
        product gate `data` responde 404 si el header no coincide; un filtro no
        aplicable devuelve 422.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: tenant
          in: query
          required: false
          schema:
            type: string
        - name: from
          in: query
          required: false
          schema:
            type: string
            format: date
        - name: to
          in: query
          required: false
          schema:
            type: string
            format: date
        - name: status
          in: query
          required: false
          schema:
            type: array
            items:
              type: string
        - name: method
          in: query
          required: false
          schema:
            type: array
            items:
              type: string
        - name: method_type
          in: query
          required: false
          schema:
            type: array
            items:
              type: string
        - name: device
          in: query
          required: false
          schema:
            type: array
            items:
              type: string
        - name: country
          in: query
          required: false
          schema:
            type: array
            items:
              type: string
        - name: region
          in: query
          required: false
          schema:
            type: array
            items:
              type: string
        - name: amount_min
          in: query
          required: false
          schema:
            type: integer
            format: int64
        - name: amount_max
          in: query
          required: false
          schema:
            type: integer
            format: int64
        - $ref: "#/components/parameters/DataTagFilter"
        - $ref: "#/components/parameters/DataFlagFilter"
        - $ref: "#/components/parameters/DataIspFilter"
        - $ref: "#/components/parameters/DataIspExclude"
        - $ref: "#/components/parameters/DataTagExclude"
        - $ref: "#/components/parameters/DataFlagExclude"
        - $ref: "#/components/parameters/DataContactable"
        - name: player
          in: query
          required: false
          schema:
            type: string
        - name: ref
          in: query
          required: false
          description: Filtro por ID exacto (withdrawal_ref, external_ref o external_player_ref).
          schema:
            type: string
        - name: page
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            default: 1
        - name: page_size
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 200
            default: 50
        - name: sort
          in: query
          required: false
          schema:
            type: string
        - name: format
          in: query
          required: false
          schema:
            type: string
            enum:
              - csv
        - $ref: "#/components/parameters/DataStatusExclude"
        - $ref: "#/components/parameters/DataMethodExclude"
        - $ref: "#/components/parameters/DataMethodTypeExclude"
        - $ref: "#/components/parameters/DataDeviceExclude"
        - $ref: "#/components/parameters/DataCountryExclude"
        - $ref: "#/components/parameters/DataRegionExclude"
      responses:
        "200":
          description: Página de transacciones (JSON) o CSV
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataWithdrawalTransactionsPage"
            text/csv:
              schema:
                type: string
                format: binary
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /data/withdrawals/statuses:
    get:
      tags:
        - Data
      operationId: getDataWithdrawalStatuses
      summary: Catálogo de estados de retiro
      x-convray-runtime-status: active-data-v1
      description: >-
        Catálogo estático de estados de retiro (código + etiqueta en español).
        Autoriza por la puerta data viewer sobre ?tenant=. El product gate `data`
        responde 404 si el header de producto no coincide.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: tenant
          in: query
          required: false
          schema:
            type: string
      responses:
        "200":
          description: Catálogo de estados
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataStatusCatalog"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
  /data/withdrawals/{ref}/context:
    get:
      tags:
        - Data
      operationId: getDataWithdrawalContext
      summary: Ficha de contexto de un retiro
      x-convray-runtime-status: active-data-v1
      description: >-
        Transacción completa de un retiro más el contexto del jugador (mismos
        bloques que la ficha de depósito). {ref} es el ID de Centrivo
        (withdrawal_ref). 404 si la transacción no es del tenant o el header de
        producto no coincide. La PII sólo se expone con rol admin y clave del CRM.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: ref
          in: path
          required: true
          description: ID de Centrivo del retiro (withdrawal_ref).
          schema:
            type: string
        - name: tenant
          in: query
          required: false
          schema:
            type: string
        - name: from
          in: query
          required: false
          schema:
            type: string
        - name: to
          in: query
          required: false
          schema:
            type: string
      responses:
        "200":
          description: Ficha de la transacción y contexto del jugador
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataTransactionContext"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /data/casino-bets/summary:
    get:
      tags:
        - Data
      operationId: getDataCasinoBetsSummary
      summary: Totales, serie y desgloses de apuestas de casino
      x-convray-runtime-status: active-data-v1
      description: >-
        Totales sin bono, subtotal de bono, serie diaria y desgloses por
        proveedor, categoría, dispositivo y bono del rango de apuestas de casino.
        Acepta los filtros por jugador tag/flag/isp: el resumen suma solo a los
        jugadores que cumplen el filtro (se computa desde el agregado
        jugador×juego×día). Un filtro no aplicable devuelve 422. El product gate
        `data` responde 404 si el header no coincide.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: tenant
          in: query
          required: false
          schema:
            type: string
        - name: from
          in: query
          required: false
          schema:
            type: string
            format: date
        - name: to
          in: query
          required: false
          schema:
            type: string
            format: date
        - name: provider
          in: query
          required: false
          schema:
            type: string
        - name: category
          in: query
          required: false
          schema:
            type: string
        - name: game_id
          in: query
          required: false
          schema:
            type: string
        - name: device
          in: query
          required: false
          schema:
            type: string
        - name: player
          in: query
          required: false
          schema:
            type: string
        - name: bonus
          in: query
          required: false
          description: Filtro de bono (yes|no).
          schema:
            type: string
        - $ref: "#/components/parameters/DataTagFilter"
        - $ref: "#/components/parameters/DataFlagFilter"
        - $ref: "#/components/parameters/DataIspFilter"
        - $ref: "#/components/parameters/DataIspExclude"
        - $ref: "#/components/parameters/DataTagExclude"
        - $ref: "#/components/parameters/DataFlagExclude"
        - $ref: "#/components/parameters/DataContactable"
        - $ref: "#/components/parameters/DataProviderExclude"
        - $ref: "#/components/parameters/DataCategoryExclude"
        - $ref: "#/components/parameters/DataGameExclude"
        - $ref: "#/components/parameters/DataDeviceExclude"
      responses:
        "200":
          description: Resumen de apuestas de casino del rango
          headers:
            X-Cache:
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataCasinoSummary"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /data/casino-bets/games:
    get:
      tags:
        - Data
      operationId: getDataCasinoBetsGames
      summary: Ranking de juegos de casino
      x-convray-runtime-status: active-data-v1
      description: >-
        Ranking paginado de juegos de casino por métrica. Con format=csv baja el
        ranking como CSV. Acepta los filtros por jugador tag/flag/isp: el ranking
        filtra por jugador desde el agregado jugador×juego×día. El product gate
        `data` responde 404 si el header no coincide; un filtro no aplicable
        devuelve 422.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: tenant
          in: query
          required: false
          schema:
            type: string
        - name: from
          in: query
          required: false
          schema:
            type: string
            format: date
        - name: to
          in: query
          required: false
          schema:
            type: string
            format: date
        - name: provider
          in: query
          required: false
          schema:
            type: string
        - name: category
          in: query
          required: false
          schema:
            type: string
        - name: game_id
          in: query
          required: false
          schema:
            type: string
        - name: device
          in: query
          required: false
          schema:
            type: string
        - name: player
          in: query
          required: false
          schema:
            type: string
        - name: bonus
          in: query
          required: false
          schema:
            type: string
        - name: sort
          in: query
          required: false
          schema:
            type: string
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 200
            default: 50
        - name: offset
          in: query
          required: false
          schema:
            type: integer
            minimum: 0
            default: 0
        - name: format
          in: query
          required: false
          schema:
            type: string
            enum:
              - csv
        - $ref: "#/components/parameters/DataTagFilter"
        - $ref: "#/components/parameters/DataFlagFilter"
        - $ref: "#/components/parameters/DataIspFilter"
        - $ref: "#/components/parameters/DataIspExclude"
        - $ref: "#/components/parameters/DataTagExclude"
        - $ref: "#/components/parameters/DataFlagExclude"
        - $ref: "#/components/parameters/DataContactable"
        - $ref: "#/components/parameters/DataProviderExclude"
        - $ref: "#/components/parameters/DataCategoryExclude"
        - $ref: "#/components/parameters/DataGameExclude"
        - $ref: "#/components/parameters/DataDeviceExclude"
      responses:
        "200":
          description: Ranking de juegos (JSON) o CSV
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataCasinoGamesPage"
            text/csv:
              schema:
                type: string
                format: binary
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /data/casino-bets/games/{gameId}/daily:
    get:
      tags:
        - Data
      operationId: getDataCasinoGameDaily
      summary: GGR por día de un juego de casino
      x-convray-runtime-status: active-data-v1
      description: >-
        Devuelve el GGR y lo apostado (sin bono) por día del juego dentro del
        período (from/to), para el gráfico de la ficha de juego. Respeta las
        mismas dimensiones que el ranking de juegos (proveedor, categoría,
        dispositivo, sus exclusiones y los filtros por jugador tag/flag/isp),
        así la serie cuadra con los totales del juego en esa vista; el juego es
        siempre el del path, aunque la consulta traiga otra lista de juegos.
        Lectura agregada sin datos personales. El product gate `data` responde
        404 si el header no coincide; un filtro no aplicable devuelve 422.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: gameId
          in: path
          required: true
          schema:
            type: string
        - name: tenant
          in: query
          required: false
          schema:
            type: string
        - name: from
          in: query
          required: false
          schema:
            type: string
            format: date
        - name: to
          in: query
          required: false
          schema:
            type: string
            format: date
        - name: provider
          in: query
          required: false
          schema:
            type: string
        - name: category
          in: query
          required: false
          schema:
            type: string
        - name: device
          in: query
          required: false
          schema:
            type: string
        - $ref: "#/components/parameters/DataTagFilter"
        - $ref: "#/components/parameters/DataFlagFilter"
        - $ref: "#/components/parameters/DataIspFilter"
        - $ref: "#/components/parameters/DataIspExclude"
        - $ref: "#/components/parameters/DataTagExclude"
        - $ref: "#/components/parameters/DataFlagExclude"
        - $ref: "#/components/parameters/DataContactable"
        - $ref: "#/components/parameters/DataProviderExclude"
        - $ref: "#/components/parameters/DataCategoryExclude"
        - $ref: "#/components/parameters/DataGameExclude"
        - $ref: "#/components/parameters/DataDeviceExclude"
      responses:
        "200":
          description: GGR por día del juego
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CasinoGameDailyPage"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /data/casino-bets/players:
    get:
      tags:
        - Data
      operationId: getDataCasinoBetsPlayers
      summary: Ranking de jugadores de casino
      x-convray-runtime-status: active-data-v1
      description: >-
        Ranking paginado de jugadores por actividad de casino. El product gate
        `data` responde 404 si el header no coincide; un filtro no aplicable
        devuelve 422.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: tenant
          in: query
          required: false
          schema:
            type: string
        - name: from
          in: query
          required: false
          schema:
            type: string
            format: date
        - name: to
          in: query
          required: false
          schema:
            type: string
            format: date
        - name: provider
          in: query
          required: false
          schema:
            type: string
        - name: category
          in: query
          required: false
          schema:
            type: string
        - name: game_id
          in: query
          required: false
          schema:
            type: string
        - name: device
          in: query
          required: false
          schema:
            type: string
        - name: player
          in: query
          required: false
          schema:
            type: string
        - name: bonus
          in: query
          required: false
          schema:
            type: string
        - $ref: "#/components/parameters/DataTagFilter"
        - $ref: "#/components/parameters/DataFlagFilter"
        - $ref: "#/components/parameters/DataIspFilter"
        - $ref: "#/components/parameters/DataIspExclude"
        - $ref: "#/components/parameters/DataTagExclude"
        - $ref: "#/components/parameters/DataFlagExclude"
        - $ref: "#/components/parameters/DataContactable"
        - name: sort
          in: query
          required: false
          schema:
            type: string
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 200
            default: 50
        - name: offset
          in: query
          required: false
          schema:
            type: integer
            minimum: 0
            default: 0
        - $ref: "#/components/parameters/DataProviderExclude"
        - $ref: "#/components/parameters/DataCategoryExclude"
        - $ref: "#/components/parameters/DataGameExclude"
        - $ref: "#/components/parameters/DataDeviceExclude"
      responses:
        "200":
          description: Ranking de jugadores de casino
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataCasinoPlayersPage"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /data/casino-bets/transactions:
    get:
      tags:
        - Data
      operationId: getDataCasinoBetsTransactions
      summary: Ventana de detalle de apuestas de casino
      x-convray-runtime-status: active-data-v1
      description: >-
        Detalle paginado de apuestas de casino dentro de la ventana permitida
        (DATA_CASINO_BETS_DETAIL_DAYS). Con format=csv baja el detalle como CSV. El
        product gate `data` responde 404 si el header no coincide; un filtro no
        aplicable devuelve 422. La PII sólo se expone con rol admin y clave del CRM.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: tenant
          in: query
          required: false
          schema:
            type: string
        - name: from
          in: query
          required: false
          schema:
            type: string
            format: date
        - name: to
          in: query
          required: false
          schema:
            type: string
            format: date
        - name: provider
          in: query
          required: false
          schema:
            type: string
        - name: category
          in: query
          required: false
          schema:
            type: string
        - name: game_id
          in: query
          required: false
          schema:
            type: string
        - name: device
          in: query
          required: false
          schema:
            type: string
        - name: player
          in: query
          required: false
          schema:
            type: string
        - name: bonus
          in: query
          required: false
          schema:
            type: string
        - $ref: "#/components/parameters/DataTagFilter"
        - $ref: "#/components/parameters/DataFlagFilter"
        - $ref: "#/components/parameters/DataIspFilter"
        - $ref: "#/components/parameters/DataIspExclude"
        - $ref: "#/components/parameters/DataTagExclude"
        - $ref: "#/components/parameters/DataFlagExclude"
        - $ref: "#/components/parameters/DataContactable"
        - name: sort
          in: query
          required: false
          schema:
            type: string
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 200
            default: 50
        - name: offset
          in: query
          required: false
          schema:
            type: integer
            minimum: 0
            default: 0
        - name: format
          in: query
          required: false
          schema:
            type: string
            enum:
              - csv
        - $ref: "#/components/parameters/DataProviderExclude"
        - $ref: "#/components/parameters/DataCategoryExclude"
        - $ref: "#/components/parameters/DataGameExclude"
        - $ref: "#/components/parameters/DataDeviceExclude"
      responses:
        "200":
          description: Detalle de apuestas de casino (JSON) o CSV
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataCasinoTransactionsPage"
            text/csv:
              schema:
                type: string
                format: binary
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /data/sport-bets/summary:
    get:
      tags:
        - Data
      operationId: getDataSportBetsSummary
      summary: Totales, serie y desgloses de apuestas deportivas
      x-convray-runtime-status: active-data-v1
      description: >-
        Totales sin bono, subtotal de bono, serie diaria y desgloses por estado,
        tipo de apuesta, tipo deportivo, dispositivo y bono del rango de apuestas
        deportivas. Acepta los filtros por jugador tag/flag/isp: el resumen suma
        solo las apuestas de los jugadores que cumplen el filtro (se computa desde
        el ledger por apuesta). Un filtro no aplicable devuelve 422. El product
        gate `data` responde 404 si el header no coincide.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: tenant
          in: query
          required: false
          schema:
            type: string
        - name: from
          in: query
          required: false
          schema:
            type: string
            format: date
        - name: to
          in: query
          required: false
          schema:
            type: string
            format: date
        - name: state
          in: query
          required: false
          description: Lista de estados separada por comas (won, lost, cash_out, new, cancelled).
          schema:
            type: string
        - name: bet_type
          in: query
          required: false
          schema:
            type: string
        - name: sport_bet_type
          in: query
          required: false
          schema:
            type: string
        - name: device
          in: query
          required: false
          schema:
            type: string
        - name: bonus
          in: query
          required: false
          schema:
            type: string
        - name: cash_out
          in: query
          required: false
          description: Filtro de cash out (yes|no).
          schema:
            type: string
        - $ref: "#/components/parameters/DataTagFilter"
        - $ref: "#/components/parameters/DataFlagFilter"
        - $ref: "#/components/parameters/DataIspFilter"
        - $ref: "#/components/parameters/DataIspExclude"
        - $ref: "#/components/parameters/DataTagExclude"
        - $ref: "#/components/parameters/DataFlagExclude"
        - $ref: "#/components/parameters/DataContactable"
        - $ref: "#/components/parameters/DataStateExclude"
      responses:
        "200":
          description: Resumen de apuestas deportivas del rango
          headers:
            X-Cache:
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataSportSummary"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /data/sport-bets/transactions:
    get:
      tags:
        - Data
      operationId: getDataSportBetsTransactions
      summary: Listado paginado de apuestas deportivas
      x-convray-runtime-status: active-data-v1
      description: >-
        Tabla paginada de apuestas deportivas. Con format=csv baja el listado como
        CSV. El product gate `data` responde 404 si el header no coincide; un
        filtro no aplicable devuelve 422. La PII sólo se expone con rol admin y
        clave del CRM.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: tenant
          in: query
          required: false
          schema:
            type: string
        - name: from
          in: query
          required: false
          schema:
            type: string
            format: date
        - name: to
          in: query
          required: false
          schema:
            type: string
            format: date
        - name: state
          in: query
          required: false
          schema:
            type: string
        - name: bet_type
          in: query
          required: false
          schema:
            type: string
        - name: sport_bet_type
          in: query
          required: false
          schema:
            type: string
        - name: device
          in: query
          required: false
          schema:
            type: string
        - name: bonus
          in: query
          required: false
          schema:
            type: string
        - name: cash_out
          in: query
          required: false
          schema:
            type: string
        - $ref: "#/components/parameters/DataTagFilter"
        - $ref: "#/components/parameters/DataFlagFilter"
        - $ref: "#/components/parameters/DataIspFilter"
        - $ref: "#/components/parameters/DataIspExclude"
        - $ref: "#/components/parameters/DataTagExclude"
        - $ref: "#/components/parameters/DataFlagExclude"
        - $ref: "#/components/parameters/DataContactable"
        - name: player
          in: query
          required: false
          schema:
            type: string
        - name: ref
          in: query
          required: false
          schema:
            type: string
        - name: page
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            default: 1
        - name: page_size
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 200
            default: 50
        - name: sort
          in: query
          required: false
          description: created_at:desc (default) o created_at:asc.
          schema:
            type: string
            enum:
              - created_at:desc
              - created_at:asc
        - name: format
          in: query
          required: false
          schema:
            type: string
            enum:
              - csv
        - $ref: "#/components/parameters/DataStateExclude"
      responses:
        "200":
          description: Página de apuestas deportivas (JSON) o CSV
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataSportTransactionsPage"
            text/csv:
              schema:
                type: string
                format: binary
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /data/sport-bets/statuses:
    get:
      tags:
        - Data
      operationId: getDataSportStatuses
      summary: Catálogo de estados de apuesta deportiva
      x-convray-runtime-status: active-data-v1
      description: >-
        Catálogo estático de estados de apuesta deportiva (código + etiqueta en
        español). Autoriza por la puerta data viewer sobre ?tenant=. El product
        gate `data` responde 404 si el header de producto no coincide.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: tenant
          in: query
          required: false
          schema:
            type: string
      responses:
        "200":
          description: Catálogo de estados
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataStatusCatalog"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
  /data/player-totals/periods:
    get:
      tags:
        - Data
      operationId: getDataPlayerTotalsPeriods
      summary: Lista los períodos cargados de totales por jugador
      x-convray-runtime-status: active-data-v1
      description: >-
        Devuelve los períodos de totales por jugador ya cargados (una fila por
        período del rollup, orden descendente). Requiere el producto Data; si el
        header de producto no coincide la puerta responde 404.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: tenant
          in: query
          required: false
          description: Slug del tenant a consultar (para operadores multi-tenant).
          schema:
            type: string
      responses:
        "200":
          description: Períodos cargados del tenant
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PlayerTotalsPeriodsResponse"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
  /data/player-totals/summary:
    get:
      tags:
        - Data
      operationId: getDataPlayerTotalsSummary
      summary: Totales agregados de un período por jugador
      x-convray-runtime-status: active-data-v1
      description: >-
        Devuelve los KPIs de un período exacto (period_from/period_to
        obligatorios): depósitos, retiros, net cash, apostado y GGR por vertical,
        NGR, impuestos, hold y margen GGR. Requiere el producto Data; si el header
        de producto no coincide la puerta responde 404.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: tenant
          in: query
          required: false
          schema:
            type: string
        - name: period_from
          in: query
          required: true
          description: Inicio del período (YYYY-MM-DD, inclusivo).
          schema:
            type: string
            format: date
        - name: period_to
          in: query
          required: true
          description: Fin del período (YYYY-MM-DD, exclusivo).
          schema:
            type: string
            format: date
        - $ref: "#/components/parameters/DataTagFilter"
        - $ref: "#/components/parameters/DataFlagFilter"
        - $ref: "#/components/parameters/DataIspFilter"
        - $ref: "#/components/parameters/DataIspExclude"
        - $ref: "#/components/parameters/DataTagExclude"
        - $ref: "#/components/parameters/DataFlagExclude"
        - $ref: "#/components/parameters/DataContactable"
      responses:
        "200":
          description: Totales del período
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PlayerTotalsSummary"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /data/player-totals/list:
    get:
      tags:
        - Data
      operationId: getDataPlayerTotalsList
      summary: Listado paginado de totales por jugador del período
      x-convray-runtime-status: active-data-v1
      description: >-
        Devuelve los jugadores del período con sus totales (PII solo para el rol
        admin). Con format=csv responde el archivo CSV descargable y registra el
        reporte en el log de exportaciones. Requiere el producto Data; si el
        header de producto no coincide la puerta responde 404.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: tenant
          in: query
          required: false
          schema:
            type: string
        - name: period_from
          in: query
          required: true
          schema:
            type: string
            format: date
        - name: period_to
          in: query
          required: true
          schema:
            type: string
            format: date
        - name: player
          in: query
          required: false
          description: Filtra por external_player_ref.
          schema:
            type: string
        - name: sort
          in: query
          required: false
          description: >-
            Columna de orden: ngr, ggr, deposits, net_cash, player (acepta
            además los alias *_minor y external_player_ref). Prefijo "-" para
            descendente.
          schema:
            type: string
        - name: limit
          in: query
          required: false
          schema:
            type: integer
        - name: offset
          in: query
          required: false
          schema:
            type: integer
        - name: format
          in: query
          required: false
          description: Con valor "csv" descarga el listado como archivo CSV.
          schema:
            type: string
            enum:
              - csv
        - $ref: "#/components/parameters/DataTagFilter"
        - $ref: "#/components/parameters/DataFlagFilter"
        - $ref: "#/components/parameters/DataIspFilter"
        - $ref: "#/components/parameters/DataIspExclude"
        - $ref: "#/components/parameters/DataTagExclude"
        - $ref: "#/components/parameters/DataFlagExclude"
        - $ref: "#/components/parameters/DataContactable"
      responses:
        "200":
          description: Listado del período (JSON) o descarga CSV si format=csv
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PlayerTotalsListPage"
            text/csv:
              schema:
                type: string
                format: binary
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /data/player-totals/ltv:
    get:
      tags:
        - Data
      operationId: getDataPlayerTotalsLtv
      summary: LTV por jugador sumando períodos sin solape
      x-convray-runtime-status: active-data-v1
      description: >-
        Devuelve el LTV por jugador sumando todos los períodos cargados sin
        solape (se usan solo los períodos maximales). periods_used expone el
        conjunto de períodos aplicados. Requiere el producto Data; si el header
        de producto no coincide la puerta responde 404.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: tenant
          in: query
          required: false
          schema:
            type: string
        - name: sort
          in: query
          required: false
          description: Columna de orden (ngr, ggr, deposits, net_cash, player).
          schema:
            type: string
        - name: limit
          in: query
          required: false
          schema:
            type: integer
        - name: offset
          in: query
          required: false
          schema:
            type: integer
      responses:
        "200":
          description: LTV por jugador
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PlayerTotalsLTVPage"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /data/player-totals/range/summary:
    get:
      tags:
        - Data
      operationId: getDataPlayerTotalsRangeSummary
      summary: Totales por RANGO LIBRE (no atado a periodos de 24 h)
      x-convray-runtime-status: active-data-v1
      description: >-
        Totales por jugador de un RANGO civil arbitrario [from, to]
        (America/Santiago), con los mismos filtros globales que Jugadores
        (segmento tag, estado flag, compania isp, partner y "deposito minimo"
        dep_min). Se computa desde data_player_activity_daily: depositado,
        retirado, net cash, apostado y GGR por vertical (con y sin bono) son
        exactos para cualquier rango. NGR e impuestos son exactos SOLO si el
        rango se cubre exactamente con periodos cargados (ngr_available); si no,
        ngr_minor y taxes_minor son null. Requiere el producto Data.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: tenant
          in: query
          required: false
          schema:
            type: string
        - name: from
          in: query
          required: false
          description: Inicio del rango (YYYY-MM-DD, inclusivo). Default mes en curso.
          schema:
            type: string
            format: date
        - name: to
          in: query
          required: false
          description: Fin del rango (YYYY-MM-DD, inclusivo).
          schema:
            type: string
            format: date
        - name: partner
          in: query
          required: false
          description: "Filtra por partner: none/organic (sin BTAG), any (con BTAG) o un slug de partner."
          schema:
            type: string
        - name: dep_min
          in: query
          required: false
          description: Deja solo los jugadores que depositaron (pagado) al menos este monto (unidad menor) en su ventana dep_from/dep_to (o en el rango si no se da).
          schema:
            type: integer
            format: int64
        - name: dep_from
          in: query
          required: false
          description: Inicio de la ventana propia del filtro dep_min (YYYY-MM-DD).
          schema:
            type: string
            format: date
        - name: dep_to
          in: query
          required: false
          description: Fin de la ventana propia del filtro dep_min (YYYY-MM-DD).
          schema:
            type: string
            format: date
        - $ref: "#/components/parameters/DataTagFilter"
        - $ref: "#/components/parameters/DataFlagFilter"
        - $ref: "#/components/parameters/DataIspFilter"
        - $ref: "#/components/parameters/DataIspExclude"
        - $ref: "#/components/parameters/DataTagExclude"
        - $ref: "#/components/parameters/DataFlagExclude"
        - $ref: "#/components/parameters/DataContactable"
      responses:
        "200":
          description: Totales del rango
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PlayerTotalsRangeSummary"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /data/player-totals/range/list:
    get:
      tags:
        - Data
      operationId: getDataPlayerTotalsRangeList
      summary: Listado paginado de totales por jugador del RANGO
      x-convray-runtime-status: active-data-v1
      description: >-
        Jugadores del rango con sus totales calculados desde
        data_player_activity_daily (mismos filtros que Jugadores). El NGR por
        fila viene solo cuando el rango coincide con periodos cargados
        (ngr_available); si no, ngr_minor es null. Con format=csv responde el CSV
        descargable (separador ';', montos enteros, fechas YYYY-MM-DD) y registra
        el reporte. Requiere el producto Data.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: tenant
          in: query
          required: false
          schema:
            type: string
        - name: from
          in: query
          required: false
          schema:
            type: string
            format: date
        - name: to
          in: query
          required: false
          schema:
            type: string
            format: date
        - name: player
          in: query
          required: false
          description: Filtra por external_player_ref.
          schema:
            type: string
        - name: sort
          in: query
          required: false
          description: "Columna de orden: ggr, deposits, net_cash, player. Sufijo :asc o :desc."
          schema:
            type: string
        - name: partner
          in: query
          required: false
          schema:
            type: string
        - name: dep_min
          in: query
          required: false
          schema:
            type: integer
            format: int64
        - name: dep_from
          in: query
          required: false
          schema:
            type: string
            format: date
        - name: dep_to
          in: query
          required: false
          schema:
            type: string
            format: date
        - name: limit
          in: query
          required: false
          schema:
            type: integer
        - name: offset
          in: query
          required: false
          schema:
            type: integer
        - name: format
          in: query
          required: false
          description: Con valor "csv" descarga el listado como archivo CSV.
          schema:
            type: string
            enum:
              - csv
        - $ref: "#/components/parameters/DataTagFilter"
        - $ref: "#/components/parameters/DataFlagFilter"
        - $ref: "#/components/parameters/DataIspFilter"
        - $ref: "#/components/parameters/DataIspExclude"
        - $ref: "#/components/parameters/DataTagExclude"
        - $ref: "#/components/parameters/DataFlagExclude"
        - $ref: "#/components/parameters/DataContactable"
      responses:
        "200":
          description: Listado del rango (JSON) o descarga CSV si format=csv
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PlayerTotalsRangeListPage"
            text/csv:
              schema:
                type: string
                format: binary
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /data/saved-views:
    get:
      tags:
        - Data
      operationId: listDataSavedViews
      summary: Vistas guardadas de filtros del usuario
      x-convray-runtime-status: active-data-v1
      description: >-
        Lista las vistas de filtros guardadas por el usuario autenticado (patrón de filtros
        globales "Ledger", OLA 2). Cada vista guarda un conjunto de filtros (JSON) por módulo.
        La RLS del producto Data aísla por usuario y por tenant: cada quien ve sólo las propias.
        Requiere el producto Data; si el header de producto no coincide la puerta responde 404.
      security:
        - adminBearer: []
      parameters:
        - name: tenant
          in: query
          required: false
          schema:
            type: string
        - name: module
          in: query
          required: false
          description: Filtra las vistas por módulo (p. ej. data-players, data-deposits).
          schema:
            type: string
      responses:
        "200":
          description: Vistas guardadas del usuario
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SavedViewList"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
    post:
      tags:
        - Data
      operationId: createDataSavedView
      summary: Crear una vista guardada de filtros
      x-convray-runtime-status: active-data-v1
      description: >-
        Crea una vista de filtros para el usuario autenticado. El nombre debe ser único por
        (tenant, usuario, módulo): un choque responde 409. Requiere el producto Data (404 si no).
      security:
        - adminBearer: []
      parameters:
        - name: tenant
          in: query
          required: false
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SavedViewCreateInput"
      responses:
        "201":
          description: Vista creada
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SavedView"
        "400":
          description: Cuerpo inválido
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "409":
          description: Ya existe una vista con ese nombre para el usuario en ese módulo
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /data/saved-views/{id}:
    patch:
      tags:
        - Data
      operationId: updateDataSavedView
      summary: Actualizar una vista guardada
      x-convray-runtime-status: active-data-v1
      description: >-
        Actualiza el nombre y/o los filtros de una vista propia. Campos ausentes no cambian.
        Un nombre repetido en el mismo módulo responde 409; la vista de otro usuario, 404.
      security:
        - adminBearer: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: tenant
          in: query
          required: false
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SavedViewPatchInput"
      responses:
        "200":
          description: Vista actualizada
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SavedView"
        "400":
          description: Cuerpo inválido
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "409":
          description: Ya existe una vista con ese nombre para el usuario en ese módulo
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "422":
          $ref: "#/components/responses/ValidationFailed"
    delete:
      tags:
        - Data
      operationId: deleteDataSavedView
      summary: Archivar (borrado suave) una vista guardada
      x-convray-runtime-status: active-data-v1
      description: >-
        Archiva una vista propia (borrado suave): deja de listarse y su nombre queda libre para
        reutilizarse. La vista de otro usuario responde 404. Requiere el producto Data.
      security:
        - adminBearer: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: tenant
          in: query
          required: false
          schema:
            type: string
      responses:
        "204":
          description: Vista archivada
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
  /data/players/summary:
    get:
      tags:
        - Data
      operationId: getDataPlayersSummary
      summary: Resumen de jugadores por rango de registro
      x-convray-runtime-status: active-data-v1
      description: >-
        Devuelve los KPIs del directorio de jugadores: totales del snapshot,
        registros y depositantes del rango de fecha de registro, serie diaria y
        cortes por dispositivo, estado de cuenta y partner. Requiere el producto
        Data; si el header de producto no coincide la puerta responde 404.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: tenant
          in: query
          required: false
          schema:
            type: string
        - name: from
          in: query
          required: false
          description: Inicio del rango de registro (YYYY-MM-DD). Default mes en curso.
          schema:
            type: string
            format: date
        - name: to
          in: query
          required: false
          description: Fin del rango de registro (YYYY-MM-DD).
          schema:
            type: string
            format: date
        - name: status
          in: query
          required: false
          description: Estado de cuenta único (código del catálogo); acota la base de los totales.
          schema:
            type: string
        - name: state
          in: query
          required: false
          description: Estado de cuenta multivalor (códigos separados por coma); reemplaza a status.
          schema:
            type: string
        - $ref: "#/components/parameters/DataTagFilter"
        - $ref: "#/components/parameters/DataFlagFilter"
        - name: dep_min
          in: query
          required: false
          description: >-
            Depósito mínimo (entero CLP >= 0) que un jugador debe acumular en la ventana
            dep_from/dep_to (sólo pagados) para entrar en los totales. Va junto con dep_from/dep_to.
          schema:
            type: integer
            minimum: 0
        - name: dep_from
          in: query
          required: false
          description: Inicio de la ventana de depósitos (YYYY-MM-DD, día civil America/Santiago).
          schema:
            type: string
            format: date
        - name: dep_to
          in: query
          required: false
          description: Fin inclusivo de la ventana de depósitos (YYYY-MM-DD, día civil America/Santiago).
          schema:
            type: string
            format: date
        - $ref: "#/components/parameters/DataIspFilter"
        - $ref: "#/components/parameters/DataIspExclude"
        - $ref: "#/components/parameters/DataTagExclude"
        - $ref: "#/components/parameters/DataFlagExclude"
        - $ref: "#/components/parameters/DataContactable"
        - $ref: "#/components/parameters/DataStateExclude"
      responses:
        "200":
          description: Resumen de jugadores
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PlayerSummary"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /data/players/list:
    get:
      tags:
        - Data
      operationId: getDataPlayersList
      summary: Listado paginado del directorio de jugadores
      x-convray-runtime-status: active-data-v1
      description: >-
        Devuelve el directorio de jugadores con filtros (rango de registro,
        estado, dispositivo, partner, exclusión, depósito, segmento y búsqueda de
        jugador); PII solo para el rol admin. Con format=csv responde el archivo
        CSV y registra el reporte. Requiere el producto Data; si el header de
        producto no coincide la puerta responde 404.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: tenant
          in: query
          required: false
          schema:
            type: string
        - name: from
          in: query
          required: false
          description: Inicio del rango de registro (YYYY-MM-DD). Opcional.
          schema:
            type: string
            format: date
        - name: to
          in: query
          required: false
          description: Fin del rango de registro (YYYY-MM-DD). Opcional.
          schema:
            type: string
            format: date
        - name: status
          in: query
          required: false
          description: Estado de cuenta único (código del catálogo de estados).
          schema:
            type: string
        - name: state
          in: query
          required: false
          description: >-
            Filtro multivalor de estado de cuenta (códigos separados por coma);
            reemplaza a status cuando viene.
          schema:
            type: string
        - name: device
          in: query
          required: false
          description: Dispositivo de registro.
          schema:
            type: string
        - name: partner
          in: query
          required: false
          description: >-
            Filtro de partner por BTAG: none/organic (sin BTAG), any (con BTAG) o
            un slug de socio (resuelto exacto-o-prefijo).
          schema:
            type: string
        - name: exclusion
          in: query
          required: false
          description: Filtro ternario de autoexclusión (yes/no).
          schema:
            type: string
        - name: deposited
          in: query
          required: false
          description: Filtro ternario de si depositó (yes/no).
          schema:
            type: string
        - $ref: "#/components/parameters/DataTagFilter"
        - $ref: "#/components/parameters/DataFlagFilter"
        - $ref: "#/components/parameters/DataIspFilter"
        - $ref: "#/components/parameters/DataIspExclude"
        - $ref: "#/components/parameters/DataTagExclude"
        - $ref: "#/components/parameters/DataFlagExclude"
        - $ref: "#/components/parameters/DataContactable"
        - name: player
          in: query
          required: false
          description: Búsqueda por external_player_ref o casino_player_ref.
          schema:
            type: string
        - name: segment
          in: query
          required: false
          description: UUID de un segmento de Data; restringe el listado a sus jugadores.
          schema:
            type: string
            format: uuid
        - name: dep_min
          in: query
          required: false
          description: >-
            Depósito mínimo (entero CLP >= 0) que un jugador debe acumular en la ventana
            dep_from/dep_to (sólo pagados) para aparecer. Va junto con dep_from/dep_to (422 si falta
            alguno o el monto es inválido).
          schema:
            type: integer
            minimum: 0
        - name: dep_from
          in: query
          required: false
          description: Inicio de la ventana de depósitos (YYYY-MM-DD, día civil America/Santiago).
          schema:
            type: string
            format: date
        - name: dep_to
          in: query
          required: false
          description: Fin inclusivo de la ventana de depósitos (YYYY-MM-DD, día civil America/Santiago).
          schema:
            type: string
            format: date
        - name: sort
          in: query
          required: false
          description: >-
            Orden del listado: registered_at:desc|asc, o las métricas deposited_window:desc,
            net_cash:desc, ltv:desc (siempre descendente). Con la ventana de depósitos activa y sin sort
            explícito, el default es deposited_window:desc. deposited_window requiere dep_from/dep_to.
          schema:
            type: string
            enum:
              - registered_at:desc
              - registered_at:asc
              - deposited_window:desc
              - net_cash:desc
              - ltv:desc
        - name: limit
          in: query
          required: false
          schema:
            type: integer
        - name: offset
          in: query
          required: false
          schema:
            type: integer
        - name: format
          in: query
          required: false
          description: Con valor "csv" descarga el listado como archivo CSV.
          schema:
            type: string
            enum:
              - csv
        - $ref: "#/components/parameters/DataStateExclude"
      responses:
        "200":
          description: Listado de jugadores (JSON) o descarga CSV si format=csv
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PlayerListPage"
            text/csv:
              schema:
                type: string
                format: binary
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /data/players/statuses:
    get:
      tags:
        - Data
      operationId: getDataPlayerStatuses
      summary: Catálogo de estados de cuenta de jugadores
      x-convray-runtime-status: active-data-v1
      description: >-
        Devuelve el catálogo estático de estados de cuenta (código y etiqueta)
        para los filtros del directorio. Requiere el producto Data; si el header
        de producto no coincide la puerta responde 404.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: tenant
          in: query
          required: false
          schema:
            type: string
      responses:
        "200":
          description: Catálogo de estados de cuenta
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/PlayerStatusOption"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
  /data/players/{ref}/profile:
    get:
      tags:
        - Data
      operationId: getDataPlayerProfile
      summary: Ficha del jugador
      x-convray-runtime-status: active-data-v1
      description: >-
        Devuelve la ficha del jugador: identidad, verificaciones, primer y último
        depósito pagado, KPIs históricos, conteos y PII (solo con permiso). El
        ref puede ser external_player_ref o casino_player_ref; responde 404 si el
        jugador no pertenece al tenant. Requiere el producto Data; si el header de
        producto no coincide la puerta responde 404.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: ref
          in: path
          required: true
          description: external_player_ref o casino_player_ref del jugador.
          schema:
            type: string
        - name: tenant
          in: query
          required: false
          schema:
            type: string
      responses:
        "200":
          description: Ficha del jugador
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PlayerProfile"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
  /data/players/{ref}/profile/activity:
    get:
      tags:
        - Data
      operationId: getDataPlayerProfileActivity
      summary: Línea de tiempo de movimientos del jugador
      x-convray-runtime-status: active-data-v1
      description: >-
        Devuelve la línea de tiempo paginada de movimientos del jugador
        (registro, depósitos, retiros, apuestas deportivas y casino por día). El
        ref puede ser external_player_ref o casino_player_ref; responde 404 si el
        jugador no pertenece al tenant. Requiere el producto Data; si el header de
        producto no coincide la puerta responde 404.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: ref
          in: path
          required: true
          schema:
            type: string
        - name: tenant
          in: query
          required: false
          schema:
            type: string
        - name: limit
          in: query
          required: false
          schema:
            type: integer
        - name: offset
          in: query
          required: false
          schema:
            type: integer
      responses:
        "200":
          description: Movimientos del jugador
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PlayerActivityPage"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /data/players/{ref}/profile/deposits:
    get:
      tags:
        - Data
      operationId: getDataPlayerProfileDeposits
      summary: Transacciones de depósito del jugador
      x-convray-runtime-status: active-data-v1
      description: >-
        Devuelve las transacciones de depósito paginadas del jugador. El ref
        puede ser external_player_ref o casino_player_ref; responde 404 si el
        jugador no pertenece al tenant. Requiere el producto Data; si el header de
        producto no coincide la puerta responde 404.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: ref
          in: path
          required: true
          schema:
            type: string
        - name: tenant
          in: query
          required: false
          schema:
            type: string
        - name: limit
          in: query
          required: false
          schema:
            type: integer
        - name: offset
          in: query
          required: false
          schema:
            type: integer
      responses:
        "200":
          description: Depósitos del jugador
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PlayerProfileDepositsPage"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /data/players/{ref}/profile/bets:
    get:
      tags:
        - Data
      operationId: getDataPlayerProfileBets
      summary: Apuestas deportivas del jugador
      x-convray-runtime-status: active-data-v1
      description: >-
        Devuelve las apuestas deportivas paginadas (fila por fila) del jugador;
        el casino se guarda agregado y aparece por día en la línea de tiempo. El
        ref puede ser external_player_ref o casino_player_ref; responde 404 si el
        jugador no pertenece al tenant. Requiere el producto Data; si el header de
        producto no coincide la puerta responde 404.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: ref
          in: path
          required: true
          schema:
            type: string
        - name: tenant
          in: query
          required: false
          schema:
            type: string
        - name: limit
          in: query
          required: false
          schema:
            type: integer
        - name: offset
          in: query
          required: false
          schema:
            type: integer
      responses:
        "200":
          description: Apuestas deportivas del jugador
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PlayerProfileBetsPage"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /data/players/{ref}/profile/monthly:
    get:
      tags:
        - Data
      operationId: getDataPlayerProfileMonthly
      summary: Serie mensual del jugador (depósitos, retiros, net cash y GGR)
      x-convray-runtime-status: active-data-v1
      description: >-
        Devuelve por mes del jugador, en America/Santiago, lo depositado y lo
        retirado (pagados, con conteos), el net cash y el GGR sin bono, hasta
        los últimos 24 meses con actividad, en orden ascendente. Lectura agregada sin datos personales. El ref puede ser
        external_player_ref o casino_player_ref; responde 404 si el jugador no
        pertenece al tenant. Requiere el producto Data; si el header de producto
        no coincide la puerta responde 404.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: ref
          in: path
          required: true
          schema:
            type: string
        - name: tenant
          in: query
          required: false
          schema:
            type: string
      responses:
        "200":
          description: Serie mensual del jugador
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PlayerProfileMonthlyPage"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /data/players/{ref}/profile/games:
    get:
      tags:
        - Data
      operationId: getDataPlayerProfileGames
      summary: Juegos más jugados del jugador
      x-convray-runtime-status: active-data-v1
      description: >-
        Devuelve los juegos de casino del jugador ordenados por lo apostado, con
        su porcentaje del total apostado. Lectura agregada sin datos personales.
        El ref puede ser external_player_ref o casino_player_ref; responde 404 si
        el jugador no pertenece al tenant. Requiere el producto Data; si el header
        de producto no coincide la puerta responde 404.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: ref
          in: path
          required: true
          schema:
            type: string
        - name: tenant
          in: query
          required: false
          schema:
            type: string
      responses:
        "200":
          description: Juegos más jugados del jugador
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PlayerProfileGamesPage"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /data/players/{ref}/profile/withdrawals:
    get:
      tags:
        - Data
      operationId: getDataPlayerProfileWithdrawals
      summary: Transacciones de retiro del jugador
      x-convray-runtime-status: active-data-v1
      description: >-
        Devuelve las transacciones de retiro paginadas del jugador. El ref puede
        ser external_player_ref o casino_player_ref; responde 404 si el jugador no
        pertenece al tenant. Requiere el producto Data; si el header de producto
        no coincide la puerta responde 404.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: ref
          in: path
          required: true
          schema:
            type: string
        - name: tenant
          in: query
          required: false
          schema:
            type: string
        - name: limit
          in: query
          required: false
          schema:
            type: integer
        - name: offset
          in: query
          required: false
          schema:
            type: integer
      responses:
        "200":
          description: Retiros del jugador
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PlayerProfileWithdrawalsPage"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /data/players/{ref}/profile/casino:
    get:
      tags:
        - Data
      operationId: getDataPlayerProfileCasino
      summary: Apuestas de casino del jugador por día y juego
      x-convray-runtime-status: active-data-v1
      description: >-
        Devuelve las apuestas de casino del jugador agregadas por día y juego
        (el casino se guarda agregado, no fila por fila), orden por día
        descendente. Lectura agregada sin datos personales. El ref puede ser
        external_player_ref o casino_player_ref; responde 404 si el jugador no
        pertenece al tenant. Requiere el producto Data; si el header de producto
        no coincide la puerta responde 404.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: ref
          in: path
          required: true
          schema:
            type: string
        - name: tenant
          in: query
          required: false
          schema:
            type: string
        - name: limit
          in: query
          required: false
          schema:
            type: integer
        - name: offset
          in: query
          required: false
          schema:
            type: integer
      responses:
        "200":
          description: Apuestas de casino del jugador (día×juego)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PlayerProfileCasinoPage"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /data/players/{ref}/profile/bonuses:
    get:
      tags:
        - Data
      operationId: getDataPlayerProfileBonuses
      summary: Bonos del jugador (sección Bonos de la ficha)
      x-convray-runtime-status: active-data-v1
      description: >-
        Lo que el jugador recibió en bonos (regalías, campañas y promo codes), lo que cobró, por
        familia (free_spins, free_bet, rollover, special siempre presentes; other solo si tiene
        bonos; used = bonos activados) y lo jugado con bono en casino y deportes desde el ledger
        diario (since = primer día con apuestas con bono). by_vertical agrupa por vertical (casino,
        sport, cash, unknown) con sus tipos (free_spins, free_bet, wager, cash, other) y cada fila trae
        la vertical del bono. Los bonos de prueba del propio jugador se listan marcados (is_test) pero
        no suman en kpis, by_family ni by_vertical; total cuenta todas las filas.
        period=month acota al mes civil en curso (America/Santiago). Sin PII: solo el Player ID de
        Centrivo. El ref puede ser external_player_ref o casino_player_ref; responde 404 si el
        jugador no pertenece al tenant. Requiere el producto Data; si el header de producto no
        coincide la puerta responde 404.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: ref
          in: path
          required: true
          schema:
            type: string
        - name: tenant
          in: query
          required: false
          schema:
            type: string
        - name: period
          in: query
          required: false
          description: historic (toda la historia, default) o month (mes civil en curso).
          schema:
            type: string
            enum:
              - historic
              - month
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 200
            default: 50
        - name: offset
          in: query
          required: false
          schema:
            type: integer
            minimum: 0
            default: 0
      responses:
        "200":
          description: Bonos del jugador
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataPlayerBonuses"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /data/players/rankings:
    get:
      tags:
        - Data
      operationId: getDataPlayersRankings
      summary: Ranking de jugadores por métrica
      x-convray-runtime-status: active-data-v1
      description: >-
        Devuelve el ranking de jugadores por métrica (net_cash, deposits,
        withdrawals, ggr) y dirección (top/bottom por valor, gainers/losers por
        variación contra el período anterior). PII solo para el rol admin.
        Requiere el producto Data; si el header de producto no coincide la puerta
        responde 404.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: tenant
          in: query
          required: false
          schema:
            type: string
        - name: from
          in: query
          required: false
          schema:
            type: string
            format: date
        - name: to
          in: query
          required: false
          schema:
            type: string
            format: date
        - name: metric
          in: query
          required: false
          description: Métrica del ranking. Default net_cash.
          schema:
            type: string
            enum:
              - net_cash
              - deposits
              - withdrawals
              - ggr
        - name: direction
          in: query
          required: false
          description: Dirección del ranking. Default top.
          schema:
            type: string
            enum:
              - top
              - bottom
              - gainers
              - losers
        - name: limit
          in: query
          required: false
          description: >-
            Cantidad de filas (Top N). El selector de la web manda 50/100/150/200;
            se acepta cualquier entero 1..200. Default 50.
          schema:
            type: integer
            minimum: 1
            maximum: 200
        - $ref: "#/components/parameters/DataTagFilter"
        - $ref: "#/components/parameters/DataFlagFilter"
        - $ref: "#/components/parameters/DataIspFilter"
        - $ref: "#/components/parameters/DataIspExclude"
        - $ref: "#/components/parameters/DataTagExclude"
        - $ref: "#/components/parameters/DataFlagExclude"
        - $ref: "#/components/parameters/DataContactable"
        - name: partner
          in: query
          required: false
          description: >-
            Filtra por socio/BTAG. Tokens none/organic (sin BTAG), any (con BTAG)
            o un slug de socio (exacto-o-prefijo).
          schema:
            type: string
        - name: dep_min
          in: query
          required: false
          description: >-
            "Depositó al menos $X": incluye sólo a los jugadores cuyo depósito
            pagado acumulado (en la ventana dep_from/dep_to, o histórico si no se
            da ventana) alcanza este mínimo, en unidad menor (CLP).
          schema:
            type: integer
            minimum: 0
        - name: dep_from
          in: query
          required: false
          description: >-
            Inicio (día civil Santiago) de la ventana propia de dep_min. Va junto
            con dep_to (ambos o ninguno).
          schema:
            type: string
            format: date
        - name: dep_to
          in: query
          required: false
          description: Fin inclusivo (día civil Santiago) de la ventana propia de dep_min.
          schema:
            type: string
            format: date
      responses:
        "200":
          description: Ranking de jugadores
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PlayerRankingsResponse"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /data/players/risk:
    get:
      tags:
        - Data
      operationId: getDataPlayersRisk
      summary: Señales de riesgo de jugadores del período
      x-convray-runtime-status: active-data-v1
      description: >-
        Devuelve las listas de riesgo del período: caída fuerte de actividad,
        retiros anómalos y depositantes con restricciones. PII solo para el rol
        admin. Requiere el producto Data; si el header de producto no coincide la
        puerta responde 404.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: tenant
          in: query
          required: false
          schema:
            type: string
        - name: from
          in: query
          required: false
          schema:
            type: string
            format: date
        - name: to
          in: query
          required: false
          schema:
            type: string
            format: date
        - $ref: "#/components/parameters/DataTagFilter"
        - $ref: "#/components/parameters/DataFlagFilter"
        - $ref: "#/components/parameters/DataIspFilter"
        - $ref: "#/components/parameters/DataIspExclude"
        - $ref: "#/components/parameters/DataTagExclude"
        - $ref: "#/components/parameters/DataFlagExclude"
        - $ref: "#/components/parameters/DataContactable"
      responses:
        "200":
          description: Señales de riesgo del período
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PlayerRiskResponse"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /data/players/cohorts:
    get:
      tags:
        - Data
      operationId: getDataPlayersCohorts
      summary: Cohortes de retención por mes de registro
      x-convray-runtime-status: active-data-v1
      description: >-
        Devuelve la matriz de cohortes por mes de registro con retención M0..Mn y
        conversión a primer depósito. basis define la actividad de retención
        (deposits o bets). Requiere el producto Data; si el header de producto no
        coincide la puerta responde 404.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: tenant
          in: query
          required: false
          schema:
            type: string
        - name: months
          in: query
          required: false
          description: Cantidad de meses de cohortes (1..24). Default 6.
          schema:
            type: integer
            minimum: 1
            maximum: 24
        - name: basis
          in: query
          required: false
          description: Base de actividad de la retención. Default deposits.
          schema:
            type: string
            enum:
              - deposits
              - bets
      responses:
        "200":
          description: Cohortes de retención
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PlayerCohortsResponse"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /data/players/active-by-login:
    get:
      tags:
        - Data
      operationId: getDataPlayersActiveByLogin
      summary: Jugadores activos por último login en el rango
      x-convray-runtime-status: active-data-v1
      description: >-
        Devuelve una foto de los jugadores cuyo último login cae en el rango (no
        es un histórico de logins), junto con el total con login alguna vez y la
        fecha desde la que el login es confiable. Requiere el producto Data; si el
        header de producto no coincide la puerta responde 404.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: tenant
          in: query
          required: false
          schema:
            type: string
        - name: from
          in: query
          required: false
          schema:
            type: string
            format: date
        - name: to
          in: query
          required: false
          schema:
            type: string
            format: date
      responses:
        "200":
          description: Activos por login del rango
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PlayerActiveByLoginResponse"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /data/players/vip-recovery:
    get:
      tags:
        - Data
      operationId: getDataPlayersVIPRecovery
      summary: Recuperación VIP del mes (apagados, bajando ritmo, net cash negativo)
      x-convray-runtime-status: active-data-v1
      description: >-
        Lista de recuperación de los jugadores con etiqueta VIP vigente
        (Diamante + Zafiro) para el mes evaluado (default: mes civil actual en
        Santiago; parámetro month=YYYY-MM). Clasifica en Apagados (depositó el
        mes anterior y nada en el actual), Bajando ritmo (por debajo del 60 % del
        ritmo diario del mes anterior) y Net Cash negativo (retiros pagados >
        depósitos del mes). Los autoexcluidos y bloqueados NUNCA aparecen en las
        listas: se informan solo como conteo (juego responsable). Con
        format=csv&list=... descarga la lista activa como CSV y registra el
        reporte en el log de exportaciones. Sin PII. Requiere el producto Data;
        si el header de producto no coincide la puerta responde 404.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: tenant
          in: query
          required: false
          schema:
            type: string
        - name: month
          in: query
          required: false
          description: Mes evaluado YYYY-MM. Default el mes civil actual en Santiago.
          schema:
            type: string
            pattern: ^[0-9]{4}-[0-9]{2}$
        - name: format
          in: query
          required: false
          description: Con valor "csv" descarga la lista activa como archivo CSV.
          schema:
            type: string
            enum:
              - csv
        - name: list
          in: query
          required: false
          description: >-
            Lista a exportar cuando format=csv. Obligatorio con format=csv.
          schema:
            type: string
            enum:
              - apagados
              - bajando_ritmo
              - net_cash_negativo
      responses:
        "200":
          description: Recuperación VIP del mes (JSON) o descarga CSV si format=csv
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/VIPRecoveryResponse"
            text/csv:
              schema:
                type: string
                format: binary
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /data/vip-program/candidates:
    get:
      tags:
        - Data
      operationId: getDataVIPProgramCandidates
      summary: Candidatos del programa de detección temprana VIP (por carril y nivel)
      x-convray-runtime-status: active-data-v1
      description: >-
        Lista de candidatos del programa de detección temprana de jugadores VIP para un día
        puntuado. Sin date, devuelve la última fecha puntuada disponible; sin lane, ambos carriles
        (nuevos y escalamiento). Cada candidato trae su nivel, score, motivos legibles, la marca de
        juego responsable con sus señales y el grupo del experimento (tratamiento/control). Solo
        lectura de data_vip_candidates bajo la RLS de producto; el detalle lo escribe el worker
        diario. Sin PII. Requiere el producto Data; si el header de producto no coincide la puerta
        responde 404.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: tenant
          in: query
          required: false
          schema:
            type: string
        - name: date
          in: query
          required: false
          description: Día puntuado YYYY-MM-DD. Default la última fecha puntuada disponible.
          schema:
            type: string
            format: date
        - name: lane
          in: query
          required: false
          description: Carril a filtrar. Sin valor, ambos carriles.
          schema:
            type: string
            enum:
              - new
              - escalation
      responses:
        "200":
          description: Candidatos del día por carril y nivel, con resumen.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/VIPProgramCandidatesResponse"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /data/bonuses/summary:
    get:
      tags:
        - Data
      operationId: getDataBonusesSummary
      summary: Resumen del módulo Bonos (regalado, cobrado, propósitos, familias y pestañas)
      x-convray-runtime-status: active-data-v1
      description: >-
        Resumen de los bonos entregados en el período (PRD-52 ola 4): lo regalado (Σ
        bonus_amount_minor), lo cobrado a dinero real (Σ redeemed_amount_minor de los bonos
        cerrados), bonos, jugadores y conteo por etiqueta (cobrado, sin_cobro, vencido, cancelado,
        activo); desglose por propósito y por familia (free_spins, free_bet, rollover, special
        siempre presentes; other solo si tiene bonos); conteos de pestañas y las 6 campañas con más
        regalado. La fecha del bono es COALESCE(triggered_at, campaign_started_at, first_seen_at) en
        días civiles America/Santiago. Por defecto los bonos de prueba (receptor Test Player o staff
        vigente, o campaña de prueba efectiva) no suman; include_tests=true los incluye. Un bono
        entregado cuenta una sola vez. Montos CLP en unidades menores (minor = pesos). Sin PII: solo
        el Player ID de Centrivo. Por vertical (migración 20260927120000): by_vertical suma bonos,
        jugadores, campañas, regalado, cobrado y los Wager de cada vertical con bonos (casino, sport,
        cash, unknown), y series reparte lo regalado por vertical y lo cobrado en baldes de día, semana
        (lunes a domingo) o mes en días civiles de Chile; los indicadores suman siempre el período
        completo. vertical filtra todo el resumen por la vertical del bono. Requiere el producto Data
        (rol mínimo viewer); sin acceso, con tenant ajeno o con el header de producto de otra puerta
        responde 404.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: tenant
          in: query
          required: false
          schema:
            type: string
        - name: from
          in: query
          required: false
          description: Primer día civil (YYYY-MM-DD). Default los 45 días que terminan en to.
          schema:
            type: string
            format: date
        - name: to
          in: query
          required: false
          description: Último día civil inclusivo (YYYY-MM-DD). Default hoy en America/Santiago.
          schema:
            type: string
            format: date
        - name: include_tests
          in: query
          required: false
          description: true incluye los bonos de prueba y staff. Default false.
          schema:
            type: boolean
        - name: vertical
          in: query
          required: false
          description: >-
            all (default), casino o sport. Filtra por la vertical de cada BONO: una campaña Mixta entra en
            casino y en sport con solo sus bonos de esa vertical; los de dinero real (cash) y sin vertical
            (unknown) solo entran en all. Otro valor responde 422.
          schema:
            type: string
            enum:
              - all
              - casino
              - sport
            default: all
        - name: granularity
          in: query
          required: false
          description: >-
            Baldes de series: day (solo con períodos de hasta 92 días), week (default, lunes a domingo;
            períodos de hasta 1092 días) o month (períodos de hasta 3653 días). La serie lleva todos sus
            baldes, también los vacíos. Otro valor, o un período más largo que el tope de la granularidad
            (también la semana por defecto), responde 422.
          schema:
            type: string
            enum:
              - day
              - week
              - month
            default: week
      responses:
        "200":
          description: Resumen del período.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataBonusSummary"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /data/bonuses/campaigns:
    get:
      tags:
        - Data
      operationId: listDataBonusCampaigns
      summary: Campañas de bonos (regalías cross-platform y campañas normales) del período
      x-convray-runtime-status: active-data-v1
      description: >-
        Listado paginado de campañas de bonos. Entra una campaña si tuvo bonos que suman en el
        período o si se creó en el período (las nuevas aún sin bonos aparecen en cero); las campañas
        de prueba efectiva solo con include_tests=true. Las cifras son las del período. Orden
        descendente por sort, desempate por id. Es la misma regla de tabs.cross_platform y
        tabs.campaign del resumen. created_by es el operador de Centrivo que creó la campaña (staff).
        Con vertical=casino|sport las cifras cuentan solo los bonos de esa vertical (una campaña Mixta
        aparece en las dos) y una campaña nueva todavía sin bonos entra si su vertical es la pedida o
        mixed. Requiere el producto Data (rol mínimo viewer); sin acceso o con tenant ajeno responde 404.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: tenant
          in: query
          required: false
          schema:
            type: string
        - name: from
          in: query
          required: false
          schema:
            type: string
            format: date
        - name: to
          in: query
          required: false
          schema:
            type: string
            format: date
        - name: include_tests
          in: query
          required: false
          schema:
            type: boolean
        - name: vertical
          in: query
          required: false
          description: >-
            all (default), casino o sport. Filtra por la vertical de cada BONO: una campaña Mixta entra en
            casino y en sport con solo sus bonos de esa vertical; los de dinero real (cash) y sin vertical
            (unknown) solo entran en all. Otro valor responde 422.
          schema:
            type: string
            enum:
              - all
              - casino
              - sport
            default: all
        - name: kind
          in: query
          required: false
          description: Tipo de campaña. Default all.
          schema:
            type: string
            enum:
              - all
              - cross_platform
              - campaign
        - name: purpose
          in: query
          required: false
          description: Propósito de la campaña.
          schema:
            $ref: "#/components/schemas/DataBonusPurpose"
        - name: q
          in: query
          required: false
          description: Busca en el nombre (sin distinguir mayúsculas) o por id exacto. Hasta 100 caracteres.
          schema:
            type: string
            maxLength: 100
        - name: sort
          in: query
          required: false
          description: Orden descendente por regalado, cobrado, bonos o fecha de creación. Default given.
          schema:
            type: string
            enum:
              - given
              - paid
              - grants
              - created
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 200
            default: 50
        - name: offset
          in: query
          required: false
          schema:
            type: integer
            minimum: 0
            default: 0
      responses:
        "200":
          description: Página de campañas.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataBonusCampaignList"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /data/bonuses/campaigns/{kind}/{id}:
    get:
      tags:
        - Data
      operationId: getDataBonusCampaign
      summary: Ficha de una campaña de bonos
      x-convray-runtime-status: active-data-v1
      description: >-
        Ficha de una campaña: su fila con las cifras de TODA su historia, datos del catálogo (unidades,
        bonos, disparador, inicio y fin, última lectura, giros y rollover más frecuentes), qué pasó
        con sus bonos, los depósitos pagados de sus receptores en los `days` días antes y después del
        inicio de la campaña, cuántos jugadores recibieron más de un bono y la página de receptores
        (solo Player ID) ordenada por cobrado. Una campaña de prueba efectiva (is_test) incluye siempre
        sus bonos, porque todos son de prueba (include_tests no aplica); las demás siguen
        include_tests. 404 si la campaña no existe en el tenant. Requiere el producto Data (rol
        mínimo viewer).
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: kind
          in: path
          required: true
          schema:
            type: string
            enum:
              - cross_platform
              - campaign
        - name: id
          in: path
          required: true
          description: Id de la campaña en Centrivo.
          schema:
            type: integer
            format: int64
            minimum: 1
        - name: tenant
          in: query
          required: false
          schema:
            type: string
        - name: include_tests
          in: query
          required: false
          schema:
            type: boolean
        - name: days
          in: query
          required: false
          description: Días de la ventana de depósitos antes y después del inicio. Default 7.
          schema:
            type: integer
            minimum: 1
            maximum: 31
            default: 7
        - name: recipients_limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 200
            default: 20
        - name: recipients_offset
          in: query
          required: false
          schema:
            type: integer
            minimum: 0
            default: 0
      responses:
        "200":
          description: Ficha de la campaña.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataBonusCampaignDetail"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /data/bonuses/rollover:
    get:
      tags:
        - Data
      operationId: getDataBonusesRollover
      summary: Bonos que activó un depósito (rollover) en el período
      x-convray-runtime-status: active-data-v1
      description: >-
        Cifras de los bonos con depósito disparador (trigger_transaction_ref) entregados en el
        período: lo depositado, el bono entregado, el rollover exigido, lo cobrado, cuántos
        cumplieron, cuántos canceló el jugador, los abiertos con su rollover pendiente y cuántos
        depósitos se encontraron en Data; y las campañas con bono por depósito (por monto depositado)
        con sus bonos del catálogo para abrir /data/bonuses/rollover/bonus/{bonusId}. by_vertical
        compara casino y deporte (solo las verticales con bonos) y cada campaña trae su vertical y sus
        bonos de casino y de deporte. Requiere el producto Data (rol mínimo viewer).
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: tenant
          in: query
          required: false
          schema:
            type: string
        - name: from
          in: query
          required: false
          schema:
            type: string
            format: date
        - name: to
          in: query
          required: false
          schema:
            type: string
            format: date
        - name: include_tests
          in: query
          required: false
          schema:
            type: boolean
        - name: vertical
          in: query
          required: false
          description: >-
            all (default), casino o sport. Filtra por la vertical de cada BONO: una campaña Mixta entra en
            casino y en sport con solo sus bonos de esa vertical; los de dinero real (cash) y sin vertical
            (unknown) solo entran en all. Otro valor responde 422.
          schema:
            type: string
            enum:
              - all
              - casino
              - sport
            default: all
      responses:
        "200":
          description: Rollover del período.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataBonusRollover"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /data/bonuses/rollover/bonus/{bonusId}:
    get:
      tags:
        - Data
      operationId: listDataBonusRolloverGrants
      summary: Bonos con depósito de un bono del catálogo
      x-convray-runtime-status: active-data-v1
      description: >-
        Lista paginada (más recientes primero) de los bonos que activó un depósito para un bono del
        catálogo de Centrivo: depósito → bono, rollover exigido y pendiente, etiqueta, cobrado y
        quién lo canceló. deposit_ref va enmascarado salvo con permiso de PII completa (misma regla
        que las transacciones de la ficha). vertical es la del bono y siblings lista los bonos con
        depósito de la misma campaña, incluido este (en una campaña Mixta, uno por vertical). 404 si
        Data no tiene bonos con depósito de ese bono. Requiere el producto Data (rol mínimo viewer).
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: bonusId
          in: path
          required: true
          schema:
            type: integer
            format: int64
            minimum: 1
        - name: tenant
          in: query
          required: false
          schema:
            type: string
        - name: include_tests
          in: query
          required: false
          schema:
            type: boolean
        - name: vertical
          in: query
          required: false
          description: >-
            all (default), casino o sport. Filtra por la vertical de cada BONO: una campaña Mixta entra en
            casino y en sport con solo sus bonos de esa vertical; los de dinero real (cash) y sin vertical
            (unknown) solo entran en all. Otro valor responde 422.
          schema:
            type: string
            enum:
              - all
              - casino
              - sport
            default: all
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 200
            default: 50
        - name: offset
          in: query
          required: false
          schema:
            type: integer
            minimum: 0
            default: 0
      responses:
        "200":
          description: Bonos con depósito del bono.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataBonusRolloverBonus"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /data/bonuses/promo/packs:
    get:
      tags:
        - Data
      operationId: listDataBonusPromoPacks
      summary: Packs de promo codes
      x-convray-runtime-status: active-data-v1
      description: >-
        Packs de promo codes de Centrivo (Single, Multiple, External), los más recientes primero
        (sort=recent, por id descendente) o por usos (sort=used). kpis son de los packs del casino SIN
        los descartados y sin filtros (kpis.discarded cuenta los descartados); total y rows respetan
        type, status y q. Por defecto solo los activos (status=active). used/codes son
        los contadores de Centrivo; uses_loaded los usos que Data tiene cargados y uses_with_bonus
        los que generaron bono, ambos con todos los usos (pruebas y staff incluidos, la misma base
        que used); paid_minor es lo cobrado de esos bonos (cada bono una vez, sin pruebas). Requiere
        el producto Data (rol mínimo viewer).
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: tenant
          in: query
          required: false
          schema:
            type: string
        - name: type
          in: query
          required: false
          description: Tipo de pack. Sin valor (o all), todos.
          schema:
            type: string
            enum:
              - all
              - single
              - multiple
              - external
        - name: q
          in: query
          required: false
          description: >-
            Busca por palabras: todas deben estar en el nombre, en cualquier orden y posición, sin
            distinguir mayúsculas ni tildes («halloween nivel» encuentra «Halloween - Nivel 5»); un
            número también encuentra el pack con ese id. Hasta 100 caracteres.
          schema:
            type: string
            maxLength: 100
        - name: sort
          in: query
          required: false
          description: Orden de la lista. recent = id descendente (los ids de Centrivo crecen con cada pack); used = usos de Centrivo.
          schema:
            type: string
            enum:
              - recent
              - used
            default: recent
        - name: status
          in: query
          required: false
          description: Estado de la marca manual. active = sin descartados; discarded = solo los descartados; all = todos.
          schema:
            type: string
            enum:
              - active
              - discarded
              - all
            default: active
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 200
            default: 50
        - name: offset
          in: query
          required: false
          schema:
            type: integer
            minimum: 0
            default: 0
      responses:
        "200":
          description: Packs de promo codes.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataBonusPromoPacks"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /data/bonuses/promo/packs/{id}/uses:
    get:
      tags:
        - Data
      operationId: listDataBonusPromoPackUses
      summary: Usos de un pack de promo codes
      x-convray-runtime-status: active-data-v1
      description: >-
        El pack (misma fila que /data/bonuses/promo/packs) y una página de sus usos, más recientes
        primero: código, Player ID, fecha y el bono que generó (etiqueta y cobrado). Un canje que
        entregó varios bonos trae una fila por bono, con el mismo use_id. Varios usos pueden enlazar el
        mismo bono; solo cuenta el uso con is_grant_owner. La lista trae todos los usos (total = filas
        de la lista; uses_loaded del pack cuenta canjes), pruebas y staff incluidos: el cobrado de un
        uso de prueba no suma en el paid_minor del pack. Sin correos ni nombres de jugador. 404 si el
        pack no existe en el tenant. Requiere el producto Data (rol mínimo viewer).
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
            format: int64
            minimum: 1
        - name: tenant
          in: query
          required: false
          schema:
            type: string
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 200
            default: 50
        - name: offset
          in: query
          required: false
          schema:
            type: integer
            minimum: 0
            default: 0
      responses:
        "200":
          description: Usos del pack.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataBonusPromoUses"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /data/bonuses/promo/packs/{id}/discard:
    put:
      tags:
        - Data
      operationId: discardDataBonusPromoPack
      summary: Marcar un pack de promo codes como descartado
      x-convray-runtime-status: active-data-v1
      description: >-
        Marca el pack como descartado (packs que se crearon y luego se desecharon sin borrarlos en
        Centrivo). Deja de contar en Emitidos, en los KPIs y en el conteo de la pestaña, y sale de la
        lista por defecto (status=active). No toca el dinero: el cobrado sale de los bonos entregados, así
        que un pack descartado con canjes sigue contando en los totales de bonos y solo se avisa en
        "Para revisar". Idempotente: marcar un pack ya marcado solo actualiza la nota. Cuerpo opcional con
        una nota de hasta 500 caracteres. Solo admin de Data o super_admin de plataforma; sin ese rol
        responde 404, igual que el resto de Data. 404 también si el pack no existe en el tenant. Devuelve el
        pack con su marca al día. Detrás de la puerta de producto 'data'.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
            format: int64
            minimum: 1
        - name: tenant
          in: query
          required: false
          schema:
            type: string
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                note:
                  type: [string, "null"]
                  maxLength: 500
                  description: Motivo del descarte. Vacío o ausente = sin nota.
      responses:
        "200":
          description: El pack ya marcado como descartado.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataBonusPromoPackDiscard"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
    delete:
      tags:
        - Data
      operationId: undiscardDataBonusPromoPack
      summary: Quitar la marca de descartado de un pack de promo codes
      x-convray-runtime-status: active-data-v1
      description: >-
        Quita la marca de descartado: el pack vuelve a contar en Emitidos, en los KPIs y en la lista por
        defecto. Idempotente: quitar la marca de un pack sin marca no cambia nada. Mismos permisos y
        404 que el PUT. Devuelve el pack con su marca al día.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
            format: int64
            minimum: 1
        - name: tenant
          in: query
          required: false
          schema:
            type: string
      responses:
        "200":
          description: El pack sin marca de descartado.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataBonusPromoPackDiscard"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /data/bonuses/promo/code:
    get:
      tags:
        - Data
      operationId: getDataBonusPromoCode
      summary: Buscar un promo code
      x-convray-runtime-status: active-data-v1
      description: >-
        Busca UN promo code y dice si se canjeó y si su campaña ya venció. El código se compara sin
        espacios al borde y sin distinguir mayúsculas contra los canjes guardados desde Centrivo y el
        código de los packs Single. status es redeemed (tiene canjes), not_redeemed (existe como código
        de un pack Single sin canjes) o not_found. La vigencia es la de la campaña de cada pack: vencido si
        terminó en Centrivo o su fecha de fin ya pasó, programado si todavía no empieza, vigente si está
        activa y sin_dato si el pack no tiene campaña leída ni estado; validity agrega los packs (gana
        vigente, luego programado, luego vencido). rows es una página de canjes, más recientes primero; un
        canje que entregó varios bonos trae una fila por bono (total cuenta filas, uses cuenta canjes). Un
        código de un pack Multiple o External que nadie canjeó responde not_found: sus códigos sin usar no
        se leen en Data. Sin correos ni nombres de jugador. Requiere el producto Data (rol mínimo viewer).
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: tenant
          in: query
          required: false
          schema:
            type: string
        - name: code
          in: query
          required: true
          description: El código a buscar, hasta 64 caracteres, sin caracteres de control.
          schema:
            type: string
            minLength: 1
            maxLength: 64
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 200
            default: 50
        - name: offset
          in: query
          required: false
          schema:
            type: integer
            minimum: 0
            default: 0
      responses:
        "200":
          description: El código con sus packs y una página de sus canjes.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataBonusPromoCode"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /data/bonuses/review:
    get:
      tags:
        - Data
      operationId: getDataBonusesReview
      summary: Hallazgos para revisar del módulo Bonos
      x-convray-runtime-status: active-data-v1
      description: >-
        Hallazgos de toda la historia, en orden fijo y con el texto listo para mostrar: expired_share
        (campañas con al menos 500 bonos y 80 % o más vencidos), recreated (campañas cuyo nombre
        normalizado coincide: minúsculas, sin los sufijos " ok", " 2" y " v2" y sin la s final de cada
        palabra), uses_without_bonus (packs con usos que no generaron bono), pack_discardable
        (packs sin marca, sin una sola activación ni used en Centrivo, cuya campaña terminó o que se
        vieron por primera vez hace más de 7 días; es una sugerencia), pack_discarded_with_redemptions
        (packs marcados como descartados con canjes que generaron bono: ese dinero sí salió y sigue
        contando), test_by_recipients (campañas cuyos bonos fueron solo a prueba o staff) y
        test_review (nombre de prueba con receptores reales). Con filtro por jugador no hay hallazgos
        de packs descartados. Un tipo sin hallazgos no aparece. Las campañas de prueba no cuentan en
        vencidos ni en recreadas, y los usos de prueba o staff no cuentan en uses_without_bonus.
        Requiere el producto Data (rol mínimo viewer).
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: tenant
          in: query
          required: false
          schema:
            type: string
      responses:
        "200":
          description: Hallazgos para revisar.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataBonusReview"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
  /data/filters/options:
    get:
      tags:
        - Data
      operationId: getDataFilterOptions
      summary: Opciones de filtro por dimensión de un dataset
      x-convray-runtime-status: active-data-v1
      description: >-
        Puebla los selectores del contrato: por cada dimensión del dataset,
        los valores distintos con su conteo, ordenados por frecuencia, con tope
        y búsqueda por prefijo. Sale de los agregados, no de filas crudas. Si el
        producto Data no está habilitado para el tenant, responde 404.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: tenant
          in: query
          required: false
          description: Slug del tenant cuando la sesión abarca varios.
          schema:
            type: string
        - name: dataset
          in: query
          required: true
          description: Dataset del contrato (deposits, withdrawals, casino_bets, sport_bets, players).
          schema:
            type: string
        - name: dimension
          in: query
          required: false
          description: Limita la respuesta a una sola dimensión; si se omite, devuelve todas.
          schema:
            type: string
        - name: q
          in: query
          required: false
          description: Prefijo de búsqueda para dimensiones largas (juegos, btag).
          schema:
            type: string
        - name: from
          in: query
          required: false
          description: Inicio del rango como día civil America/Santiago (YYYY-MM-DD).
          schema:
            type: string
            format: date
        - name: to
          in: query
          required: false
          description: Fin del rango como día civil America/Santiago (YYYY-MM-DD).
          schema:
            type: string
            format: date
      responses:
        "200":
          description: Dimensiones pedidas con sus valores
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataFilterOptionsResponse"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /data/metric-alerts/catalog:
    get:
      tags:
        - Data
      operationId: getDataMetricAlertCatalog
      summary: Catálogo de métricas vigilables
      x-convray-runtime-status: active-data-v1
      description: >-
        Catálogo cerrado de métricas sobre las que se pueden crear reglas de
        alerta, con su etiqueta y unidad de formato. Si el producto Data no está
        habilitado para el tenant, responde 404.
      security:
        - adminBearer: []
        - dataApiToken: []
      responses:
        "200":
          description: Métricas vigilables
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataMetricAlertCatalogResponse"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
  /data/metric-alerts/rules:
    get:
      tags:
        - Data
      operationId: listDataMetricAlertRules
      summary: Lista las reglas de alerta de métricas
      x-convray-runtime-status: active-data-v1
      description: >-
        Devuelve las reglas de alerta del tenant. Si el producto Data no está
        habilitado para el tenant, responde 404.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: tenant
          in: query
          required: false
          description: Slug del tenant cuando la sesión abarca varios.
          schema:
            type: string
      responses:
        "200":
          description: Reglas del tenant
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataMetricAlertRuleList"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
    post:
      tags:
        - Data
      operationId: createDataMetricAlertRule
      summary: Crea una regla de alerta de métrica
      x-convray-runtime-status: active-data-v1
      description: >-
        Crea una regla que vigila una métrica del catálogo por día civil. Exige
        admin de Data. Si el producto Data no está habilitado para el tenant,
        responde 404.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: tenant
          in: query
          required: false
          description: Slug del tenant cuando la sesión abarca varios.
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/DataMetricAlertRuleCreateBody"
      responses:
        "201":
          description: Regla creada
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataMetricAlertRule"
        "400":
          description: Cuerpo inválido
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /data/metric-alerts/rules/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
    patch:
      tags:
        - Data
      operationId: updateDataMetricAlertRule
      summary: Edita parcialmente una regla de alerta
      x-convray-runtime-status: active-data-v1
      description: >-
        Actualiza umbral, baseline, severidad o activación de una regla. Exige
        admin de Data. Si el producto Data no está habilitado para el tenant,
        responde 404.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: tenant
          in: query
          required: false
          description: Slug del tenant cuando la sesión abarca varios.
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/DataMetricAlertRulePatchBody"
      responses:
        "200":
          description: Regla actualizada
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataMetricAlertRule"
        "400":
          description: Cuerpo inválido
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
    delete:
      tags:
        - Data
      operationId: deleteDataMetricAlertRule
      summary: Borra una regla de alerta
      x-convray-runtime-status: active-data-v1
      description: >-
        Elimina una regla de alerta. Exige admin de Data. Si el producto Data no
        está habilitado para el tenant, responde 404.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: tenant
          in: query
          required: false
          description: Slug del tenant cuando la sesión abarca varios.
          schema:
            type: string
      responses:
        "204":
          description: Regla eliminada
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
  /data/metric-alerts/events:
    get:
      tags:
        - Data
      operationId: listDataMetricAlertEvents
      summary: Lista los eventos de alerta disparados
      x-convray-runtime-status: active-data-v1
      description: >-
        Devuelve los eventos abiertos por el evaluador, opcionalmente filtrados
        por estado. Si el producto Data no está habilitado para el tenant,
        responde 404.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: tenant
          in: query
          required: false
          description: Slug del tenant cuando la sesión abarca varios.
          schema:
            type: string
        - name: status
          in: query
          required: false
          description: Filtra por estado del evento (open, acknowledged, resolved).
          schema:
            type: string
      responses:
        "200":
          description: Eventos del tenant
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataMetricAlertEventList"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
  /data/metric-alerts/events/{id}/acknowledge:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
    post:
      tags:
        - Data
      operationId: acknowledgeDataMetricAlertEvent
      summary: Reconoce un evento de alerta
      x-convray-runtime-status: active-data-v1
      description: >-
        Marca un evento como reconocido. Exige admin de Data. Si el producto
        Data no está habilitado para el tenant, responde 404.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: tenant
          in: query
          required: false
          description: Slug del tenant cuando la sesión abarca varios.
          schema:
            type: string
      responses:
        "200":
          description: Evento reconocido
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataMetricAlertEvent"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
  /data/metric-alerts/events/{id}/resolve:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
    post:
      tags:
        - Data
      operationId: resolveDataMetricAlertEvent
      summary: Resuelve un evento de alerta
      x-convray-runtime-status: active-data-v1
      description: >-
        Marca un evento como resuelto. Exige admin de Data. Si el producto Data
        no está habilitado para el tenant, responde 404.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: tenant
          in: query
          required: false
          description: Slug del tenant cuando la sesión abarca varios.
          schema:
            type: string
      responses:
        "200":
          description: Evento resuelto
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataMetricAlertEvent"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
  /data/reports/preview:
    post:
      tags:
        - Data
      operationId: previewDataReport
      summary: Genera un informe estructurado de solo lectura
      x-convray-runtime-status: active-data-v1
      description: >-
        Arma un informe agregado (nunca lista de jugadores ni PII) a partir de
        un rango y un tipo (monthly_summary, deposits, withdrawals, casino,
        sports, players, segment). Con ?format=markdown|md responde text/markdown
        y con ?format=csv responde text/csv; por defecto JSON. Si el producto
        Data no está habilitado para el tenant, responde 404.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: tenant
          in: query
          required: false
          description: Slug del tenant cuando la sesión abarca varios.
          schema:
            type: string
        - name: format
          in: query
          required: false
          description: Formato de salida (json por defecto, markdown/md, csv).
          schema:
            type: string
            enum:
              - json
              - markdown
              - md
              - csv
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/DataReportRequest"
      responses:
        "200":
          description: Informe generado (JSON, markdown o CSV según format)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataReport"
            text/markdown:
              schema:
                type: string
            text/csv:
              schema:
                type: string
        "400":
          description: Cuerpo inválido
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /data/segments:
    get:
      tags:
        - Data
      operationId: listDataSegments
      summary: Lista los segmentos guardados
      x-convray-runtime-status: active-data-v1
      description: >-
        Devuelve los segmentos del tenant (dinámicos y estáticos) con sus
        conteos cuando aplican. Si el producto Data no está habilitado para el
        tenant, responde 404.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: tenant
          in: query
          required: false
          description: Slug del tenant cuando la sesión abarca varios.
          schema:
            type: string
      responses:
        "200":
          description: Segmentos del tenant
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataSegmentList"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
    post:
      tags:
        - Data
      operationId: createDataSegment
      summary: Crea un segmento dinámico
      x-convray-runtime-status: active-data-v1
      description: >-
        Crea un segmento guardado a partir de una combinación de filtros del
        contrato (dataset players). Si el producto Data no está habilitado para
        el tenant, responde 404.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: tenant
          in: query
          required: false
          description: Slug del tenant cuando la sesión abarca varios.
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/DataSegmentCreateBody"
      responses:
        "201":
          description: Segmento creado
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataSegment"
        "400":
          description: Cuerpo inválido
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "409":
          description: Ya existe un segmento con ese nombre
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /data/segments/import:
    post:
      tags:
        - Data
      operationId: importDataSegment
      summary: Crea un segmento estático desde una lista de Player IDs
      x-convray-runtime-status: active-data-v1
      description: >-
        Sube una lista de Player IDs (multipart con un CSV en el campo file, o
        JSON con player_ids), la valida contra el snapshot de Players y crea un
        segmento estático con su foto congelada. Nunca guarda el archivo ni PII.
        Si el producto Data no está habilitado para el tenant, responde 404.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: tenant
          in: query
          required: false
          description: Slug del tenant cuando la sesión abarca varios.
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/DataStaticImportBody"
          multipart/form-data:
            schema:
              $ref: "#/components/schemas/DataStaticImportMultipart"
      responses:
        "201":
          description: Segmento estático creado
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataStaticImportResult"
        "400":
          description: Cuerpo, archivo o CSV inválido
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "413":
          description: El archivo o la lista supera el tope permitido
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /data/segments/preview:
    post:
      tags:
        - Data
      operationId: previewDataSegmentFilters
      summary: Previsualiza filtros de segmento sin guardarlo
      x-convray-runtime-status: active-data-v1
      description: >-
        Recibe un objeto de filtros del contrato (mismo formato que un segmento
        dinámico) y devuelve el mismo preview que por id, sin persistir. Nunca
        la lista de jugadores. Si el producto Data no está habilitado para el
        tenant, responde 404.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: tenant
          in: query
          required: false
          description: Slug del tenant cuando la sesión abarca varios.
          schema:
            type: string
        - name: from
          in: query
          required: false
          description: Inicio del rango como día civil America/Santiago (YYYY-MM-DD).
          schema:
            type: string
            format: date
        - name: to
          in: query
          required: false
          description: Fin del rango como día civil America/Santiago (YYYY-MM-DD).
          schema:
            type: string
            format: date
      requestBody:
        required: true
        description: Objeto de filtros del contrato (dataset players).
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Preview del segmento (agregados y juego responsable)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataSegmentPreview"
        "400":
          description: Filtros inválidos
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /data/segments/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
    patch:
      tags:
        - Data
      operationId: updateDataSegment
      summary: Edita parcialmente un segmento
      x-convray-runtime-status: active-data-v1
      description: >-
        Actualiza nombre, descripción, compartición o filtros de un segmento. Si
        el producto Data no está habilitado para el tenant, responde 404.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: tenant
          in: query
          required: false
          description: Slug del tenant cuando la sesión abarca varios.
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/DataSegmentPatchBody"
      responses:
        "200":
          description: Segmento actualizado
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataSegment"
        "400":
          description: Cuerpo inválido
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "409":
          description: Ya existe un segmento con ese nombre
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "422":
          $ref: "#/components/responses/ValidationFailed"
    delete:
      tags:
        - Data
      operationId: deleteDataSegment
      summary: Archiva un segmento
      x-convray-runtime-status: active-data-v1
      description: >-
        Archivado suave de un segmento. Si el producto Data no está habilitado
        para el tenant, responde 404.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: tenant
          in: query
          required: false
          description: Slug del tenant cuando la sesión abarca varios.
          schema:
            type: string
      responses:
        "204":
          description: Segmento archivado
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
  /data/segments/{id}/preview:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
    get:
      tags:
        - Data
      operationId: previewDataSegment
      summary: Previsualiza un segmento guardado
      x-convray-runtime-status: active-data-v1
      description: >-
        Devuelve el conteo y los agregados del segmento en el rango, más el
        conteo de juego responsable. Nunca la lista de jugadores. Si el producto
        Data no está habilitado para el tenant, responde 404.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: tenant
          in: query
          required: false
          description: Slug del tenant cuando la sesión abarca varios.
          schema:
            type: string
        - name: from
          in: query
          required: false
          description: Inicio del rango como día civil America/Santiago (YYYY-MM-DD).
          schema:
            type: string
            format: date
        - name: to
          in: query
          required: false
          description: Fin del rango como día civil America/Santiago (YYYY-MM-DD).
          schema:
            type: string
            format: date
      responses:
        "200":
          description: Preview del segmento
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataSegmentPreview"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /data/segments/{id}/uploads:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
    get:
      tags:
        - Data
      operationId: listDataSegmentUploads
      summary: Historial de cargas de CSV de un segmento estático
      x-convray-runtime-status: active-data-v1
      description: >-
        Devuelve las cargas de CSV de un segmento estático (auditoría: autor,
        fecha, nombre y hash del archivo y conteos). Nunca la lista de Player
        IDs. Si el producto Data no está habilitado para el tenant, responde 404.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: tenant
          in: query
          required: false
          description: Slug del tenant cuando la sesión abarca varios.
          schema:
            type: string
      responses:
        "200":
          description: Historial de cargas
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataSegmentUploadList"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
  /data/connector/centrivo:
    get:
      tags:
        - Data
      operationId: getCentrivoConnector
      summary: Estado del conector de Centrivo
      x-convray-runtime-status: active-connector-centrivo
      description: >-
        Devuelve la configuración del conector, los reportes con su cobertura y la hora del
        servidor. Detrás de la puerta de producto 'data': si el tenant no tiene ese producto
        habilitado responde 404.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: tenant
          in: query
          required: false
          description: Slug del tenant (override del tenant de la sesión).
          schema:
            type: string
      responses:
        "200":
          description: Estado actual del conector
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CentrivoConnectorView"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
    put:
      tags:
        - Data
      operationId: updateCentrivoConnectorSettings
      summary: Actualiza la configuración del conector de Centrivo
      x-convray-runtime-status: active-connector-centrivo
      description: >-
        Habilita o deshabilita el conector y fija el intervalo de corrida (entre 30 y 1440
        minutos). Detrás de la puerta de producto 'data': responde 404 si el tenant no tiene
        ese producto.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: tenant
          in: query
          required: false
          description: Slug del tenant (override del tenant de la sesión).
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CentrivoPutSettingsRequest"
      responses:
        "200":
          description: Configuración actualizada
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CentrivoConnectorView"
        "400":
          description: El cuerpo no es JSON válido
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /data/connector/centrivo/run:
    post:
      tags:
        - Data
      operationId: runCentrivoConnector
      summary: Encola una corrida del conector de Centrivo
      x-convray-runtime-status: active-connector-centrivo
      description: >-
        Encola una corrida bajo demanda para el reporte indicado. Responde 202 al aceptar la
        solicitud. Si el conector está pausado responde 409 con el cuerpo {"error":"paused"}.
        Detrás de la puerta de producto 'data': responde 404 si el tenant no tiene ese producto.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: tenant
          in: query
          required: false
          description: Slug del tenant (override del tenant de la sesión).
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CentrivoRunRequest"
      responses:
        "202":
          description: Corrida encolada
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CentrivoRunQueuedResponse"
        "400":
          description: El cuerpo no es JSON válido
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "409":
          description: El conector está pausado
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CentrivoPausedError"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /data/connector/centrivo/resume:
    post:
      tags:
        - Data
      operationId: resumeCentrivoConnector
      summary: Reanuda el conector de Centrivo
      x-convray-runtime-status: active-connector-centrivo
      description: >-
        Reanuda el conector tras una pausa y devuelve el estado actualizado. Detrás de la puerta
        de producto 'data': responde 404 si el tenant no tiene ese producto.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: tenant
          in: query
          required: false
          description: Slug del tenant (override del tenant de la sesión).
          schema:
            type: string
      responses:
        "200":
          description: Estado del conector tras reanudar
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CentrivoConnectorView"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
  /data/support/tokens:
    get:
      tags:
        - Support
      operationId: listSupportTokens
      summary: Lista los tokens de servicio de soporte
      x-convray-runtime-status: active-support-v1
      description: >-
        Lista los tokens de servicio (cvs_) del tenant, sin el secreto. Administración de Data
        (JWT o token personal cvr_) detrás de la puerta de producto 'data': responde 404 si el
        tenant no tiene ese producto.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: tenant
          in: query
          required: false
          description: Slug del tenant (override del tenant de la sesión).
          schema:
            type: string
      responses:
        "200":
          description: Tokens de servicio del tenant
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SupportTokenListResponse"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
    post:
      tags:
        - Support
      operationId: createSupportToken
      summary: Crea un token de servicio de soporte
      x-convray-runtime-status: active-support-v1
      description: >-
        Crea un token de servicio (cvs_) con scope support.read. El secreto en claro se devuelve
        UNA sola vez en el campo token. Detrás de la puerta de producto 'data': responde 404 si
        el tenant no tiene ese producto.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: tenant
          in: query
          required: false
          description: Slug del tenant (override del tenant de la sesión).
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SupportCreateTokenRequest"
      responses:
        "201":
          description: Token creado (el secreto se muestra una sola vez)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SupportCreateTokenResponse"
        "400":
          description: El cuerpo no es JSON válido
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /data/support/tokens/{id}:
    delete:
      tags:
        - Support
      operationId: revokeSupportToken
      summary: Revoca un token de servicio de soporte
      x-convray-runtime-status: active-support-v1
      description: >-
        Revoca el token de servicio indicado. Detrás de la puerta de producto 'data': responde
        404 si el tenant no tiene ese producto o el token no existe.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: id
          in: path
          required: true
          description: Identificador del token de servicio.
          schema:
            type: string
            format: uuid
        - name: tenant
          in: query
          required: false
          description: Slug del tenant (override del tenant de la sesión).
          schema:
            type: string
      responses:
        "204":
          description: Token revocado
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
  /data/support/schedule:
    get:
      tags:
        - Support
      operationId: listSupportSchedule
      summary: Lista el calendario de cargas de soporte
      x-convray-runtime-status: active-support-v1
      description: >-
        Lista el calendario de cargas por dataset (horas locales, gracia y zona horaria). Detrás
        de la puerta de producto 'data': responde 404 si el tenant no tiene ese producto.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: tenant
          in: query
          required: false
          description: Slug del tenant (override del tenant de la sesión).
          schema:
            type: string
      responses:
        "200":
          description: Calendario de cargas del tenant
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SupportScheduleListResponse"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
  /data/support/schedule/{dataset}:
    put:
      tags:
        - Support
      operationId: updateSupportSchedule
      summary: Actualiza el calendario de cargas de un dataset
      x-convray-runtime-status: active-support-v1
      description: >-
        Crea o actualiza el calendario de cargas del dataset indicado. Detrás de la puerta de
        producto 'data': responde 404 si el tenant no tiene ese producto.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: dataset
          in: path
          required: true
          description: Código del dataset (por ejemplo players, deposits, withdrawals).
          schema:
            type: string
        - name: tenant
          in: query
          required: false
          description: Slug del tenant (override del tenant de la sesión).
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SupportPutScheduleRequest"
      responses:
        "200":
          description: Calendario actualizado
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SupportScheduleView"
        "400":
          description: El cuerpo no es JSON válido
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /data/support/integration:
    get:
      tags:
        - Support
      operationId: getSupportIntegration
      summary: Estado de encendido de la API de soporte
      x-convray-runtime-status: active-support-v1
      description: >-
        Dice si la API de soporte del tenant está encendida o en pausa, y quién y cuándo la cambió por
        última vez. Hay una sola pausa para toda la API de soporte, sea cual sea la integración de cada
        token. Mientras nadie la haya pausado ni reanudado responde enabled true con updated_at y
        updated_by en null. Detrás de la puerta de producto 'data', para owner o admin de Data.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: tenant
          in: query
          required: false
          description: Slug del tenant (override del tenant de la sesión).
          schema:
            type: string
      responses:
        "200":
          description: Estado de la API de soporte
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiIntegrationState"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
    put:
      tags:
        - Support
      operationId: updateSupportIntegration
      summary: Enciende o pausa la API de soporte
      x-convray-runtime-status: active-support-v1
      description: >-
        Enciende o pausa la API de soporte del tenant. Pausar NO revoca los tokens: mientras dura la
        pausa, /support/players/* responde 503 con code integration_paused a toda llamada con un token
        del tenant (auditada), y al encender los mismos tokens vuelven a funcionar. Pedir el estado que
        ya tiene no cambia nada (updated_at y updated_by quedan como estaban). Exige sesión web (JWT),
        de un owner o admin de Data.
      security:
        - adminBearer: []
      parameters:
        - name: tenant
          in: query
          required: false
          description: Slug del tenant (override del tenant de la sesión).
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ApiIntegrationStateInput"
      responses:
        "200":
          description: Estado guardado
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiIntegrationState"
        "400":
          description: El cuerpo no es JSON válido
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /data/support/audit:
    get:
      tags:
        - Support
      operationId: listSupportAudit
      summary: Lista los eventos de auditoría de soporte
      x-convray-runtime-status: active-support-v1
      description: >-
        Lista los eventos de auditoría de la API de soporte, con filtros por endpoint,
        conversación y rango de fechas. Detrás de la puerta de producto 'data': responde 404 si
        el tenant no tiene ese producto.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: tenant
          in: query
          required: false
          description: Slug del tenant (override del tenant de la sesión).
          schema:
            type: string
        - name: endpoint
          in: query
          required: false
          description: Filtra por endpoint auditado (account, verification, withdrawals, deposits).
          schema:
            type: string
        - name: conversation_id
          in: query
          required: false
          description: Filtra por identificador de conversación.
          schema:
            type: string
        - name: from
          in: query
          required: false
          description: Límite inferior del rango (ISO-8601 date-time o fecha AAAA-MM-DD).
          schema:
            type: string
        - name: to
          in: query
          required: false
          description: Límite superior del rango (ISO-8601 date-time o fecha AAAA-MM-DD).
          schema:
            type: string
        - name: limit
          in: query
          required: false
          description: Tope de filas a devolver.
          schema:
            type: integer
      responses:
        "200":
          description: Eventos de auditoría
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SupportAuditListResponse"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /support/players/{player_id}/account:
    get:
      tags:
        - Support
      operationId: getSupportPlayerAccount
      summary: Estado de cuenta del jugador (API de soporte)
      x-convray-runtime-status: active-support-v1
      description: >-
        Devuelve el estado de cuenta normalizado del jugador, resuelto por el Player ID de
        Juégalo (casino_player_ref). Solo lectura, con token de servicio cvs_ (scope
        support.read). Exige la cabecera X-Conversation-Id.
      security:
        - supportServiceToken: []
      parameters:
        - name: player_id
          in: path
          required: true
          description: Player ID de Juégalo (casino_player_ref).
          schema:
            type: string
        - $ref: "#/components/parameters/SupportConversationId"
      responses:
        "200":
          description: Estado de cuenta normalizado
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SupportAccountResponse"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/IntegrationPaused"
  /support/players/{player_id}/verification:
    get:
      tags:
        - Support
      operationId: getSupportPlayerVerification
      summary: Estado de verificación (KYC) del jugador (API de soporte)
      x-convray-runtime-status: active-support-v1
      description: >-
        Devuelve el estado de verificación KYC derivado y los booleanos por control del jugador,
        resuelto por el Player ID de Juégalo (casino_player_ref). Solo lectura, con token de
        servicio cvs_. Exige la cabecera X-Conversation-Id.
      security:
        - supportServiceToken: []
      parameters:
        - name: player_id
          in: path
          required: true
          description: Player ID de Juégalo (casino_player_ref).
          schema:
            type: string
        - $ref: "#/components/parameters/SupportConversationId"
      responses:
        "200":
          description: Estado de verificación KYC
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SupportVerificationResponse"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/IntegrationPaused"
  /support/players/{player_id}/withdrawals:
    get:
      tags:
        - Support
      operationId: getSupportPlayerWithdrawals
      summary: Últimos retiros del jugador (API de soporte)
      x-convray-runtime-status: active-support-v1
      description: >-
        Devuelve los últimos retiros del jugador (ventana de 30 días, tope 10) con estados
        normalizados y el tiempo típico de pago, resuelto por el Player ID de Juégalo. Solo
        lectura, con token de servicio cvs_. Exige la cabecera X-Conversation-Id.
      security:
        - supportServiceToken: []
      parameters:
        - name: player_id
          in: path
          required: true
          description: Player ID de Juégalo (casino_player_ref).
          schema:
            type: string
        - $ref: "#/components/parameters/SupportConversationId"
        - name: limit
          in: query
          required: false
          description: Tope de filas (máximo 10; por defecto 5).
          schema:
            type: integer
      responses:
        "200":
          description: Últimos retiros del jugador
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SupportWithdrawalsResponse"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/IntegrationPaused"
  /support/players/{player_id}/deposits:
    get:
      tags:
        - Support
      operationId: getSupportPlayerDeposits
      summary: Últimos depósitos del jugador (API de soporte)
      x-convray-runtime-status: active-support-v1
      description: >-
        Devuelve los últimos depósitos del jugador (ventana de 14 días, tope 10) con estados
        normalizados y el tipo de método, resuelto por el Player ID de Juégalo. Solo lectura, con
        token de servicio cvs_. Exige la cabecera X-Conversation-Id.
      security:
        - supportServiceToken: []
      parameters:
        - name: player_id
          in: path
          required: true
          description: Player ID de Juégalo (casino_player_ref).
          schema:
            type: string
        - $ref: "#/components/parameters/SupportConversationId"
        - name: limit
          in: query
          required: false
          description: Tope de filas (máximo 10; por defecto 5).
          schema:
            type: integer
      responses:
        "200":
          description: Últimos depósitos del jugador
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SupportDepositsResponse"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/IntegrationPaused"
  /retention/promo/redemptions:
    get:
      tags:
        - Retention
      operationId: getRetentionPromoRedemptions
      summary: Feed por cursor de códigos canjeados de los packs del token (API de retención)
      x-convray-runtime-status: active-retention-v1
      description: >-
        Entrega cada canje de promo code (una fila por redemption_id, aunque el canje haya entregado varios
        bonos) de los packs cuyo nombre actual contiene el patrón (coincidencia literal, sin distinguir
        mayúsculas), en el orden en que Convray lo vio por primera vez (observed_at, que nunca cambia) y con
        cursor opaco (AES-256-GCM; el tenant, el endpoint y el patrón normalizado van como datos asociados,
        así que un cursor de otro endpoint o de otro patrón responde 400 invalid_cursor). El patrón lo fija el
        servidor desde promo_pack_patterns del token: sin pack_pattern se usa el único patrón del token; un
        patrón que el token no tiene (o un token sin patrones) responde 403 forbidden y uno con forma inválida,
        vacío o de solo espacios (o ausente con un token de varios patrones) responde 400 invalid_parameter,
        todos auditados. Los packs single y unknown salen con code null: su código sigue sirviendo para otros
        jugadores. Solo entrega canjes observados antes de now() - settle_seconds. Al menos una vez: el
        consumidor deduplica por redemption_id y por code. El feed no vuelve a emitir un canje cuando cambia
        el estado de su bono (la reconciliación lo relee). promo_data_through y promo_backlog_packs se
        calculan solo con los packs vigentes del patrón y nunca afirman una completitud que el feed todavía no
        entregó. Sin PII: player_ref es el external_player_ref de Centrivo; nunca el usuario ni el correo del
        canje. Parámetros desconocidos, repetidos, tenant o token en la query responden 400 invalid_parameter.
      security:
        - retentionServiceToken: []
      parameters:
        - name: cursor
          in: query
          required: false
          description: El next_cursor de la respuesta anterior. No va junto con since.
          schema:
            type: string
        - name: since
          in: query
          required: false
          description: RFC 3339 con zona; solo sin cursor y como máximo 45 días atrás. Empieza en observed_at >= since.
          schema:
            type: string
            format: date-time
        - name: limit
          in: query
          required: false
          description: Filas por página (1 a 1000; por defecto 500).
          schema:
            type: integer
            minimum: 1
            maximum: 1000
            default: 500
        - name: pack_pattern
          in: query
          required: false
          description: >-
            Patrón de nombre de pack. Se normaliza (espacios U+0020 al principio y al final fuera, minúsculas)
            y debe quedar de 3 a 40 letras, dígitos o espacios, con al menos 3 letras o dígitos; después tiene
            que estar en promo_pack_patterns del token (si no, 403). Por defecto, el único patrón del token;
            con un token de varios patrones es obligatorio. Entra, normalizado, en la AAD del cursor.
          schema:
            type: string
            minLength: 1
            maxLength: 60
          example: halloween
      responses:
        "200":
          description: Página del feed de canjes, con la frescura del patrón
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RetentionPromoRedemptionsResponse"
              example:
                items:
                  - redemption_id: "8800001"
                    code: HWN10-7K2QZ9
                    pack_id: "612"
                    pack_name: Halloween Nivel 10
                    pack_type: multiple
                    player_ref: "1000001"
                    casino_player_ref: "90000001"
                    redeemed_at: "2026-10-05T22:14:03Z"
                    observed_at: "2026-10-05T22:31:18.004512Z"
                    bonus_found: true
                    bonus_statuses:
                      - active
                    staff: false
                    test_player: false
                next_cursor: v1.k2.Hc81mPz0sQwE4nVtRk7w
                has_more: false
                promo_data_through: "2026-10-05T22:30:00Z"
                promo_backlog_packs: 0
                settle_seconds: 300
        "400":
          $ref: "#/components/responses/RetentionProblem"
        "401":
          $ref: "#/components/responses/RetentionProblem"
        "403":
          $ref: "#/components/responses/RetentionProblem"
        "405":
          $ref: "#/components/responses/RetentionProblem"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/RetentionProblem"
        "503":
          $ref: "#/components/responses/RetentionUnavailable"
  /retention/promo/packs:
    get:
      tags:
        - Retention
      operationId: getRetentionPromoPacks
      summary: Packs de códigos de premio de la campaña del token, con su estado de sincronización (API de retención)
      x-convray-runtime-status: active-retention-v1
      description: >-
        Solo para un token de scope retention.codes (integración juego-codigos): un token de scope
        retention.read (juego-octubre, qa-retencion) responde 403 forbidden auditado. Lista los packs Multiple
        y External de Centrivo cuyo nombre actual contiene el patrón (coincidencia literal, sin distinguir
        mayúsculas), en orden de pack_id, con cuántos códigos declara Centrivo (codes_count), cuántos guardó
        Convray (codes_synced) y si el pack está completo: la última lectura de su lista trajo exactamente
        codes_count códigos distintos con el codes_count actual. Nunca entrega códigos. El patrón lo fija el
        servidor desde promo_pack_patterns del token, con las mismas reglas que promo/redemptions (403 si el
        token no lo tiene, 400 si su forma es inválida). Los packs Single (un código compartido) no aparecen.
        Parámetros desconocidos, repetidos, tenant o token en la query responden 400 invalid_parameter.
      security:
        - retentionServiceToken: []
      parameters:
        - name: pack_pattern
          in: query
          required: false
          description: >-
            Patrón de nombre de pack. Se normaliza (espacios U+0020 al borde fuera, minúsculas) y debe estar en
            promo_pack_patterns del token (si no, 403). Por defecto, el único patrón del token; con un token de
            varios patrones es obligatorio.
          schema:
            type: string
            minLength: 1
            maxLength: 60
          example: halloween
      responses:
        "200":
          description: Packs de la campaña con su estado de sincronización
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RetentionPromoPacksResponse"
              example:
                items:
                  - pack_id: "612"
                    pack_name: Halloween Nivel 5
                    pack_type: multiple
                    codes_count: 10000
                    codes_synced: 10000
                    complete: true
                    used_count: 37
                    first_seen_at: "2026-10-01T12:40:00Z"
                    codes_synced_at: "2026-10-01T13:10:04Z"
        "400":
          $ref: "#/components/responses/RetentionProblem"
        "401":
          $ref: "#/components/responses/RetentionProblem"
        "403":
          $ref: "#/components/responses/RetentionProblem"
        "405":
          $ref: "#/components/responses/RetentionProblem"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/RetentionProblem"
        "503":
          $ref: "#/components/responses/RetentionUnavailable"
  /retention/promo/codes:
    get:
      tags:
        - Retention
      operationId: getRetentionPromoCodes
      summary: Feed por cursor de TODOS los códigos de premio de los packs de la campaña del token (API de retención)
      x-convray-runtime-status: active-retention-v1
      description: >-
        Solo para un token de scope retention.codes (integración juego-codigos): un token de scope
        retention.read responde 403 forbidden auditado, y un token juego-codigos responde 403 en deposits/*,
        players/* (email-check incluido) y promo/redemptions. Entrega cada código de los packs Multiple y
        External cuyo nombre contiene el patrón (o solo del pack pack_id, si llega y es de ese patrón; si no,
        la página viene vacía), con si ya se canjeó, quién lo canjeó (player_ref de Centrivo) y cuándo, en el
        orden en que Convray vio cada código por primera vez (first_seen_at; Centrivo no da fecha por código)
        y con cursor opaco (AES-256-GCM; el tenant, el endpoint, el patrón normalizado y pack_id van como
        datos asociados, así que un cursor de otro patrón, otro pack u otro endpoint responde 400
        invalid_cursor). Cada código sale una sola vez al recorrer el cursor, con cualquier limit. Solo
        entrega códigos vistos antes de now() - settle_seconds. Los códigos se guardan cifrados en Convray y
        nunca van a la auditoría ni a los logs. Tope diario de filas por token (50.000 por defecto, día UTC):
        la página se acota a las filas que quedan del día y, agotado el tope, responde 429 rate_limited con
        Retry-After hasta las 00:00Z, auditado. Sin la clave de PII configurada responde 503.
      security:
        - retentionServiceToken: []
      parameters:
        - name: cursor
          in: query
          required: false
          description: El next_cursor de la respuesta anterior (con los mismos pack_pattern y pack_id).
          schema:
            type: string
        - name: limit
          in: query
          required: false
          description: Filas por página (1 a 1000; por defecto 500), acotadas además por el tope diario del token.
          schema:
            type: integer
            minimum: 1
            maximum: 1000
            default: 500
        - name: pack_pattern
          in: query
          required: false
          description: >-
            Patrón de nombre de pack, con las reglas de promo/packs. Entra, normalizado, en la AAD del cursor.
          schema:
            type: string
            minLength: 1
            maxLength: 60
          example: halloween
        - name: pack_id
          in: query
          required: false
          description: Id numérico de un pack del patrón, para leer solo ese pack. Entra en la AAD del cursor.
          schema:
            type: string
            pattern: "^[0-9]{1,18}$"
          example: "612"
      responses:
        "200":
          description: Página del feed de códigos
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RetentionPromoCodesResponse"
              example:
                items:
                  - code: FKTEST01A
                    pack_id: "612"
                    pack_name: Halloween Nivel 5
                    first_seen_at: "2026-10-01T13:10:04.281377Z"
                    used: true
                    used_by_player_ref: "1000001"
                    used_at: "2026-10-05T22:14:03Z"
                  - code: FKTEST02B
                    pack_id: "612"
                    pack_name: Halloween Nivel 5
                    first_seen_at: "2026-10-01T13:10:04.281377Z"
                    used: false
                    used_by_player_ref: null
                    used_at: null
                next_cursor: v1.k2.Hc81mPz0sQwE4nVtRk7w
                has_more: true
                settle_seconds: 300
        "400":
          $ref: "#/components/responses/RetentionProblem"
        "401":
          $ref: "#/components/responses/RetentionProblem"
        "403":
          $ref: "#/components/responses/RetentionProblem"
        "405":
          $ref: "#/components/responses/RetentionProblem"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/RetentionProblem"
        "503":
          $ref: "#/components/responses/RetentionUnavailable"
  /retention/health:
    get:
      tags:
        - Retention
      operationId: getRetentionHealth
      summary: Frescura de los datos del conector (API de retención)
      x-convray-runtime-status: active-retention-v1
      description: >-
        Devuelve hasta dónde están completos los depósitos (data_through) y los canjes de promo codes
        (promo_data_through) del tenant del token, si el conector de Centrivo está pausado (con un
        código grueso de motivo) y cuántos packs vigentes tienen usos pendientes de releer. Solo
        servidor a servidor, con token de máquina cvj_ (scope retention.read); sin CORS, sin
        productgate (responde en cualquier host) y con Cache-Control no-store. No acepta parámetros.
      security:
        - retentionServiceToken: []
      responses:
        "200":
          description: Frescura del conector
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RetentionHealthResponse"
        "400":
          $ref: "#/components/responses/RetentionProblem"
        "401":
          $ref: "#/components/responses/RetentionProblem"
        "403":
          $ref: "#/components/responses/RetentionProblem"
        "404":
          $ref: "#/components/responses/RetentionProblem"
        "405":
          $ref: "#/components/responses/RetentionProblem"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/RetentionProblem"
        "503":
          $ref: "#/components/responses/RetentionUnavailable"
  /retention/deposits/paid:
    get:
      tags:
        - Retention
      operationId: getRetentionPaidDeposits
      summary: Feed por cursor de depósitos pagados (API de retención)
      x-convray-runtime-status: active-retention-v1
      description: >-
        Entrega cada depósito del tenant del token cuando Convray ve su paso a pagado, en orden estable
        y con cursor opaco (AES-256-GCM; el tenant, el endpoint y los filtros van como datos asociados,
        así que un cursor usado con otros filtros responde 400 invalid_cursor). Solo entrega filas con
        posición anterior a now() - settle_seconds. Al menos una vez: el consumidor deduplica por
        deposit_ref. Sin PII: player_ref (external_player_ref de Centrivo) junto con montos es dato
        personal seudónimo. Nunca entrega un depósito creado antes del piso deposits_from del token, ni
        siquiera con since o con un created_from menor. Parámetros desconocidos, repetidos, tenant o token
        en la query responden 400 invalid_parameter.
      security:
        - retentionServiceToken: []
      parameters:
        - name: cursor
          in: query
          required: false
          description: El next_cursor de la respuesta anterior. No va junto con since.
          schema:
            type: string
        - name: since
          in: query
          required: false
          description: RFC 3339 con zona; solo sin cursor y como máximo 45 días atrás.
          schema:
            type: string
            format: date-time
        - name: limit
          in: query
          required: false
          description: Filas por página (1 a 1000; por defecto 500).
          schema:
            type: integer
            minimum: 1
            maximum: 1000
            default: 500
        - name: created_from
          in: query
          required: false
          description: >-
            Filtra created_at >= created_from (el juego manda 2026-10-01T03:00:00Z). El valor efectivo es
            max(created_from, deposits_from del token): uno menor, o ninguno, se ajusta en silencio al piso.
            El efectivo entra en la AAD del cursor y fija la cota de partida de la primera página
            (created_from − 24 h).
          schema:
            type: string
            format: date-time
        - name: min_amount_clp
          in: query
          required: false
          description: Solo depósitos en CLP de ese monto o más (1 a 100.000.000). Entra en la AAD del cursor.
          schema:
            type: integer
            minimum: 1
            maximum: 100000000
      responses:
        "200":
          description: Página del feed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RetentionPaidDepositsResponse"
        "400":
          $ref: "#/components/responses/RetentionProblem"
        "401":
          $ref: "#/components/responses/RetentionProblem"
        "403":
          $ref: "#/components/responses/RetentionProblem"
        "405":
          $ref: "#/components/responses/RetentionProblem"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/RetentionProblem"
        "503":
          $ref: "#/components/responses/RetentionUnavailable"
  /retention/deposits/reversed:
    get:
      tags:
        - Retention
      operationId: getRetentionReversedDeposits
      summary: Feed por cursor de reversos de depósitos (API de retención)
      x-convray-runtime-status: active-retention-v1
      description: >-
        Entrega cada depósito del tenant del token cuando Convray ve que salió de pagado (paid_reversed_at),
        en orden (paid_reversed_at, id), con el mismo cursor opaco, horizonte y filtros que deposits/paid; since
        compara con paid_reversed_at. Cada fila trae el status actual sin normalizar: un depósito que se
        revirtió y volvió a pagado sale con status paid (el consumidor cierra su alerta). Nunca entrega un
        depósito creado antes del piso deposits_from del token. Un cursor de deposits/paid no sirve acá (ni
        al revés): responde 400 invalid_cursor. Al menos una vez: el consumidor deduplica por deposit_ref y
        paid_reversed_at.
      security:
        - retentionServiceToken: []
      parameters:
        - name: cursor
          in: query
          required: false
          description: El next_cursor de la respuesta anterior de este mismo feed. No va junto con since.
          schema:
            type: string
        - name: since
          in: query
          required: false
          description: RFC 3339 con zona; solo sin cursor y como máximo 45 días atrás. Compara con paid_reversed_at.
          schema:
            type: string
            format: date-time
        - name: limit
          in: query
          required: false
          description: Filas por página (1 a 1000; por defecto 500).
          schema:
            type: integer
            minimum: 1
            maximum: 1000
            default: 500
        - name: created_from
          in: query
          required: false
          description: >-
            Filtra created_at >= created_from. El valor efectivo es max(created_from, deposits_from del
            token), ajustado en silencio; entra en la AAD del cursor.
          schema:
            type: string
            format: date-time
        - name: min_amount_clp
          in: query
          required: false
          description: Solo depósitos en CLP de ese monto o más (1 a 100.000.000). Entra en la AAD del cursor.
          schema:
            type: integer
            minimum: 1
            maximum: 100000000
      responses:
        "200":
          description: Página del feed de reversos
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RetentionReversedDepositsResponse"
              example:
                items:
                  - deposit_ref: "500000003"
                    player_ref: "1000001"
                    casino_player_ref: "90000001"
                    amount_clp: 10000
                    currency: CLP
                    created_at: "2026-10-04T13:40:00Z"
                    paid_observed_at: "2026-10-04T13:47:10.500000Z"
                    eligible: true
                    staff: false
                    test_player: false
                    status: failed
                    paid_reversed_at: "2026-10-04T18:02:31.200000Z"
                next_cursor: v1.k2.q3Vb0tQm9rX2hN7yLw
                has_more: false
                data_through: "2026-10-04T18:00:00Z"
                settle_seconds: 300
        "400":
          $ref: "#/components/responses/RetentionProblem"
        "401":
          $ref: "#/components/responses/RetentionProblem"
        "403":
          $ref: "#/components/responses/RetentionProblem"
        "405":
          $ref: "#/components/responses/RetentionProblem"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/RetentionProblem"
        "503":
          $ref: "#/components/responses/RetentionUnavailable"
  /retention/players/flags:
    get:
      tags:
        - Retention
      operationId: getRetentionPlayerFlags
      summary: Marcas actuales de hasta 100 jugadores (API de retención)
      x-convray-runtime-status: active-retention-v1
      description: >-
        Devuelve eligible, staff y test_player (misma regla que deposits/paid, al momento de la solicitud) y el
        casino_player_ref de hasta 100 jugadores distintos por llamada. Solo responde por jugadores con al menos
        un depósito pagado del tenant creado desde el piso deposits_from del token (el inicio de la campaña);
        los demás refs (sin depósitos de la campaña, inexistentes o de otro tenant) solo se cuentan en unknown y
        no se nombran. items sale en el orden de entrada, deduplicado. Un ref inválido, refs vacío o más de 100
        refs distintos responden 400 para toda la llamada. Si un ref supera 20 por minuto o la llamada supera el
        tope diario de refs (50.000 por token), responde 429 para toda la llamada, con Retry-After, sin consumir
        cupo. La auditoría registra rows_returned sin la lista de refs.
      security:
        - retentionServiceToken: []
      parameters:
        - name: refs
          in: query
          required: true
          description: player_ref (external_player_ref) separados por coma, cada uno de 1 a 40 caracteres [A-Za-z0-9_-]; hasta 100 distintos (los repetidos se cuentan una vez).
          schema:
            type: string
          example: "1000001,1000002,1000003"
      responses:
        "200":
          description: Marcas de los jugadores con depósitos de la campaña, en el orden de entrada
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RetentionPlayerFlagsResponse"
              example:
                items:
                  - player_ref: "1000001"
                    casino_player_ref: "90000001"
                    eligible: true
                    staff: false
                    test_player: false
                  - player_ref: "1000003"
                    casino_player_ref: null
                    eligible: null
                    staff: false
                    test_player: false
                unknown: 1
        "400":
          $ref: "#/components/responses/RetentionProblem"
        "401":
          $ref: "#/components/responses/RetentionProblem"
        "403":
          $ref: "#/components/responses/RetentionProblem"
        "405":
          $ref: "#/components/responses/RetentionProblem"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/RetentionProblem"
        "503":
          $ref: "#/components/responses/RetentionUnavailable"
  /retention/players/{ref}/deposits:
    get:
      tags:
        - Retention
      operationId: getRetentionPlayerDeposits
      summary: Depósitos pagados de un jugador, para el SAC (API de retención)
      x-convray-runtime-status: active-retention-v1
      description: >-
        Lista los depósitos de un jugador del tenant del token que llegaron a pagado (paid_observed_at no
        nulo), incluidos los que después se revirtieron, que salen con su status actual y paid_reversed_at;
        nunca pendientes ni fallidos que no se pagaron. Solo depósitos con created_at en [from efectivo, to),
        donde from efectivo es max(from, deposits_from del token), ajustado en silencio; más recientes
        primero, con 500 como máximo (truncated). Un jugador que existe (está en la foto de jugadores o tiene
        algún depósito) sin depósitos pagados en la ventana responde 200 con deposits vacío y sus marcas; el
        404 es solo para un jugador inexistente o de otro tenant, con el mismo cuerpo en los dos casos. Tope
        de 500 consultas por token y día y 20 por minuto por jugador.
      security:
        - retentionServiceToken: []
      parameters:
        - name: ref
          in: path
          required: true
          description: player_ref (external_player_ref) o, con ref_kind=casino, casino_player_ref; de 1 a 40 caracteres [A-Za-z0-9_-]. Con otra forma responde 404.
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,40}$"
            minLength: 1
            maxLength: 40
        - name: ref_kind
          in: query
          required: false
          description: player (por defecto) busca por external_player_ref; casino, por casino_player_ref (si apunta a más de un jugador, 409 ambiguous_ref).
          schema:
            type: string
            enum:
              - player
              - casino
            default: player
        - name: from
          in: query
          required: false
          description: >-
            RFC 3339 con zona; por defecto to − 8 días. El valor efectivo es max(from, deposits_from del
            token) y es el from de la respuesta. to − from ≤ 31 días y from < to se validan sobre lo pedido;
            si el from efectivo queda >= to, la respuesta es 200 con deposits vacío.
          schema:
            type: string
            format: date-time
        - name: to
          in: query
          required: false
          description: RFC 3339 con zona; por defecto ahora. Excluyente.
          schema:
            type: string
            format: date-time
      responses:
        "200":
          description: Jugador con sus marcas y sus depósitos pagados en la ventana
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RetentionPlayerDepositsResponse"
              example:
                player_ref: "1000001"
                casino_player_ref: "90000001"
                eligible: true
                staff: false
                test_player: false
                from: "2026-10-01T03:00:00Z"
                to: "2026-10-05T14:00:00Z"
                data_through: "2026-10-05T14:00:00Z"
                truncated: false
                deposits:
                  - deposit_ref: "500000003"
                    amount_clp: 10000
                    currency: CLP
                    status: failed
                    created_at: "2026-10-04T13:40:00Z"
                    paid_observed_at: "2026-10-04T13:47:10.500000Z"
                    paid_reversed_at: "2026-10-04T18:02:31.200000Z"
                  - deposit_ref: "500000001"
                    amount_clp: 20000
                    currency: CLP
                    status: paid
                    created_at: "2026-10-03T13:58:12Z"
                    paid_observed_at: "2026-10-03T14:05:40.123456Z"
                    paid_reversed_at: null
        "400":
          $ref: "#/components/responses/RetentionProblem"
        "401":
          $ref: "#/components/responses/RetentionProblem"
        "403":
          $ref: "#/components/responses/RetentionProblem"
        "404":
          $ref: "#/components/responses/RetentionProblem"
        "405":
          $ref: "#/components/responses/RetentionProblem"
        "409":
          $ref: "#/components/responses/RetentionProblem"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/RetentionProblem"
        "503":
          $ref: "#/components/responses/RetentionUnavailable"
  /retention/players/{ref}/email-check:
    post:
      tags:
        - Retention
      operationId: checkRetentionPlayerEmail
      summary: Verificación del correo de la cuenta de un jugador, sin PII (API de retención)
      x-convray-runtime-status: active-retention-v1
      description: >-
        Responde si el SHA-256 del correo que escribió el jugador coincide con el correo de su cuenta en
        Centrivo, sin entregar nunca el correo. El cuerpo lleva exactamente la clave email_sha256 =
        hex(sha256(utf8(lower(trim(correo))))), en minúsculas. Convray descifra la PII del jugador en
        memoria, normaliza el correo de la misma forma y compara en tiempo constante. Un jugador
        inexistente, de otro tenant o sin correo responde {"match": false}, igual que un correo distinto.
        Si la PII no se puede descifrar (clave ausente o dato corrupto) responde 503, nunca false. Es la
        única operación POST de la superficie; cualquier otro método (GET incluido) responde 405. Primero
        se autentica: sin Authorization es 401 aunque el cuerpo sea válido. Topes propios: 10 por jugador
        y día UTC y 5.000 por día por token, más 20 por minuto por jugador y el límite por minuto del
        token; cuentan todas las respuestas con token resuelto, incluidos los 400 y match false. La
        auditoría registra endpoint player_email_check con rows_returned 1 si coincidió y 0 si no; ni el
        hash ni el correo se guardan ni se registran en logs.
      security:
        - retentionServiceToken: []
      parameters:
        - name: ref
          in: path
          required: true
          description: >-
            player_ref del jugador (external_player_ref de Centrivo), de 1 a 40 caracteres
            [A-Za-z0-9_-]; con otra forma responde 404. No acepta parámetros de query (ni ref_kind): uno
            cualquiera responde 400 invalid_parameter.
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,40}$"
            minLength: 1
            maxLength: 40
          example: "1000001"
      requestBody:
        required: true
        description: application/json de 1 KB como máximo. Content-Type distinto, cuerpo que no es JSON, claves de más o de menos, o un valor que no son 64 caracteres [0-9a-f] responden 400 invalid_parameter.
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RetentionEmailCheckRequest"
            example:
              email_sha256: 79f4dfdddae87cb347d4743877793c9bc29908c2c3e7bb70b947d153b4dabb67
      responses:
        "200":
          description: Resultado de la verificación; siempre exactamente la clave match.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RetentionEmailCheckResponse"
              examples:
                coincide:
                  summary: El hash es el del correo de la cuenta
                  value:
                    match: true
                noCoincide:
                  summary: Otro correo, jugador inexistente, de otro tenant o sin correo
                  value:
                    match: false
        "400":
          $ref: "#/components/responses/RetentionProblem"
        "401":
          $ref: "#/components/responses/RetentionProblem"
        "403":
          $ref: "#/components/responses/RetentionProblem"
        "404":
          $ref: "#/components/responses/RetentionProblem"
        "405":
          $ref: "#/components/responses/RetentionProblem"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/RetentionProblem"
        "503":
          $ref: "#/components/responses/RetentionUnavailable"
  /data/retention/tokens:
    get:
      tags:
        - Retention
      operationId: listRetentionTokens
      summary: Lista los tokens de la API de retención
      x-convray-runtime-status: active-retention-v1
      description: >-
        Lista los tokens cvj_ del tenant, sin el secreto. Administración de Data detrás de la puerta
        de producto 'data' (solo por el host data.): responde 404 por otro host.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: tenant
          in: query
          required: false
          description: Slug del tenant (override del tenant de la sesión).
          schema:
            type: string
      responses:
        "200":
          description: Tokens del tenant
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RetentionTokenListResponse"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
    post:
      tags:
        - Retention
      operationId: createRetentionToken
      summary: Emite un token de la API de retención
      x-convray-runtime-status: active-retention-v1
      description: >-
        Emite un token de máquina cvj_ (scope retention.read) atado al tenant, a una integración
        (juego-octubre o qa-retencion) y a una lista cerrada de patrones de nombre de pack. El secreto en
        claro se devuelve UNA sola vez. Vence a los 90 días como máximo; un token de juego-octubre sin
        lista de IP vence a los 45 días como máximo (más responde 400). La lista de IP no admite rangos
        más anchos que /24 en IPv4 o /48 en IPv6 (responde 400), así que una lista ancha no evita esa
        regla. Exige deposits_from, el piso de fecha de los depósitos del token, fijo desde la emisión. Hasta
        2 tokens vivos por integración (rotación sin corte; un tercero responde 409). Exige sesión web
        (JWT).
      security:
        - adminBearer: []
      parameters:
        - name: tenant
          in: query
          required: false
          description: Slug del tenant (override del tenant de la sesión).
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RetentionTokenCreateInput"
      responses:
        "201":
          description: Token emitido (el secreto va solo en esta respuesta)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RetentionTokenCreated"
        "400":
          $ref: "#/components/responses/RetentionProblem"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "409":
          $ref: "#/components/responses/RetentionProblem"
  /data/retention/tokens/{id}:
    delete:
      tags:
        - Retention
      operationId: revokeRetentionToken
      summary: Revoca un token de la API de retención
      x-convray-runtime-status: active-retention-v1
      description: >-
        Revoca un token cvj_ del tenant. El siguiente request con ese token responde 401. Exige sesión
        web (JWT).
      security:
        - adminBearer: []
      parameters:
        - name: id
          in: path
          required: true
          description: Identificador del token.
          schema:
            type: string
            format: uuid
        - name: tenant
          in: query
          required: false
          description: Slug del tenant (override del tenant de la sesión).
          schema:
            type: string
      responses:
        "204":
          description: Token revocado
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
  /data/retention/audit:
    get:
      tags:
        - Retention
      operationId: listRetentionAudit
      summary: Lista la auditoría de la API de retención
      x-convray-runtime-status: active-retention-v1
      description: >-
        Lista una fila por solicitud con token resuelto (los 401 y los 404/405 del router no se
        auditan). Nunca incluye cuerpos, montos, códigos de promo, hashes, la cabecera Authorization ni
        el cursor. Detrás de la puerta de producto 'data' (solo por el host data.).
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: tenant
          in: query
          required: false
          description: Slug del tenant (override del tenant de la sesión).
          schema:
            type: string
        - name: endpoint
          in: query
          required: false
          description: >-
            Filtra por endpoint auditado (health, deposits_paid, player_email_check, etc.). Un nombre
            con otra forma responde 400; uno desconocido no trae filas.
          schema:
            type: string
            pattern: "^[a-z][a-z_]{1,63}$"
        - name: token
          in: query
          required: false
          description: Filtra por token (id del token, uuid). En el servidor, no sobre las filas cargadas.
          schema:
            type: string
            format: uuid
        - name: result
          in: query
          required: false
          description: Filtra por resultado. ok = status menor que 400; error = status 400 o mayor.
          schema:
            type: string
            enum:
              - ok
              - error
        - name: from
          in: query
          required: false
          description: >-
            Límite inferior inclusivo. RFC 3339, o AAAA-MM-DD = las 00:00 de ese día en hora de Chile
            (America/Santiago).
          schema:
            type: string
        - name: to
          in: query
          required: false
          description: >-
            Límite superior EXCLUSIVO. RFC 3339, o AAAA-MM-DD = las 00:00 de ese día en hora de Chile
            (para incluir el 29 completo se pide to=30). Debe ser posterior a from.
          schema:
            type: string
        - name: limit
          in: query
          required: false
          description: Tope de filas (1 a 200; por defecto 100).
          schema:
            type: integer
            minimum: 1
            maximum: 200
        - name: offset
          in: query
          required: false
          description: >-
            Filas a saltar, de la más reciente a la más antigua, para paginar junto con limit (la
            página n de 20 filas es limit=20 y offset=(n-1)*20). De 0 a 100000; por defecto 0. El
            total de cada pestaña viene en totals.
          schema:
            type: integer
            minimum: 0
            maximum: 100000
      responses:
        "200":
          description: Eventos de auditoría y conteos por resultado
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RetentionAuditListResponse"
        "400":
          $ref: "#/components/responses/RetentionProblem"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
  /data/retention/integrations:
    get:
      tags:
        - Retention
      operationId: listRetentionIntegrations
      summary: Estado de encendido de las integraciones de retención
      x-convray-runtime-status: active-retention-v1
      description: >-
        Devuelve, siempre en el mismo orden, el estado de las tres integraciones de la API de retención
        (juego-octubre, juego-codigos y qa-retencion): si está encendida o en pausa, y quién y cuándo la
        cambió por última vez. Una integración que nadie pausó ni reanudó responde enabled true con
        updated_at y updated_by en null. Detrás de la puerta de producto 'data' (solo por el host
        data.), para owner o admin de Data.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: tenant
          in: query
          required: false
          description: Slug del tenant (override del tenant de la sesión).
          schema:
            type: string
      responses:
        "200":
          description: Estado de las integraciones
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RetentionIntegrationListResponse"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
  /data/retention/integrations/{integration}:
    put:
      tags:
        - Retention
      operationId: updateRetentionIntegration
      summary: Enciende o pausa una integración de retención
      x-convray-runtime-status: active-retention-v1
      description: >-
        Enciende o pausa una integración de la API de retención. Pausar NO revoca los tokens: mientras
        dura la pausa, /retention/* responde 503 con code integration_paused a toda llamada con un token
        de esa integración (auditada y sin consumir cupo), las otras integraciones siguen respondiendo y,
        al encender, los mismos tokens vuelven a funcionar. Pedir el estado que ya tiene no cambia nada
        (updated_at y updated_by quedan como estaban). Una integración desconocida o un cuerpo sin
        enabled responden 400. Exige sesión web (JWT), de un owner o admin de Data.
      security:
        - adminBearer: []
      parameters:
        - name: integration
          in: path
          required: true
          description: Integración a encender o pausar.
          schema:
            type: string
            enum:
              - juego-octubre
              - juego-codigos
              - qa-retencion
        - name: tenant
          in: query
          required: false
          description: Slug del tenant (override del tenant de la sesión).
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ApiIntegrationStateInput"
      responses:
        "200":
          description: Estado guardado
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiIntegrationState"
        "400":
          $ref: "#/components/responses/RetentionProblem"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
  /data/retention/summary:
    get:
      tags:
        - Retention
      operationId: getRetentionSummary
      summary: Resumen de hoy de la API de retención
      x-convray-runtime-status: active-retention-v1
      description: >-
        Indicadores de la pantalla de Data: tokens vivos por integración, llamadas de hoy (día civil
        de Chile) y de ayer hasta la misma hora, llamadas con error (status 400 o mayor) y cuántas
        fueron del servidor (5xx), p95 del tiempo de respuesta de hoy, hasta cuándo están al día los
        depósitos (misma regla que data_through de /retention/health) y, por token, las llamadas de
        cada una de las últimas 24 horas y su último uso. Lee la auditoría y los tokens del tenant; no
        expone IP, jugadores ni secretos. Detrás de la puerta de producto 'data' (solo por el host
        data.), para owner o admin de Data.
      security:
        - adminBearer: []
        - dataApiToken: []
      parameters:
        - name: tenant
          in: query
          required: false
          description: Slug del tenant (override del tenant de la sesión).
          schema:
            type: string
      responses:
        "200":
          description: Resumen de hoy
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RetentionSummary"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
  /crm/settings/mail:
    get:
      tags:
        - CRM
      operationId: getCrmMailSettings
      summary: Obtiene la configuración de correo del CRM
      x-convray-runtime-status: active-crm-v1
      description: >-
        Devuelve la configuración SMTP y del remitente del tenant. El secreto de la
        contraseña nunca se expone (solo has_password). Detrás de la puerta de producto
        'crm': responde 404 si el header X-Convray-Product no coincide.
      security:
        - adminBearer: []
      parameters:
        - name: tenant
          in: query
          required: false
          description: Slug del tenant a operar (SuperAdmin/cuentas con acceso múltiple).
          schema:
            type: string
      responses:
        "200":
          description: Configuración de correo del tenant
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CrmMailSettingsResponse"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
    put:
      tags:
        - CRM
      operationId: updateCrmMailSettings
      summary: Actualiza la configuración de correo del CRM
      x-convray-runtime-status: active-crm-v1
      description: >-
        Hace upsert de la configuración SMTP y del remitente del tenant. Detrás de la
        puerta de producto 'crm' (404 si no coincide).
      security:
        - adminBearer: []
      parameters:
        - name: tenant
          in: query
          required: false
          description: Slug del tenant a operar.
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CrmMailSettingsRequest"
      responses:
        "200":
          description: Configuración actualizada
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CrmMailSettingsResponse"
        "400":
          description: Cuerpo inválido
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /crm/settings/mail/test:
    post:
      tags:
        - CRM
      operationId: testCrmMailSettings
      summary: Envía un correo de prueba con la configuración del CRM
      x-convray-runtime-status: active-crm-v1
      description: >-
        Envía un correo de prueba a la dirección indicada usando el SMTP configurado.
        Requiere que el correo esté configurado (422 mail-not-configured en caso contrario).
        Detrás de la puerta de producto 'crm' (404 si no coincide).
      security:
        - adminBearer: []
      parameters:
        - name: tenant
          in: query
          required: false
          description: Slug del tenant a operar.
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CrmTestMailRequest"
      responses:
        "200":
          description: Resultado del envío de prueba
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CrmTestMailResponse"
        "400":
          description: Cuerpo inválido
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
        "503":
          description: CRM deshabilitado (falta CRM_PII_ENCRYPTION_KEY)
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
  /crm/imports:
    get:
      tags:
        - CRM
      operationId: listCrmImports
      summary: Lista los lotes de importación de contactos
      x-convray-runtime-status: active-crm-v1
      description: >-
        Devuelve los lotes de importación de contactos del tenant, del más reciente al
        más antiguo. Detrás de la puerta de producto 'crm' (404 si no coincide).
      security:
        - adminBearer: []
      parameters:
        - name: tenant
          in: query
          required: false
          description: Slug del tenant a operar.
          schema:
            type: string
        - name: limit
          in: query
          required: false
          description: Máximo de lotes a devolver (acotado por el servidor).
          schema:
            type: integer
      responses:
        "200":
          description: Lotes de importación del tenant
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CrmImportListResponse"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
    post:
      tags:
        - CRM
      operationId: createCrmImport
      summary: Importa un CSV de contactos
      x-convray-runtime-status: active-crm-v1
      description: >-
        Sube un CSV de contactos (campo 'file' de un multipart/form-data) y crea un lote de
        importación. El límite de tamaño es 64 MB. Detrás de la puerta de producto 'crm'
        (404 si no coincide).
      security:
        - adminBearer: []
      parameters:
        - name: tenant
          in: query
          required: false
          description: Slug del tenant a operar.
          schema:
            type: string
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required:
                - file
              properties:
                file:
                  type: string
                  format: binary
                  description: Archivo CSV de contactos.
                dataset:
                  type: string
                  description: Dataset destino (opcional; por defecto 'contacts').
      responses:
        "201":
          description: Lote de importación creado
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CrmImportResponse"
        "400":
          description: Archivo inválido o ausente
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "413":
          description: El archivo supera el límite de 64 MB
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "422":
          $ref: "#/components/responses/ValidationFailed"
        "503":
          description: CRM deshabilitado (falta CRM_PII_ENCRYPTION_KEY)
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
  /crm/contacts:
    get:
      tags:
        - CRM
      operationId: listCrmContacts
      summary: Lista contactos del CRM
      x-convray-runtime-status: active-crm-v1
      description: >-
        Devuelve una página de contactos filtrable. El email se enmascara salvo para
        administradores. Detrás de la puerta de producto 'crm' (404 si no coincide).
      security:
        - adminBearer: []
      parameters:
        - name: tenant
          in: query
          required: false
          description: Slug del tenant a operar.
          schema:
            type: string
        - name: q
          in: query
          required: false
          description: Búsqueda por texto.
          schema:
            type: string
        - name: state
          in: query
          required: false
          description: Filtro por estado de actividad.
          schema:
            type: string
        - name: consent
          in: query
          required: false
          description: Filtra por consentimiento (true/false).
          schema:
            type: boolean
        - name: page
          in: query
          required: false
          schema:
            type: integer
        - name: page_size
          in: query
          required: false
          schema:
            type: integer
      responses:
        "200":
          description: Página de contactos
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CrmContactListResponse"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
  /crm/contacts/{ref}:
    get:
      tags:
        - CRM
      operationId: getCrmContact
      summary: Obtiene el detalle de un contacto
      x-convray-runtime-status: active-crm-v1
      description: >-
        Devuelve un contacto por su referencia (id o external_player_ref). El email se
        enmascara salvo para administradores. Detrás de la puerta de producto 'crm'
        (404 si no coincide o si el contacto no existe).
      security:
        - adminBearer: []
      parameters:
        - name: ref
          in: path
          required: true
          description: Id del contacto o external_player_ref.
          schema:
            type: string
        - name: tenant
          in: query
          required: false
          description: Slug del tenant a operar.
          schema:
            type: string
      responses:
        "200":
          description: Detalle del contacto
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CrmContactResponse"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
  /crm/attributes/recompute:
    post:
      tags:
        - CRM
      operationId: recomputeCrmAttributes
      summary: Recalcula los atributos derivados de los contactos
      x-convray-runtime-status: active-crm-v1
      description: >-
        Dispara el recálculo de los atributos derivados (estado de actividad, totales de
        depósito, etc.) de los contactos del tenant. Detrás de la puerta de producto 'crm'
        (404 si no coincide).
      security:
        - adminBearer: []
      parameters:
        - name: tenant
          in: query
          required: false
          description: Slug del tenant a operar.
          schema:
            type: string
      responses:
        "200":
          description: Recálculo ejecutado
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CrmRecomputeResponse"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
  /crm/summary:
    get:
      tags:
        - CRM
      operationId: getCrmSummary
      summary: Resumen agregado de contactos del CRM
      x-convray-runtime-status: active-crm-v1
      description: >-
        Devuelve el resumen de contactos del tenant: totales, con consentimiento,
        suscritos, suprimidos y desglose por estado. Detrás de la puerta de producto 'crm'
        (404 si no coincide).
      security:
        - adminBearer: []
      parameters:
        - name: tenant
          in: query
          required: false
          description: Slug del tenant a operar.
          schema:
            type: string
      responses:
        "200":
          description: Resumen de contactos
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CrmSummary"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
  /crm/nexor/settings:
    get:
      tags:
        - CRM
      operationId: getCrmNexorSettings
      summary: Obtiene la configuración del espejo Nexor
      x-convray-runtime-status: active-crm-v1
      description: >-
        Devuelve la configuración del espejo Nexor por tenant. Nunca incluye secretos (la
        llave y el secreto del webhook viven en el entorno del servidor). Detrás de la
        puerta de producto 'crm' (404 si no coincide).
      security:
        - adminBearer: []
      parameters:
        - name: tenant
          in: query
          required: false
          description: Slug del tenant a operar.
          schema:
            type: string
      responses:
        "200":
          description: Configuración del espejo Nexor
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CrmNexorSettingsResponse"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
    put:
      tags:
        - CRM
      operationId: updateCrmNexorSettings
      summary: Actualiza la configuración del espejo Nexor
      x-convray-runtime-status: active-crm-v1
      description: >-
        Hace upsert de la configuración del espejo Nexor por tenant. Detrás de la puerta de
        producto 'crm' (404 si no coincide).
      security:
        - adminBearer: []
      parameters:
        - name: tenant
          in: query
          required: false
          description: Slug del tenant a operar.
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CrmNexorSettingsRequest"
      responses:
        "200":
          description: Configuración del espejo Nexor actualizada
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CrmNexorSettingsResponse"
        "400":
          description: Cuerpo inválido
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /crm/segments:
    get:
      tags:
        - CRM
      operationId: listCrmSegments
      summary: Lista los segmentos del CRM
      x-convray-runtime-status: active-crm-v1
      description: >-
        Devuelve los segmentos definidos del tenant. Detrás de la puerta de producto 'crm'
        (404 si no coincide).
      security:
        - adminBearer: []
      parameters:
        - name: tenant
          in: query
          required: false
          description: Slug del tenant a operar.
          schema:
            type: string
      responses:
        "200":
          description: Segmentos del tenant
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CrmSegmentListResponse"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
    post:
      tags:
        - CRM
      operationId: createCrmSegment
      summary: Crea un segmento del CRM
      x-convray-runtime-status: active-crm-v1
      description: >-
        Crea un segmento con un conjunto de reglas {field, op, value}. Las reglas se validan
        contra la lista blanca de fields. Detrás de la puerta de producto 'crm' (404 si no
        coincide).
      security:
        - adminBearer: []
      parameters:
        - name: tenant
          in: query
          required: false
          description: Slug del tenant a operar.
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CrmSegmentRequest"
      responses:
        "201":
          description: Segmento creado
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CrmSegmentResponse"
        "400":
          description: Cuerpo inválido
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /crm/segments/{id}:
    get:
      tags:
        - CRM
      operationId: getCrmSegment
      summary: Obtiene un segmento del CRM
      x-convray-runtime-status: active-crm-v1
      description: >-
        Devuelve un segmento por id. Detrás de la puerta de producto 'crm' (404 si no
        coincide o si el segmento no existe).
      security:
        - adminBearer: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: tenant
          in: query
          required: false
          description: Slug del tenant a operar.
          schema:
            type: string
      responses:
        "200":
          description: Segmento
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CrmSegmentResponse"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
    patch:
      tags:
        - CRM
      operationId: updateCrmSegment
      summary: Actualiza parcialmente un segmento del CRM
      x-convray-runtime-status: active-crm-v1
      description: >-
        Aplica un PATCH parcial: solo los campos presentes se modifican. Detrás de la puerta
        de producto 'crm' (404 si no coincide o si el segmento no existe).
      security:
        - adminBearer: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: tenant
          in: query
          required: false
          description: Slug del tenant a operar.
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CrmSegmentPatchRequest"
      responses:
        "200":
          description: Segmento actualizado
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CrmSegmentResponse"
        "400":
          description: Cuerpo inválido
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /crm/segments/{id}/preview:
    get:
      tags:
        - CRM
      operationId: previewCrmSegment
      summary: Previsualiza la audiencia elegible de un segmento
      x-convray-runtime-status: active-crm-v1
      description: >-
        Devuelve la cuenta elegible, el desglose de exclusiones por política y una muestra
        de hasta 10 referencias. Detrás de la puerta de producto 'crm' (404 si no coincide
        o si el segmento no existe).
      security:
        - adminBearer: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: tenant
          in: query
          required: false
          description: Slug del tenant a operar.
          schema:
            type: string
      responses:
        "200":
          description: Previsualización del segmento
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CrmSegmentPreview"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
  /crm/templates:
    get:
      tags:
        - CRM
      operationId: listCrmTemplates
      summary: Lista las plantillas de correo del CRM
      x-convray-runtime-status: active-crm-v1
      description: >-
        Devuelve las plantillas de correo del tenant. Detrás de la puerta de producto 'crm'
        (404 si no coincide).
      security:
        - adminBearer: []
      parameters:
        - name: tenant
          in: query
          required: false
          description: Slug del tenant a operar.
          schema:
            type: string
      responses:
        "200":
          description: Plantillas del tenant
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CrmTemplateListResponse"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
    post:
      tags:
        - CRM
      operationId: createCrmTemplate
      summary: Crea una plantilla de correo del CRM
      x-convray-runtime-status: active-crm-v1
      description: >-
        Crea una plantilla de correo (asunto, HTML, texto y variables). Detrás de la puerta
        de producto 'crm' (404 si no coincide).
      security:
        - adminBearer: []
      parameters:
        - name: tenant
          in: query
          required: false
          description: Slug del tenant a operar.
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CrmTemplateRequest"
      responses:
        "201":
          description: Plantilla creada
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CrmTemplateResponse"
        "400":
          description: Cuerpo inválido
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /crm/templates/{id}:
    get:
      tags:
        - CRM
      operationId: getCrmTemplate
      summary: Obtiene una plantilla de correo del CRM
      x-convray-runtime-status: active-crm-v1
      description: >-
        Devuelve una plantilla por id. Detrás de la puerta de producto 'crm' (404 si no
        coincide o si la plantilla no existe).
      security:
        - adminBearer: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: tenant
          in: query
          required: false
          description: Slug del tenant a operar.
          schema:
            type: string
      responses:
        "200":
          description: Plantilla
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CrmTemplateResponse"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
    patch:
      tags:
        - CRM
      operationId: updateCrmTemplate
      summary: Actualiza parcialmente una plantilla de correo del CRM
      x-convray-runtime-status: active-crm-v1
      description: >-
        Aplica un PATCH parcial: solo los campos presentes se modifican. Detrás de la puerta
        de producto 'crm' (404 si no coincide o si la plantilla no existe).
      security:
        - adminBearer: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: tenant
          in: query
          required: false
          description: Slug del tenant a operar.
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CrmTemplatePatchRequest"
      responses:
        "200":
          description: Plantilla actualizada
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CrmTemplateResponse"
        "400":
          description: Cuerpo inválido
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /crm/templates/{id}/test:
    post:
      tags:
        - CRM
      operationId: testCrmTemplate
      summary: Envía un correo de prueba de una plantilla
      x-convray-runtime-status: active-crm-v1
      description: >-
        Renderiza la plantilla y la envía a la dirección indicada como prueba. Detrás de la
        puerta de producto 'crm' (404 si no coincide o si la plantilla no existe).
      security:
        - adminBearer: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: tenant
          in: query
          required: false
          description: Slug del tenant a operar.
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CrmTestMailRequest"
      responses:
        "200":
          description: Resultado del envío de prueba
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CrmTestMailResponse"
        "400":
          description: Cuerpo inválido
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /crm/compare:
    get:
      tags:
        - CRM
      operationId: compareCrmCampaigns
      summary: Compara métricas de varias campañas
      x-convray-runtime-status: active-crm-v1
      description: >-
        Compara entre 2 y 6 campañas del mismo tenant con sus métricas alineadas. Las
        campañas se pasan como lista separada por comas en 'campaigns'. Detrás de la puerta
        de producto 'crm' (404 si no coincide).
      security:
        - adminBearer: []
      parameters:
        - name: campaigns
          in: query
          required: true
          description: Ids de campañas separados por coma (2 a 6).
          schema:
            type: string
        - name: tenant
          in: query
          required: false
          description: Slug del tenant a operar.
          schema:
            type: string
      responses:
        "200":
          description: Comparación de campañas
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CrmCompareResult"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /crm/impact:
    get:
      tags:
        - CRM
      operationId: getCrmImpact
      summary: Impacto agregado del CRM en un período
      x-convray-runtime-status: active-crm-v1
      description: >-
        Suma todas las campañas lanzadas en el período (día civil America/Santiago del
        lanzamiento, ambos extremos inclusive) con la misma atribución contra Data del reporte
        por campaña: embudo (contactados, enviados, entregados, aperturas aproximadas, clics),
        convertidos (depositaron dentro de la ventana de su campaña), montos y el incremental
        contra el grupo de control (solo campañas con control). Sin 'from'/'to' lee los últimos
        30 días. Montos null bajo el umbral de población. Detrás de la puerta de producto 'crm'
        (404 si no coincide).
      security:
        - adminBearer: []
      parameters:
        - name: from
          in: query
          required: false
          description: Día civil inicial (YYYY-MM-DD). Va junto con 'to'.
          schema:
            type: string
            format: date
        - name: to
          in: query
          required: false
          description: Día civil final (YYYY-MM-DD), máximo 400 días desde 'from'.
          schema:
            type: string
            format: date
        - name: channel
          in: query
          required: false
          description: Canal de las campañas a sumar.
          schema:
            type: string
            enum:
              - all
              - email
              - voice_nexor
        - name: tenant
          in: query
          required: false
          description: Slug del tenant a operar.
          schema:
            type: string
      responses:
        "200":
          description: Impacto agregado del CRM
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CrmImpact"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /crm/o/{token}:
    get:
      tags:
        - CRM
      operationId: trackCrmOpen
      summary: Pixel de apertura de correo (público)
      x-convray-runtime-status: active-crm-v1
      description: >-
        Registra la apertura del mensaje cuyo token de apertura se presenta y devuelve un
        GIF 1x1 transparente. Endpoint público sin sesión: el token es la capacidad. El
        pixel se sirve siempre (aunque el token no sea válido) con Cache-Control no-store.
        El sufijo '.gif' del token se ignora.
      security: []
      parameters:
        - name: token
          in: path
          required: true
          description: Token de apertura firmado (admite sufijo .gif).
          schema:
            type: string
      responses:
        "200":
          description: GIF 1x1 transparente
          content:
            image/gif:
              schema:
                type: string
                format: binary
  /crm/t/{token}:
    get:
      tags:
        - CRM
      operationId: trackCrmClick
      summary: Redirección de clic con seguimiento (público)
      x-convray-runtime-status: active-crm-v1
      description: >-
        Registra el clic del enlace y redirige (302) al destino original. Endpoint público
        sin sesión: el token es la capacidad. Responde 404 si el token no existe y 503 si el
        seguimiento no está disponible.
      security: []
      parameters:
        - name: token
          in: path
          required: true
          description: Token del enlace rastreado.
          schema:
            type: string
      responses:
        "302":
          description: Redirección al destino original del enlace
          headers:
            Location:
              description: URL de destino original.
              schema:
                type: string
        "404":
          description: Enlace no encontrado
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "503":
          description: Seguimiento no disponible
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
  /crm/u/{token}:
    get:
      tags:
        - CRM
      operationId: trackCrmUnsubscribe
      summary: Baja de la lista de correo (público)
      x-convray-runtime-status: active-crm-v1
      description: >-
        Procesa la baja del destinatario cuyo token se presenta (evento, subscribed=false y
        supresión) y devuelve una página HTML de confirmación. Endpoint público sin sesión:
        el token es la capacidad. Responde 200 con HTML de confirmación, 404 con HTML si el
        token no es válido y 503 si el seguimiento no está disponible.
      security: []
      parameters:
        - name: token
          in: path
          required: true
          description: Token de baja firmado.
          schema:
            type: string
      responses:
        "200":
          description: Página HTML de confirmación de baja
          content:
            text/html:
              schema:
                type: string
        "404":
          description: Token de baja inválido o expirado (página HTML)
          content:
            text/html:
              schema:
                type: string
        "503":
          description: Seguimiento no disponible
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
  /crm/campaigns:
    get:
      tags:
        - CRM
      operationId: listCrmCampaigns
      summary: Lista las campañas del tenant
      x-convray-runtime-status: active-crm-v1
      description: Devuelve las campañas de CRM del tenant. Detrás de la puerta de producto 'crm'; si el header X-Convray-Product no es 'crm' responde 404.
      security:
        - adminBearer: []
      parameters:
        - name: tenant
          in: query
          required: false
          schema:
            type: string
      responses:
        "200":
          description: Campañas del tenant
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CrmCampaignListResponse"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
    post:
      tags:
        - CRM
      operationId: createCrmCampaign
      summary: Crea una campaña en borrador
      x-convray-runtime-status: active-crm-v1
      description: Crea una campaña. Siempre nace en estado 'draft'; scheduled_at se acepta pero no programa (solo /schedule materializa y programa). Product gate 'crm' => 404.
      security:
        - adminBearer: []
      parameters:
        - name: tenant
          in: query
          required: false
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CrmCampaignCreateRequest"
      responses:
        "201":
          description: Campaña creada
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CrmCampaignEnvelope"
        "400":
          description: Cuerpo JSON ilegible
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /crm/campaigns/{id}:
    get:
      tags:
        - CRM
      operationId: getCrmCampaign
      summary: Obtiene una campaña por id
      x-convray-runtime-status: active-crm-v1
      description: Devuelve una campaña del tenant. Product gate 'crm' => 404; campaña inexistente => 404.
      security:
        - adminBearer: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
        - name: tenant
          in: query
          required: false
          schema:
            type: string
      responses:
        "200":
          description: Campaña
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CrmCampaignEnvelope"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
    patch:
      tags:
        - CRM
      operationId: updateCrmCampaign
      summary: Edita una campaña en borrador
      x-convray-runtime-status: active-crm-v1
      description: Aplica un PATCH parcial (solo campos presentes) a una campaña en 'draft'. Product gate 'crm' => 404; estado no editable => 409.
      security:
        - adminBearer: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
        - name: tenant
          in: query
          required: false
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CrmCampaignPatchRequest"
      responses:
        "200":
          description: Campaña actualizada
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CrmCampaignEnvelope"
        "400":
          description: Cuerpo JSON ilegible
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "409":
          $ref: "#/components/responses/VersionConflict"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /crm/campaigns/{id}/schedule:
    post:
      tags:
        - CRM
      operationId: scheduleCrmCampaign
      summary: Materializa y programa una campaña
      x-convray-runtime-status: active-crm-v1
      description: Congela la audiencia/control y programa la campaña. scheduled_at ISO 8601 opcional; vacío envía cuanto antes. Product gate 'crm' => 404; estado no válido => 409; fecha inválida => 422.
      security:
        - adminBearer: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
        - name: tenant
          in: query
          required: false
          schema:
            type: string
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CrmCampaignScheduleRequest"
      responses:
        "200":
          description: Campaña programada
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CrmCampaignEnvelope"
        "400":
          description: Cuerpo JSON ilegible
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "409":
          $ref: "#/components/responses/VersionConflict"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /crm/campaigns/{id}/pause:
    post:
      tags:
        - CRM
      operationId: pauseCrmCampaign
      summary: Pausa una campaña en curso
      x-convray-runtime-status: active-crm-v1
      description: Transiciona la campaña a pausada. Product gate 'crm' => 404; estado no válido para pausar => 409.
      security:
        - adminBearer: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
        - name: tenant
          in: query
          required: false
          schema:
            type: string
      responses:
        "200":
          description: Campaña pausada
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CrmCampaignEnvelope"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "409":
          $ref: "#/components/responses/VersionConflict"
  /crm/campaigns/{id}/resume:
    post:
      tags:
        - CRM
      operationId: resumeCrmCampaign
      summary: Reanuda una campaña pausada
      x-convray-runtime-status: active-crm-v1
      description: Transiciona la campaña de pausada a en curso. Product gate 'crm' => 404; estado no válido => 409.
      security:
        - adminBearer: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
        - name: tenant
          in: query
          required: false
          schema:
            type: string
      responses:
        "200":
          description: Campaña reanudada
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CrmCampaignEnvelope"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "409":
          $ref: "#/components/responses/VersionConflict"
  /crm/campaigns/{id}/cancel:
    post:
      tags:
        - CRM
      operationId: cancelCrmCampaign
      summary: Cancela una campaña
      x-convray-runtime-status: active-crm-v1
      description: Transiciona la campaña a cancelada. Product gate 'crm' => 404; estado no válido => 409.
      security:
        - adminBearer: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
        - name: tenant
          in: query
          required: false
          schema:
            type: string
      responses:
        "200":
          description: Campaña cancelada
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CrmCampaignEnvelope"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "409":
          $ref: "#/components/responses/VersionConflict"
  /crm/campaigns/{id}/report:
    get:
      tags:
        - CRM
      operationId: getCrmCampaignReport
      summary: Reporte de una campaña
      x-convray-runtime-status: active-crm-v1
      description: Devuelve el embudo, conversiones send/control, lift y KPIs ampliados. Incluye 'voice_funnel' solo para campañas del canal voice_nexor. Product gate 'crm' => 404.
      security:
        - adminBearer: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
        - name: tenant
          in: query
          required: false
          schema:
            type: string
      responses:
        "200":
          description: Reporte de la campaña
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CrmCampaignReport"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
  /crm/campaigns/{id}/recipients:
    get:
      tags:
        - CRM
      operationId: listCrmCampaignRecipients
      summary: Destinatarios de una campaña
      x-convray-runtime-status: active-crm-v1
      description: Lista los destinatarios de la campaña (email enmascarado salvo admin). Con format=csv devuelve el archivo CSV. Product gate 'crm' => 404.
      security:
        - adminBearer: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
        - name: tenant
          in: query
          required: false
          schema:
            type: string
        - name: format
          in: query
          required: false
          description: Con valor 'csv' descarga el listado como CSV.
          schema:
            type: string
            enum:
              - csv
      responses:
        "200":
          description: Destinatarios (JSON o CSV según format)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CrmCampaignRecipientsResponse"
            text/csv:
              schema:
                type: string
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
  /crm/campaigns/{id}/audience-check:
    get:
      tags:
        - CRM
      operationId: getCrmCampaignAudienceCheck
      summary: Dry-run de contactabilidad de la audiencia
      x-convray-runtime-status: active-crm-v1
      description: Estima cuántos Player IDs de la audiencia (data_segment) de la campaña son contactables y cuántos quedan excluidos por motivo, sin congelar nada. Product gate 'crm' => 404.
      security:
        - adminBearer: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
        - name: tenant
          in: query
          required: false
          schema:
            type: string
      responses:
        "200":
          description: Resultado del dry-run de audiencia
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CrmAudienceCheck"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
  /crm/audiences:
    get:
      tags:
        - CRM
      operationId: listCrmAudiences
      summary: Lista las audiencias del tenant
      x-convray-runtime-status: active-crm-v1
      description: Devuelve los data_segments del tenant usados como audiencia, con sus conteos cacheados. Product gate 'crm' => 404.
      security:
        - adminBearer: []
      parameters:
        - name: tenant
          in: query
          required: false
          schema:
            type: string
      responses:
        "200":
          description: Audiencias del tenant
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CrmAudienceListResponse"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
    post:
      tags:
        - CRM
      operationId: createCrmAudience
      summary: Crea una audiencia dinámica por filtros
      x-convray-runtime-status: active-crm-v1
      description: Crea un data_segment dinámico validando los filtros con el contrato de players. Product gate 'crm' => 404; nombre en uso => 409; filtros inválidos => 422.
      security:
        - adminBearer: []
      parameters:
        - name: tenant
          in: query
          required: false
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CrmAudienceCreateRequest"
      responses:
        "201":
          description: Audiencia creada
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CrmAudience"
        "400":
          description: Cuerpo JSON ilegible
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "409":
          $ref: "#/components/responses/VersionConflict"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /crm/audiences/preview:
    post:
      tags:
        - CRM
      operationId: previewCrmAudienceFilters
      summary: Previsualiza filtros de audiencia sin guardar
      x-convray-runtime-status: active-crm-v1
      description: Previsualiza un objeto de filtros ad-hoc (mismo contrato que /data/segments) devolviendo conteo y agregados, sin persistir. El cuerpo es el objeto de filtros. Product gate 'crm' => 404; filtros inválidos => 422.
      security:
        - adminBearer: []
      parameters:
        - name: tenant
          in: query
          required: false
          schema:
            type: string
        - name: from
          in: query
          required: false
          description: Inicio del rango (fecha).
          schema:
            type: string
        - name: to
          in: query
          required: false
          description: Fin del rango (fecha).
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CrmAudienceFilters"
      responses:
        "200":
          description: Preview de la audiencia
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CrmAudiencePreview"
        "400":
          description: Filtros ilegibles
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /crm/audiences/import:
    post:
      tags:
        - CRM
      operationId: importCrmAudience
      summary: Importa una audiencia estática de Player IDs
      x-convray-runtime-status: active-crm-v1
      description: Crea un data_segment estático desde una lista de Player IDs. Acepta multipart/form-data con un CSV (campo file) o un cuerpo JSON con player_ids. id_kind = external (ID Centrivo, default) | casino (ID de Juégalo). Product gate 'crm' => 404; nombre en uso => 409; CSV/archivo inválido => 400; archivo muy grande o demasiadas filas => 413.
      security:
        - adminBearer: []
      parameters:
        - name: tenant
          in: query
          required: false
          schema:
            type: string
        - name: limit
          in: query
          required: false
          description: Recorta a las primeras N filas antes de deduplicar (0/omitido = todas).
          schema:
            type: integer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CrmAudienceImportRequest"
          multipart/form-data:
            schema:
              $ref: "#/components/schemas/CrmAudienceImportMultipart"
      responses:
        "201":
          description: Audiencia estática importada
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CrmAudienceImportResult"
        "400":
          description: Cuerpo, archivo o CSV inválido
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "409":
          $ref: "#/components/responses/VersionConflict"
        "413":
          description: Archivo demasiado grande o demasiadas filas
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /crm/audiences/filter-options:
    get:
      tags:
        - CRM
      operationId: listCrmAudienceFilterOptions
      summary: Opciones de los filtros de audiencia
      x-convray-runtime-status: active-crm-v1
      description: Devuelve los valores distintos de cada dimensión de filtro de players (estado, país, región, dispositivo, btag, partner) con su conteo. Product gate 'crm' => 404.
      security:
        - adminBearer: []
      parameters:
        - name: tenant
          in: query
          required: false
          schema:
            type: string
      responses:
        "200":
          description: Dimensiones de filtro con sus valores
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CrmFilterOptionsResponse"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
  /crm/audiences/{id}/preview:
    get:
      tags:
        - CRM
      operationId: getCrmAudiencePreview
      summary: Previsualiza una audiencia guardada
      x-convray-runtime-status: active-crm-v1
      description: Previsualiza un data_segment guardado por id devolviendo conteo, contactables y agregados del rango. Product gate 'crm' => 404; segmento inexistente => 404.
      security:
        - adminBearer: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
        - name: tenant
          in: query
          required: false
          schema:
            type: string
        - name: from
          in: query
          required: false
          description: Inicio del rango (fecha).
          schema:
            type: string
        - name: to
          in: query
          required: false
          description: Fin del rango (fecha).
          schema:
            type: string
      responses:
        "200":
          description: Preview de la audiencia
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CrmAudiencePreview"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
  /crm/audiences/{id}/uploads:
    get:
      tags:
        - CRM
      operationId: listCrmAudienceUploads
      summary: Historial de cargas de una audiencia estática
      x-convray-runtime-status: active-crm-v1
      description: Devuelve el historial de cargas de CSV de un segmento estático (auditoría), nunca la lista de Player IDs. Product gate 'crm' => 404; segmento inexistente => 404.
      security:
        - adminBearer: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
        - name: tenant
          in: query
          required: false
          schema:
            type: string
      responses:
        "200":
          description: Historial de cargas
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CrmAudienceUploadsResponse"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
  /crm/opportunities:
    get:
      tags:
        - CRM
      operationId: listCrmOpportunities
      summary: Bandeja del detector de oportunidades
      x-convray-runtime-status: active-crm-v1
      description: Devuelve el catálogo de playbooks materializado por tenant con conteos, depositado histórico, tendencia y recomendaciones. Los conteos salen de la foto del día (se refresca tras cada import y cada hora); sólo se calculan en vivo si todavía no hay foto del día o si el request pide umbrales distintos de los defaults. Product gate 'crm' => 404.
      security:
        - adminBearer: []
      parameters:
        - name: tenant
          in: query
          required: false
          schema:
            type: string
        - name: playbook
          in: query
          required: false
          description: Filtra la bandeja a un playbook por su código.
          schema:
            type: string
        - name: withdraw_threshold_minor
          in: query
          required: false
          description: Umbral de retiro grande en unidad menor (0/omitido = default del catálogo).
          schema:
            type: integer
            format: int64
        - name: high_value_ngr_minor
          in: query
          required: false
          description: Umbral de NGR de alto valor en unidad menor (0/omitido = default del catálogo).
          schema:
            type: integer
            format: int64
      responses:
        "200":
          description: Bandeja de oportunidades
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CrmOpportunitiesResponse"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
  /crm/opportunities/{playbook}/segment:
    post:
      tags:
        - CRM
      operationId: createCrmOpportunitySegment
      summary: Crea un segmento desde una oportunidad
      x-convray-runtime-status: active-crm-v1
      description: Crea el data_segment dinámico de la variante de un playbook (deja auditoría). No crea campañas ni envía nada. Product gate 'crm' => 404; playbook o variante inexistente => 404; playbook no segmentable/no disponible => 422.
      security:
        - adminBearer: []
      parameters:
        - name: playbook
          in: path
          required: true
          description: Código del playbook del catálogo.
          schema:
            type: string
        - name: tenant
          in: query
          required: false
          schema:
            type: string
        - name: withdraw_threshold_minor
          in: query
          required: false
          description: Umbral de retiro grande en unidad menor (0/omitido = default del catálogo).
          schema:
            type: integer
            format: int64
        - name: high_value_ngr_minor
          in: query
          required: false
          description: Umbral de NGR de alto valor en unidad menor (0/omitido = default del catálogo).
          schema:
            type: integer
            format: int64
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CrmOpportunitySegmentRequest"
      responses:
        "201":
          description: Segmento de oportunidad creado
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CrmOpportunitySegmentResult"
        "400":
          description: Cuerpo JSON ilegible
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /casino/partners/metrics:
    get:
      tags:
        - Casino
      operationId: getCasinoPartnerMetrics
      summary: Métricas agregadas de partners del casino
      x-convray-runtime-status: active-casino-partners-v1
      description: >-
        Devuelve las métricas agregadas por partner del casino de la sesión (el
        tenant sale del token, no del path), paginadas por cursor, más el estado
        de ingestión inline. Solo lectura.
      security:
        - adminBearer: []
      parameters:
        - name: partner
          in: query
          required: false
          description: Slug de un partner para acotar la consulta.
          schema:
            type: string
        - name: period_from
          in: query
          required: false
          description: Inicio de la ventana en YYYY-MM (mensual) o YYYY-MM-DD (día).
          schema:
            type: string
        - name: period_to
          in: query
          required: false
          description: Fin de la ventana en el mismo formato que period_from.
          schema:
            type: string
        - name: cursor
          in: query
          required: false
          description: Cursor opaco de paginación.
          schema:
            type: string
        - name: limit
          in: query
          required: false
          schema:
            type: string
      responses:
        "200":
          description: Métricas de partners paginadas más el estado de ingestión
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CasinoPartnerMetricsPage"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /casino/partners/players/metrics:
    get:
      tags:
        - Casino
      operationId: getCasinoPartnerPlayerMetrics
      summary: Métricas por jugador de un partner
      x-convray-runtime-status: active-casino-partners-v1
      description: >-
        Drill-down por jugador de un partner del casino sobre el rango [from, to]
        inclusivo. Suma solo ventanas disjuntas para no duplicar tramos solapados.
        Solo lectura.
      security:
        - adminBearer: []
      parameters:
        - name: partner
          in: query
          required: false
          description: Slug del partner a consultar.
          schema:
            type: string
        - name: from
          in: query
          required: true
          description: Día inicial en YYYY-MM-DD (inclusivo).
          schema:
            type: string
        - name: to
          in: query
          required: true
          description: Día final en YYYY-MM-DD (inclusivo).
          schema:
            type: string
      responses:
        "200":
          description: Jugadores del partner ordenados por NGR descendente
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PlayerMetricsPage"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /casino/partners/community-metrics:
    get:
      tags:
        - Casino
      operationId: getCasinoPartnerCommunityMetrics
      summary: Métricas de comunidad por partner
      x-convray-runtime-status: active-casino-partners-v1
      description: >-
        Clicks, jugadores activos agregados y membresía de comunidad por partner
        de la allowlist del connector del casino, con serie diaria de ceros
        explícitos. Solo lectura.
      security:
        - adminBearer: []
      parameters:
        - name: window_days
          in: query
          required: false
          description: Tamaño de la ventana en días.
          schema:
            type: string
        - name: partner
          in: query
          required: false
          description: Slug de un partner para acotar la consulta.
          schema:
            type: string
      responses:
        "200":
          description: Métricas de comunidad por partner
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerCommunityPage"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /casino/partners/site-analytics:
    get:
      tags:
        - Casino
      operationId: getCasinoPartnerSiteAnalytics
      summary: Analítica web del Streamer Site de los partners gestionados
      x-convray-runtime-status: active-casino-partners-v1
      description: >-
        Rollup web (impresiones, clicks, CTR, desglose por surface, serie diaria y
        drilldown por sitio) de los streamers gestionados del casino. Solo tráfico
        web, nunca conversiones. Solo lectura.
      security:
        - adminBearer: []
      parameters:
        - name: period
          in: query
          required: false
          description: Clave del período a consultar.
          schema:
            type: string
      responses:
        "200":
          description: Analítica web agregada del período
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerSiteAnalytics"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /casino/partners/btags/unmapped:
    get:
      tags:
        - Casino
      operationId: listCasinoUnmappedBtags
      summary: Grupos de BTAG sin mapear
      x-convray-runtime-status: active-casino-partners-v1
      description: >-
        Grupos de BTAG sin mapear agrupados por prefijo o código del afiliado, con
        agregados y variantes de ejemplo, para decidir un mapeo por prefijo. Solo
        agregados, sin PII.
      security:
        - adminBearer: []
      parameters:
        - name: from
          in: query
          required: false
          description: Inicio del rango en YYYY-MM-DD.
          schema:
            type: string
        - name: to
          in: query
          required: false
          description: Fin del rango en YYYY-MM-DD.
          schema:
            type: string
      responses:
        "200":
          description: Grupos de BTAG sin mapear
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UnmappedBtagGroupList"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /casino/partners/data-metrics:
    get:
      tags:
        - Casino
      operationId: getCasinoPartnerDataMetrics
      summary: Métricas de afiliados desde Data
      x-convray-runtime-status: active-casino-partners-v1
      description: >-
        Totales, serie diaria, desglose por código y ranking por partner desde
        Data (data_partner_daily_rollup) para el rango pedido. Declara desde cuándo
        hay cada dato. Solo lectura.
      security:
        - adminBearer: []
      parameters:
        - name: partner
          in: query
          required: false
          description: Slug de un partner para acotar la consulta.
          schema:
            type: string
        - name: from
          in: query
          required: false
          description: Inicio del rango en YYYY-MM-DD.
          schema:
            type: string
        - name: to
          in: query
          required: false
          description: Fin del rango en YYYY-MM-DD.
          schema:
            type: string
      responses:
        "200":
          description: Métricas de afiliados desde Data
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerDataMetrics"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /casino/partners/data-metrics/recalculate:
    post:
      tags:
        - Casino
      operationId: recalculateCasinoPartnerDataMetrics
      summary: Recalcula el rollup por partner desde Data
      x-convray-runtime-status: active-casino-partners-v1
      description: >-
        Dispara el recálculo manual del rollup por partner de los últimos días
        (1 a 400, default 400). El cuerpo es opcional. Responde 200 con status
        "done" si terminó dentro del presupuesto síncrono, o 202 con status
        "queued" si sigue en segundo plano.
      security:
        - adminBearer: []
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RecalculatePartnerDataMetricsRequest"
      responses:
        "200":
          description: Recálculo terminado dentro del presupuesto síncrono
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerDataRecalcResult"
        "202":
          description: Recálculo encolado en segundo plano
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerDataRecalcResult"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "409":
          description: Ya hay una recalculación en curso para el tenant
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /casino/partners/reconciliation:
    get:
      tags:
        - Casino
      operationId: getCasinoPartnerReconciliation
      summary: Conciliación portal vs Data por partner
      x-convray-runtime-status: active-casino-partners-v1
      description: >-
        Compara, por partner y mes, los registros, FTD, depósitos y GGR del portal
        contra los de Data, con la diferencia absoluta y porcentual. Solo lectura.
      security:
        - adminBearer: []
      parameters:
        - name: month
          in: query
          required: false
          description: Mes a conciliar en YYYY-MM.
          schema:
            type: string
      responses:
        "200":
          description: Conciliación portal vs Data del mes
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerReconciliation"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /casino/partners/ngr-reconciliation:
    get:
      tags:
        - Casino
      operationId: getCasinoPartnerNGRReconciliation
      summary: Conciliación de NGR Data vs portal por partner
      x-convray-runtime-status: active-casino-partners-v1
      description: >-
        Compara, por partner y mes, el NGR calculado desde Data contra el NGR del
        portal de afiliados, con la diferencia absoluta y porcentual, la cobertura
        y un veredicto. Solo lectura.
      security:
        - adminBearer: []
      parameters:
        - name: from
          in: query
          required: false
          description: Inicio del rango en YYYY-MM-DD.
          schema:
            type: string
        - name: to
          in: query
          required: false
          description: Fin del rango en YYYY-MM-DD.
          schema:
            type: string
      responses:
        "200":
          description: Conciliación de NGR Data vs portal del rango
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerNGRReconciliation"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /casino/partners/{partnerSlug}/btags:
    get:
      tags:
        - Casino
      operationId: listCasinoPartnerBtags
      summary: Lista los BTAG mapeados de un partner
      x-convray-runtime-status: active-casino-partners-v1
      description: >-
        Devuelve los mapeos BTAG a partner (vigentes o cerrados) del partner
        indicado, con el conteo de jugadores por mapeo. Solo lectura.
      security:
        - adminBearer: []
      parameters:
        - name: partnerSlug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Mapeos BTAG del partner
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerBtagList"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
    post:
      tags:
        - Casino
      operationId: createCasinoPartnerBtag
      summary: Crea un mapeo BTAG para un partner
      x-convray-runtime-status: active-casino-partners-v1
      description: >-
        Crea un mapeo BTAG a partner por texto exacto ('exact') o por prefijo de
        afiliado ('prefix').
      security:
        - adminBearer: []
      parameters:
        - name: partnerSlug
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreatePartnerBtagRequest"
      responses:
        "201":
          description: Mapeo BTAG creado
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerBtag"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "409":
          $ref: "#/components/responses/VersionConflict"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /casino/partners/{partnerSlug}/btags/{id}:
    delete:
      tags:
        - Casino
      operationId: closeCasinoPartnerBtag
      summary: Cierra la vigencia de un mapeo BTAG
      x-convray-runtime-status: active-casino-partners-v1
      description: Cierra la vigencia del mapeo BTAG indicado del partner. No devuelve contenido.
      security:
        - adminBearer: []
      parameters:
        - name: partnerSlug
          in: path
          required: true
          schema:
            type: string
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "204":
          description: Mapeo BTAG cerrado
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
  /casino/partners/{partnerSlug}/performance:
    get:
      tags:
        - Casino
      operationId: getCasinoPartnerPerformance
      summary: Rendimiento de un partner con serie y comparación
      x-convray-runtime-status: active-casino-partners-v1
      description: >-
        Rendimiento por partner desde Data: totales con activos distintos y
        conversión, serie diaria y comparación contra el período anterior de igual
        largo. Solo lectura.
      security:
        - adminBearer: []
      parameters:
        - name: partnerSlug
          in: path
          required: true
          schema:
            type: string
        - name: from
          in: query
          required: false
          description: Inicio del rango en YYYY-MM-DD.
          schema:
            type: string
        - name: to
          in: query
          required: false
          description: Fin del rango en YYYY-MM-DD.
          schema:
            type: string
      responses:
        "200":
          description: Rendimiento del partner
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerPerformance"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /casino/partners/{partnerSlug}/players:
    get:
      tags:
        - Casino
      operationId: listCasinoPartnerPlayers
      summary: Jugadores de un partner (solo casino)
      x-convray-runtime-status: active-casino-partners-v1
      description: >-
        Listado por jugador de un partner visto por el casino: identidad, banderas,
        FTD, agregados del rango e históricos, NGR/LTV y últimas actividades. La PII
        solo se descifra con permiso del casino. Paginado y ordenable.
      security:
        - adminBearer: []
      parameters:
        - name: partnerSlug
          in: path
          required: true
          schema:
            type: string
        - name: from
          in: query
          required: false
          description: Inicio del rango en YYYY-MM-DD.
          schema:
            type: string
        - name: to
          in: query
          required: false
          description: Fin del rango en YYYY-MM-DD.
          schema:
            type: string
        - name: segment
          in: query
          required: false
          description: Segmento del listado.
          schema:
            type: string
            enum:
              - all
              - registered
              - ftd
              - depositors
              - bettors
              - inactive
        - name: sort
          in: query
          required: false
          description: Clave de orden (lista cerrada con alias).
          schema:
            type: string
        - name: dir
          in: query
          required: false
          description: Dirección del orden.
          schema:
            type: string
            enum:
              - asc
              - desc
        - name: page
          in: query
          required: false
          schema:
            type: string
        - name: page_size
          in: query
          required: false
          schema:
            type: string
      responses:
        "200":
          description: Jugadores del partner
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerPlayersPage"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /casino/partners/{partnerSlug}/players/{ref}/profile:
    get:
      tags:
        - Casino
      operationId: getCasinoPartnerPlayerProfile
      summary: Ficha de un jugador de un partner
      x-convray-runtime-status: active-casino-partners-v1
      description: >-
        Ficha de un jugador de un partner desde el casino. Responde 404 si el
        jugador no pertenece a ese partner. La PII solo se descifra con permiso.
      security:
        - adminBearer: []
      parameters:
        - name: partnerSlug
          in: path
          required: true
          schema:
            type: string
        - name: ref
          in: path
          required: true
          description: Referencia externa del jugador (external_player_ref).
          schema:
            type: string
        - name: from
          in: query
          required: false
          description: Inicio del rango en YYYY-MM-DD.
          schema:
            type: string
        - name: to
          in: query
          required: false
          description: Fin del rango en YYYY-MM-DD.
          schema:
            type: string
      responses:
        "200":
          description: Ficha del jugador
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerPlayerRow"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /casino/partners/{partnerSlug}/players/{ref}/profile/activity:
    get:
      tags:
        - Casino
      operationId: listCasinoPartnerPlayerActivity
      summary: Línea de tiempo de un jugador de un partner
      x-convray-runtime-status: active-casino-partners-v1
      description: >-
        Sublista paginada de movimientos mezclados (registro, depósito, retiro,
        apuesta deportiva, casino) del jugador. La referencia de depósito/retiro va
        enmascarada sin permiso de PII. Responde 404 si el jugador no es del partner.
      security:
        - adminBearer: []
      parameters:
        - name: partnerSlug
          in: path
          required: true
          schema:
            type: string
        - name: ref
          in: path
          required: true
          schema:
            type: string
        - name: limit
          in: query
          required: false
          description: Tamaño de página (1 a 200, default 50).
          schema:
            type: integer
        - name: offset
          in: query
          required: false
          schema:
            type: integer
      responses:
        "200":
          description: Movimientos del jugador
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerPlayerActivityPage"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /casino/partners/{partnerSlug}/players/{ref}/profile/deposits:
    get:
      tags:
        - Casino
      operationId: listCasinoPartnerPlayerDeposits
      summary: Depósitos de un jugador de un partner
      x-convray-runtime-status: active-casino-partners-v1
      description: >-
        Sublista paginada de transacciones de depósito del jugador. La referencia
        va enmascarada sin permiso de PII. Responde 404 si el jugador no es del partner.
      security:
        - adminBearer: []
      parameters:
        - name: partnerSlug
          in: path
          required: true
          schema:
            type: string
        - name: ref
          in: path
          required: true
          schema:
            type: string
        - name: limit
          in: query
          required: false
          description: Tamaño de página (1 a 200, default 50).
          schema:
            type: integer
        - name: offset
          in: query
          required: false
          schema:
            type: integer
      responses:
        "200":
          description: Depósitos del jugador
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerPlayerDepositsPage"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /casino/partners/{partnerSlug}/players/{ref}/profile/bets:
    get:
      tags:
        - Casino
      operationId: listCasinoPartnerPlayerBets
      summary: Apuestas deportivas de un jugador de un partner
      x-convray-runtime-status: active-casino-partners-v1
      description: >-
        Sublista paginada de apuestas deportivas fila por fila del jugador (el
        casino se guarda agregado, su detalle va en la línea de tiempo). Responde
        404 si el jugador no es del partner.
      security:
        - adminBearer: []
      parameters:
        - name: partnerSlug
          in: path
          required: true
          schema:
            type: string
        - name: ref
          in: path
          required: true
          schema:
            type: string
        - name: limit
          in: query
          required: false
          description: Tamaño de página (1 a 200, default 50).
          schema:
            type: integer
        - name: offset
          in: query
          required: false
          schema:
            type: integer
      responses:
        "200":
          description: Apuestas deportivas del jugador
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerPlayerBetsPage"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /casino/player-landing/settings:
    get:
      tags:
        - Casino
      operationId: getCasinoPlayerLandingSettings
      summary: Borrador de la landing de jugadores
      x-convray-runtime-status: active-player-landing-v1
      description: >-
        Devuelve el borrador de la Landing Casino del casino de la sesión. Si aún
        no hay fila persistida (version 0) el repositorio sintetiza los valores por
        defecto. Solo lectura.
      security:
        - adminBearer: []
      responses:
        "200":
          description: Borrador de la landing de jugadores
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PlayerLanding"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
    patch:
      tags:
        - Casino
      operationId: updateCasinoPlayerLandingSettings
      summary: Guarda el borrador de la landing de jugadores
      x-convray-runtime-status: active-player-landing-v1
      description: >-
        Guarda el borrador de la Landing Casino con candado optimista
        (expected_version obligatorio; 0 crea la landing si no existe). Si
        publication_status es 'published' publica el snapshot en la misma operación.
      security:
        - adminBearer: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdatePlayerLandingRequest"
      responses:
        "200":
          description: Borrador guardado
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PlayerLanding"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "409":
          $ref: "#/components/responses/VersionConflict"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /casino/player-landing/analytics:
    get:
      tags:
        - Casino
      operationId: getCasinoPlayerLandingAnalytics
      summary: Analítica de la landing de jugadores
      x-convray-runtime-status: active-player-landing-v1
      description: >-
        Reporte recortado de la Landing Casino: solo web (impresiones, clicks, CTR
        y desglose por surface), serie diaria y top de interacciones. Solo lectura.
      security:
        - adminBearer: []
      parameters:
        - name: period
          in: query
          required: false
          description: Clave del período a consultar.
          schema:
            type: string
      responses:
        "200":
          description: Analítica de la landing de jugadores
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PlayerLandingAnalytics"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /public/casinos/{casinoSlug}/player-landing:
    get:
      tags:
        - Casino
      operationId: getPublicCasinoPlayerLanding
      summary: Landing de jugadores publicada (pública)
      x-convray-runtime-status: active-player-landing-v1
      description: >-
        Lectura pública, sin sesión, del snapshot publicado de la Landing Casino
        que consume el frontend /juegalo/:slug. Responde 404 si no hay publicación
        vigente.
      security: []
      parameters:
        - name: casinoSlug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Landing de jugadores publicada
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PublicPlayerLanding"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
  /public/casinos/{casinoSlug}/player-landing/events:
    post:
      tags:
        - Casino
      operationId: recordPublicCasinoPlayerLandingEvent
      summary: Registra un evento de la landing de jugadores (público)
      x-convray-runtime-status: active-player-landing-v1
      description: >-
        Beacon público, sin sesión, que captura page_view/click de la Landing
        Casino. Rechaza orígenes no autorizados con 403 y deriva el visitante
        seudonimizado de una cookie. El ingreso es idempotente por event_id.
      security: []
      parameters:
        - name: casinoSlug
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PlayerLandingEventRequest"
      responses:
        "202":
          description: Evento aceptado (o reejecutado de forma idempotente)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PlayerLandingEventReceipt"
        "403":
          description: Origen no autorizado para registrar analítica
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "409":
          $ref: "#/components/responses/IdempotencyConflict"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /platform/tenants:
    get:
      tags:
        - Platform
      operationId: listPlatformTenants
      summary: Lista los casinos con contadores (SuperAdmin)
      x-convray-runtime-status: active-phase-5
      description: >-
        Devuelve los casinos con contadores de miembros activos, invitaciones
        pendientes y miembros sin rol para el módulo Personas y roles del
        super_admin. Un tenant_member recibe 403. Solo lectura.
      security:
        - adminBearer: []
      responses:
        "200":
          description: Casinos con contadores
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PlatformTenantList"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
  /casino/team/company-domains:
    get:
      tags:
        - Casino
      operationId: getCasinoTeamCompanyDomains
      summary: Lista los dominios de empresa del casino
      x-convray-runtime-status: active-casino-team
      description: >-
        Devuelve el conjunto de dominios (o emails exactos) que definen una cuenta de
        empresa del tenant. Un platform_admin puede consultar otro casino con ?tenant=.
      security:
        - adminBearer: []
      parameters:
        - name: tenant
          in: query
          required: false
          description: Slug del casino objetivo; solo lo usa un platform_admin. Vacío = el propio casino.
          schema:
            type: string
      responses:
        "200":
          description: Dominios de empresa del tenant
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CompanyDomainList"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
    put:
      tags:
        - Casino
      operationId: replaceCasinoTeamCompanyDomains
      summary: Reemplaza los dominios de empresa del casino
      x-convray-runtime-status: active-casino-team
      description: >-
        Reemplaza el conjunto completo de dominios o emails que habilitan cuenta de empresa.
        Devuelve el conjunto resultante y los emails de miembros con acceso crm/data/work que
        quedarían fuera de dominio. Un conjunto vacío levanta la restricción.
      security:
        - adminBearer: []
      parameters:
        - name: tenant
          in: query
          required: false
          description: Slug del casino objetivo; solo lo usa un platform_admin. Vacío = el propio casino.
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CasinoReplaceCompanyDomainsRequest"
      responses:
        "200":
          description: Conjunto de dominios resultante y emails afectados
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CompanyDomainReplacement"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
  /casino/team/areas:
    get:
      tags:
        - Casino
      operationId: listCasinoTeamAreas
      summary: Lista las áreas de trabajo del casino
      x-convray-runtime-status: active-casino-team
      description: Catálogo de áreas (tenant_areas) que se pueden asignar al armar accesos por producto.
      security:
        - adminBearer: []
      parameters:
        - name: tenant
          in: query
          required: false
          description: Slug del casino objetivo; solo lo usa un platform_admin. Vacío = el propio casino.
          schema:
            type: string
      responses:
        "200":
          description: Áreas del tenant
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TeamAreaList"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
  /casino/team/roles:
    get:
      tags:
        - Casino
      operationId: listCasinoTeamRoles
      summary: Lista los roles de empresa del casino
      x-convray-runtime-status: active-casino-team
      description: >-
        Catálogo de roles (tenant_roles). Cada rol empaqueta un conjunto de accesos
        {product, role, area} que se puede aplicar a un miembro.
      security:
        - adminBearer: []
      parameters:
        - name: tenant
          in: query
          required: false
          description: Slug del casino objetivo; solo lo usa un platform_admin. Vacío = el propio casino.
          schema:
            type: string
      responses:
        "200":
          description: Roles del tenant
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TeamRoleList"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
  /casino/team/invitations/{invitationID}/link:
    post:
      tags:
        - Casino
      operationId: regenerateCasinoTeamInvitationLink
      summary: Regenera el enlace de una invitación de equipo
      x-convray-runtime-status: active-casino-team
      description: >-
        Emite un token nuevo para una invitación pendiente e invalida el anterior. El body
        es opcional; si trae expires_at, extiende el vencimiento. El token en claro solo
        vuelve en la respuesta de esta operación.
      security:
        - adminBearer: []
      parameters:
        - name: invitationID
          in: path
          required: true
          schema:
            type: string
        - name: tenant
          in: query
          required: false
          description: Slug del casino objetivo; solo lo usa un platform_admin. Vacío = el propio casino.
          schema:
            type: string
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CasinoRegenerateInvitationLinkRequest"
      responses:
        "201":
          description: Invitación con el token regenerado
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TeamInvitation"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "409":
          $ref: "#/components/responses/VersionConflict"
        "422":
          $ref: "#/components/responses/ValidationFailed"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
  /casino/team/members/{membershipID}/products:
    put:
      tags:
        - Casino
      operationId: replaceCasinoTeamMemberProducts
      summary: Reemplaza los accesos por producto de un miembro
      x-convray-runtime-status: active-casino-team
      description: >-
        Reemplaza el conjunto completo de accesos {product, role, area} del miembro. Incluir
        crm/data/work para un email fuera de los dominios de empresa devuelve 422.
      security:
        - adminBearer: []
      parameters:
        - name: membershipID
          in: path
          required: true
          schema:
            type: string
        - name: tenant
          in: query
          required: false
          description: Slug del casino objetivo; solo lo usa un platform_admin. Vacío = el propio casino.
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CasinoReplaceMemberProductsRequest"
      responses:
        "200":
          description: Miembro con los accesos actualizados
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TeamMember"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "409":
          $ref: "#/components/responses/VersionConflict"
        "422":
          $ref: "#/components/responses/ValidationFailed"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
  /casino/team/members/{membershipID}/role:
    put:
      tags:
        - Casino
      operationId: assignCasinoTeamMemberRole
      summary: Asigna un rol de empresa a un miembro
      x-convray-runtime-status: active-casino-team
      description: >-
        Reemplaza los accesos del miembro por los del rol indicado y fija team_role_code.
        role_code null quita el rol: el conjunto de accesos pasa a vacío y el rol queda sin asignar.
      security:
        - adminBearer: []
      parameters:
        - name: membershipID
          in: path
          required: true
          schema:
            type: string
        - name: tenant
          in: query
          required: false
          description: Slug del casino objetivo; solo lo usa un platform_admin. Vacío = el propio casino.
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CasinoAssignMemberRoleRequest"
      responses:
        "200":
          description: Miembro con el rol asignado
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TeamMember"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "409":
          $ref: "#/components/responses/VersionConflict"
        "422":
          $ref: "#/components/responses/ValidationFailed"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
  /community:
    get:
      tags:
        - Community
      operationId: getCommunityOverview
      summary: Panorama de la comunidad del streamer
      x-convray-runtime-status: active-community-v1
      description: >-
        Devuelve el programa de puntos, la configuración de acceso y overlay, un resumen y
        las listas de fuentes, reglas, viewers y movimientos recientes de la comunidad.
      security:
        - adminBearer: []
      responses:
        "200":
          description: Panorama de la comunidad
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CommunityOverview"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
  /community/points-program:
    patch:
      tags:
        - Community
      operationId: updateCommunityPointsProgram
      summary: Actualiza el programa de puntos
      x-convray-runtime-status: active-community-v1
      description: Cambia el nombre y el símbolo del programa de puntos con bloqueo optimista por versión.
      security:
        - adminBearer: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CommunityUpdateProgramRequest"
      responses:
        "200":
          description: Programa actualizado
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CommunityProgram"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "409":
          $ref: "#/components/responses/VersionConflict"
        "422":
          $ref: "#/components/responses/ValidationFailed"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
  /community/channel-followers:
    get:
      tags:
        - Community
      operationId: getCommunityChannelFollowers
      summary: Serie diaria de nuevos seguidores de Kick
      x-convray-runtime-status: active-community-v1
      description: >-
        Serie diaria de nuevos seguidores registrados vía el webhook de Kick dentro de la
        ventana. No es el total del canal. Una ventana inválida cae en la de por defecto (30).
      security:
        - adminBearer: []
      parameters:
        - name: window_days
          in: query
          required: false
          description: Ventana en días. Valores válidos 7, 30 o 90; cualquier otro cae en 30.
          schema:
            type: integer
            enum: [7, 30, 90]
            default: 30
      responses:
        "200":
          description: Serie diaria de nuevos seguidores
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CommunityChannelFollowers"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
  /community/member-daily:
    get:
      tags:
        - Community
      operationId: getCommunityMemberDaily
      summary: Serie diaria de altas de la comunidad
      x-convray-runtime-status: active-community-v1
      description: >-
        Serie diaria de viewers que se unieron a la comunidad (Discord o Kick) dentro de la
        ventana, con ceros explícitos. Una ventana inválida cae en la de por defecto (30).
      security:
        - adminBearer: []
      parameters:
        - name: window_days
          in: query
          required: false
          description: Ventana en días. Valores válidos 7, 30 o 90; cualquier otro cae en 30.
          schema:
            type: integer
            enum: [7, 30, 90]
            default: 30
      responses:
        "200":
          description: Serie diaria de altas de miembros
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CommunityMemberDaily"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
  /community/access-settings:
    patch:
      tags:
        - Community
      operationId: updateCommunityAccessSettings
      summary: Actualiza el acceso por proveedor de la comunidad
      x-convray-runtime-status: active-community-v1
      description: Habilita o deshabilita el ingreso vía Kick y Discord con bloqueo optimista por versión.
      security:
        - adminBearer: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CommunityUpdateAccessSettingsRequest"
      responses:
        "200":
          description: Configuración de acceso actualizada
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CommunityAccessSettings"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "409":
          $ref: "#/components/responses/VersionConflict"
        "422":
          $ref: "#/components/responses/ValidationFailed"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
  /community/point-adjustments:
    post:
      tags:
        - Community
      operationId: createCommunityPointAdjustment
      summary: Ajusta el saldo de puntos de un viewer
      x-convray-runtime-status: active-community-v1
      description: >-
        Acredita o debita puntos a una suscripción activa. Es idempotente por idempotency_key:
        un reintento con la misma clave devuelve 200 con el mismo movimiento en vez de 201.
      security:
        - adminBearer: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CommunityPointAdjustmentRequest"
      responses:
        "201":
          description: Ajuste registrado
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CommunityAdjustmentResult"
        "200":
          description: Ajuste ya registrado antes con la misma idempotency_key (replay)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CommunityAdjustmentResult"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "409":
          $ref: "#/components/responses/VersionConflict"
        "422":
          $ref: "#/components/responses/ValidationFailed"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
  /giveaways/{giveawayID}/cover:
    patch:
      tags:
        - Giveaways
      operationId: updateGiveawayCover
      summary: Actualiza la portada de un sorteo
      x-convray-runtime-status: active-giveaway-foundation
      description: >-
        Cambia la portada del sorteo con bloqueo optimista por versión. Desde el runtime solo
        se admite cover_kind=gallery con una clave de la galería fija; cover_ref vacío deriva
        la portada de la descripción del premio. Un sorteo cerrado o cancelado no es editable (409).
      security:
        - adminBearer: []
      parameters:
        - name: giveawayID
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/GiveawayUpdateCoverRequest"
      responses:
        "200":
          description: Sorteo con la portada actualizada
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/GiveawayCoverResource"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "409":
          $ref: "#/components/responses/VersionConflict"
        "422":
          $ref: "#/components/responses/ValidationFailed"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
  /work/spaces:
    get:
      tags:
        - Work
      operationId: listWorkSpaces
      summary: Lista los espacios de trabajo y sus tableros
      x-convray-runtime-status: active-work-v1
      description: Devuelve los espacios (por área) con sus tableros y columnas visibles para la cuenta. Detrás de la puerta de producto 'work'; si el header X-Convray-Product no es 'work' responde 404.
      security:
        - adminBearer: []
      parameters:
        - name: tenant
          in: query
          required: false
          description: Slug del tenant. Obligatorio para platform_admin; ignorado para miembros (se usa su tenant).
          schema:
            type: string
      responses:
        "200":
          description: Espacios del tenant
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WorkSpacesResponse"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"

  /work/spaces/{id}/boards:
    post:
      tags:
        - Work
      operationId: createWorkBoard
      summary: Crea un tablero en un espacio (área)
      x-convray-runtime-status: active-work-v1
      description: >-
        Crea un tablero nuevo en el espacio (área) con la plantilla de columnas elegida (rol
        mínimo admin del producto 'work'). Detrás de la puerta de producto 'work'; si el header
        X-Convray-Product no es 'work' responde 404.
      security:
        - adminBearer: []
      parameters:
        - name: id
          in: path
          required: true
          description: Identificador del espacio (área).
          schema:
            type: string
        - name: tenant
          in: query
          required: false
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/WorkCreateBoardRequest"
      responses:
        "201":
          description: Tablero creado
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WorkBoardResponse"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"

  /work/boards/{id}:
    patch:
      tags:
        - Work
      operationId: patchWorkBoard
      summary: Renombra o archiva un tablero
      x-convray-runtime-status: active-work-v1
      description: >-
        Renombra y/o archiva/desarchiva un tablero (rol mínimo admin del producto 'work'). El
        tablero General de un área no se puede archivar. Detrás de la puerta de producto 'work';
        si el header X-Convray-Product no es 'work' responde 404.
      security:
        - adminBearer: []
      parameters:
        - name: id
          in: path
          required: true
          description: Identificador del tablero.
          schema:
            type: string
        - name: tenant
          in: query
          required: false
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/WorkPatchBoardRequest"
      responses:
        "200":
          description: Tablero actualizado
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WorkBoardResponse"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"

  /work/members:
    get:
      tags:
        - Work
      operationId: listWorkMembers
      summary: Lista los miembros con acceso a Work
      x-convray-runtime-status: active-work-v1
      description: Devuelve los miembros del tenant que pueden ser asignados a tarjetas. Detrás de la puerta de producto 'work'; si el header X-Convray-Product no es 'work' responde 404.
      security:
        - adminBearer: []
      parameters:
        - name: tenant
          in: query
          required: false
          description: Slug del tenant. Obligatorio para platform_admin.
          schema:
            type: string
      responses:
        "200":
          description: Miembros del tenant
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WorkMembersResponse"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"

  /work/boards/{id}/cards:
    get:
      tags:
        - Work
      operationId: listWorkBoardCards
      summary: Lista las tarjetas de un tablero
      x-convray-runtime-status: active-work-v1
      description: Devuelve las tarjetas del tablero, con filtros opcionales. Detrás de la puerta de producto 'work'; si el header X-Convray-Product no es 'work' responde 404.
      security:
        - adminBearer: []
      parameters:
        - name: id
          in: path
          required: true
          description: Identificador del tablero.
          schema:
            type: string
        - name: tenant
          in: query
          required: false
          schema:
            type: string
        - name: assignee
          in: query
          required: false
          description: Filtra por usuario asignado.
          schema:
            type: string
        - name: area
          in: query
          required: false
          description: Filtra por área ejecutora.
          schema:
            type: string
        - name: state
          in: query
          required: false
          description: Filtra por estado canónico de la columna.
          schema:
            type: string
        - name: q
          in: query
          required: false
          description: Búsqueda por texto en el título.
          schema:
            type: string
      responses:
        "200":
          description: Tarjetas del tablero
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WorkCardsResponse"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
    post:
      tags:
        - Work
      operationId: createWorkBoardCard
      summary: Crea una tarjeta en un tablero
      x-convray-runtime-status: active-work-v1
      description: Crea una nueva tarjeta (solicitud) en el tablero indicado. Detrás de la puerta de producto 'work'; si el header X-Convray-Product no es 'work' responde 404.
      security:
        - adminBearer: []
      parameters:
        - name: id
          in: path
          required: true
          description: Identificador del tablero.
          schema:
            type: string
        - name: tenant
          in: query
          required: false
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/WorkCreateCardRequest"
      responses:
        "201":
          description: Tarjeta creada
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WorkCardResponse"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"

  /work/cards/{id}:
    get:
      tags:
        - Work
      operationId: getWorkCard
      summary: Obtiene el detalle de una tarjeta
      x-convray-runtime-status: active-work-v1
      description: Devuelve la tarjeta con sus publicaciones y observadores. Detrás de la puerta de producto 'work'; si el header X-Convray-Product no es 'work' responde 404.
      security:
        - adminBearer: []
      parameters:
        - name: id
          in: path
          required: true
          description: Identificador de la tarjeta.
          schema:
            type: string
        - name: tenant
          in: query
          required: false
          schema:
            type: string
      responses:
        "200":
          description: Detalle de la tarjeta
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WorkCardDetail"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
    patch:
      tags:
        - Work
      operationId: patchWorkCard
      summary: Actualiza una tarjeta
      x-convray-runtime-status: active-work-v1
      description: Modifica campos de la tarjeta (título, prioridad, asignación, columna, posición, etc.). El área ejecutora no se puede cambiar. Detrás de la puerta de producto 'work'; si el header X-Convray-Product no es 'work' responde 404.
      security:
        - adminBearer: []
      parameters:
        - name: id
          in: path
          required: true
          description: Identificador de la tarjeta.
          schema:
            type: string
        - name: tenant
          in: query
          required: false
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/WorkPatchCardRequest"
      responses:
        "200":
          description: Tarjeta actualizada
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WorkCardResponse"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"

  /work/cards/{id}/posts:
    post:
      tags:
        - Work
      operationId: createWorkCardPost
      summary: Publica un comentario en una tarjeta
      x-convray-runtime-status: active-work-v1
      description: Agrega una publicación (comentario) a la tarjeta. Detrás de la puerta de producto 'work'; si el header X-Convray-Product no es 'work' responde 404.
      security:
        - adminBearer: []
      parameters:
        - name: id
          in: path
          required: true
          description: Identificador de la tarjeta.
          schema:
            type: string
        - name: tenant
          in: query
          required: false
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/WorkCreatePostRequest"
      responses:
        "201":
          description: Publicación creada
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WorkPostResponse"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"

  /work/my:
    get:
      tags:
        - Work
      operationId: listWorkMyCards
      summary: Lista mis tarjetas (asignadas, solicitadas o vencidas)
      x-convray-runtime-status: active-work-v1
      description: Devuelve las tarjetas de la cuenta según el tipo indicado. Detrás de la puerta de producto 'work'; si el header X-Convray-Product no es 'work' responde 404.
      security:
        - adminBearer: []
      parameters:
        - name: kind
          in: query
          required: true
          description: Tipo de listado.
          schema:
            type: string
            enum:
              - assigned
              - requested
              - overdue
        - name: tenant
          in: query
          required: false
          schema:
            type: string
      responses:
        "200":
          description: Mis tarjetas
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WorkCardsResponse"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"

  /tickets:
    get:
      tags:
        - Tickets
      operationId: listTickets
      summary: Lista los tickets del tenant
      x-convray-runtime-status: active-tickets-v1
      description: Devuelve los tickets con filtros opcionales y el total. Transversal (sin puerta de producto), solo requiere sesión.
      security:
        - adminBearer: []
      parameters:
        - name: tenant
          in: query
          required: false
          description: Slug del tenant. Obligatorio para platform_admin.
          schema:
            type: string
        - name: status
          in: query
          required: false
          schema:
            type: string
        - name: module
          in: query
          required: false
          schema:
            type: string
        - name: severity
          in: query
          required: false
          schema:
            type: string
        - name: assignee
          in: query
          required: false
          schema:
            type: string
        - name: mine
          in: query
          required: false
          description: "'1' para ver solo los tickets propios."
          schema:
            type: string
        - name: limit
          in: query
          required: false
          schema:
            type: integer
        - name: offset
          in: query
          required: false
          schema:
            type: integer
      responses:
        "200":
          description: Tickets del tenant
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TicketListResponse"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
    post:
      tags:
        - Tickets
      operationId: createTicket
      summary: Crea un ticket
      x-convray-runtime-status: active-tickets-v1
      description: Registra un nuevo ticket de soporte interno. Transversal (sin puerta de producto), solo requiere sesión.
      security:
        - adminBearer: []
      parameters:
        - name: tenant
          in: query
          required: false
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TicketCreateRequest"
      responses:
        "201":
          description: Ticket creado
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TicketResponse"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"

  /tickets/stats:
    get:
      tags:
        - Tickets
      operationId: getTicketStats
      summary: Devuelve el resumen de tickets
      x-convray-runtime-status: active-tickets-v1
      description: Conteos por estado y severidad, total abierto y total general. Transversal (sin puerta de producto), solo requiere sesión.
      security:
        - adminBearer: []
      parameters:
        - name: tenant
          in: query
          required: false
          schema:
            type: string
      responses:
        "200":
          description: Resumen de tickets
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TicketStatsResponse"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "404":
          $ref: "#/components/responses/ResourceNotFound"

  /tickets/{id}:
    get:
      tags:
        - Tickets
      operationId: getTicket
      summary: Obtiene un ticket
      x-convray-runtime-status: active-tickets-v1
      description: Devuelve el detalle del ticket. Los campos van en la raíz y también anidados en 'ticket' por tolerancia. Transversal (sin puerta de producto), solo requiere sesión.
      security:
        - adminBearer: []
      parameters:
        - name: id
          in: path
          required: true
          description: Identificador del ticket.
          schema:
            type: string
        - name: tenant
          in: query
          required: false
          schema:
            type: string
      responses:
        "200":
          description: Detalle del ticket
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TicketDetail"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
    patch:
      tags:
        - Tickets
      operationId: patchTicket
      summary: Actualiza un ticket
      x-convray-runtime-status: active-tickets-v1
      description: Modifica título, estado, severidad o asignación (assignee_user_id null lo desasigna). Transversal (sin puerta de producto), solo requiere sesión.
      security:
        - adminBearer: []
      parameters:
        - name: id
          in: path
          required: true
          description: Identificador del ticket.
          schema:
            type: string
        - name: tenant
          in: query
          required: false
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TicketPatchRequest"
      responses:
        "200":
          description: Ticket actualizado
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TicketResponse"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"

  /tickets/{id}/comments:
    get:
      tags:
        - Tickets
      operationId: listTicketComments
      summary: Lista los comentarios de un ticket
      x-convray-runtime-status: active-tickets-v1
      description: Devuelve los comentarios del ticket en orden. Transversal (sin puerta de producto), solo requiere sesión.
      security:
        - adminBearer: []
      parameters:
        - name: id
          in: path
          required: true
          description: Identificador del ticket.
          schema:
            type: string
        - name: tenant
          in: query
          required: false
          schema:
            type: string
      responses:
        "200":
          description: Comentarios del ticket
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TicketCommentsResponse"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
    post:
      tags:
        - Tickets
      operationId: createTicketComment
      summary: Agrega un comentario a un ticket
      x-convray-runtime-status: active-tickets-v1
      description: Publica un comentario en el ticket. Transversal (sin puerta de producto), solo requiere sesión.
      security:
        - adminBearer: []
      parameters:
        - name: id
          in: path
          required: true
          description: Identificador del ticket.
          schema:
            type: string
        - name: tenant
          in: query
          required: false
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TicketCommentCreateRequest"
      responses:
        "201":
          description: Comentario creado
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TicketCommentResponse"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"

  /assistant/status:
    get:
      tags:
        - Assistant
      operationId: getAssistantStatus
      summary: Estado y presupuesto del asistente
      x-convray-runtime-status: active-assistant-v1
      description: Indica si el asistente está configurado, su nombre, proveedor, modelo y el consumo de presupuesto. Solo para cuentas de empresa de un casino. Transversal (sin puerta de producto). Nota; los errores se emiten como JSON { code, message }, no como problem+json.
      security:
        - adminBearer: []
      parameters:
        - name: tenant
          in: query
          required: false
          schema:
            type: string
      responses:
        "200":
          description: Estado del asistente
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AssistantStatusResponse"
        "401":
          description: Sesión inválida
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AssistantError"
        "403":
          description: La cuenta no tiene acceso al asistente
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AssistantError"
        "404":
          description: Recurso no encontrado
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AssistantError"
        "503":
          description: El asistente no está configurado
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AssistantError"

  /integrations:
    get:
      tags:
        - Integrations
      operationId: listIntegrations
      summary: "Integraciones de plataforma (solo super_admin)"
      x-convray-runtime-status: active-integrations-v1
      description: "Tarjetas de SendGrid, Twilio SMS y Telegram con su estado (connected, error, off, unconfigured). El secreto nunca se devuelve; solo secret_last4, fecha y autor."
      security:
        - adminBearer: []
      parameters:
        - name: tenant
          in: query
          required: true
          description: Slug del casino sobre el que opera la sesión de plataforma (super_admin). Sin él la API responde 404.
          schema:
            type: string
      responses:
        "200":
          description: "Integraciones"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationsResponse"
        "401":
          description: "Sesión inválida"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationError"
        "403":
          description: "La cuenta no es super_admin"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationError"
        "404":
          description: "No existe"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationError"

  /integrations/audit:
    get:
      tags:
        - Integrations
      operationId: listIntegrationAudit
      summary: "Registro de cambios de integraciones (solo super_admin)"
      x-convray-runtime-status: active-integrations-v1
      description: "Últimos cambios (quién, cuándo, qué). Nunca incluye secretos."
      security:
        - adminBearer: []
      parameters:
        - name: tenant
          in: query
          required: true
          description: Slug del casino sobre el que opera la sesión de plataforma (super_admin). Sin él la API responde 404.
          schema:
            type: string
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 200
      responses:
        "200":
          description: "Registro de cambios"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationAuditResponse"
        "401":
          description: "Sesión inválida"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationError"
        "403":
          description: "La cuenta no es super_admin"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationError"
        "404":
          description: "No existe"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationError"

  /integrations/{code}:
    put:
      tags:
        - Integrations
      operationId: updateIntegration
      summary: "Actualiza una integración (solo super_admin)"
      x-convray-runtime-status: active-integrations-v1
      description: "Guarda la configuración no secreta y, si viene, el secreto (cifrado AES-256-GCM, de solo escritura). secret omitido conserva el guardado; un campo de config vacío lo borra. Un secreto distinto, o un cambio en from_email (SendGrid) o en los SID (Twilio), invalida la prueba: si la integración estaba activa queda en pausa (no envía ni descarta nada) hasta que una prueba correcta la valide. marketing_enabled solo aplica a twilio_sms activo."
      security:
        - adminBearer: []
      parameters:
        - name: tenant
          in: query
          required: true
          description: Slug del casino sobre el que opera la sesión de plataforma (super_admin). Sin él la API responde 404.
          schema:
            type: string
        - name: code
          in: path
          required: true
          schema:
            type: string
            enum: [sendgrid, twilio_sms, telegram]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/IntegrationUpdateRequest"
      responses:
        "200":
          description: "Integración actualizada"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Integration"
        "401":
          description: "Sesión inválida"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationError"
        "403":
          description: "La cuenta no es super_admin"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationError"
        "404":
          description: "No existe"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationError"
        "422":
          description: "Campos inválidos (validation_failed con fields)"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationValidationError"
        "503":
          description: "Falta la llave maestra de cifrado (integrations_encryption_unavailable)"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationError"

  /integrations/{code}/test:
    post:
      tags:
        - Integrations
      operationId: testIntegration
      summary: "Prueba una integración (solo super_admin)"
      x-convray-runtime-status: active-integrations-v1
      description: "Prueba el secreto guardado contra el proveedor: Telegram getMe (guarda el usuario del bot); SendGrid permiso mail.send y un correo de prueba real desde el remitente configurado a `to` (obligatorio, valida el remitente); Twilio cuenta y Messaging Service, y un SMS de prueba si viene `to`. Responde 200 con ok true/false y un mensaje en lenguaje claro que nunca incluye el secreto. El resultado solo se registra si la integración no cambió durante la prueba."
      security:
        - adminBearer: []
      parameters:
        - name: tenant
          in: query
          required: true
          description: Slug del casino sobre el que opera la sesión de plataforma (super_admin). Sin él la API responde 404.
          schema:
            type: string
        - name: code
          in: path
          required: true
          schema:
            type: string
            enum: [sendgrid, twilio_sms, telegram]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/IntegrationTestRequest"
      responses:
        "200":
          description: "Resultado de la prueba"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationTestResult"
        "401":
          description: "Sesión inválida"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationError"
        "403":
          description: "La cuenta no es super_admin"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationError"
        "404":
          description: "No existe"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationError"
        "409":
          description: "Conflicto (falta el secreto, una prueba reciente o el grupo vinculado; nombre repetido)"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationError"
        "422":
          description: "Campos inválidos (validation_failed con fields)"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationValidationError"
        "502":
          description: "El proveedor rechazó la operación (provider_error)"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationError"
        "503":
          description: "Falta la llave maestra de cifrado (integrations_encryption_unavailable)"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationError"

  /integrations/{code}/enable:
    post:
      tags:
        - Integrations
      operationId: enableIntegration
      summary: "Activa una integración (solo super_admin)"
      x-convray-runtime-status: active-integrations-v1
      description: "Exige secreto, configuración completa y una prueba exitosa en los últimos 30 minutos. 502 si el secreto guardado no se puede descifrar (hay que volver a cargarlo); 503 si falta la llave maestra."
      security:
        - adminBearer: []
      parameters:
        - name: tenant
          in: query
          required: true
          description: Slug del casino sobre el que opera la sesión de plataforma (super_admin). Sin él la API responde 404.
          schema:
            type: string
        - name: code
          in: path
          required: true
          schema:
            type: string
            enum: [sendgrid, twilio_sms, telegram]
      responses:
        "200":
          description: "Integración activa"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Integration"
        "401":
          description: "Sesión inválida"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationError"
        "403":
          description: "La cuenta no es super_admin"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationError"
        "404":
          description: "No existe"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationError"
        "409":
          description: "Conflicto (falta el secreto, una prueba reciente o el grupo vinculado; nombre repetido)"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationError"
        "422":
          description: "Campos inválidos (validation_failed con fields)"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationValidationError"
        "502":
          description: "El proveedor rechazó la operación (provider_error)"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationError"
        "503":
          description: "Falta la llave maestra de cifrado (integrations_encryption_unavailable)"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationError"

  /integrations/{code}/disable:
    post:
      tags:
        - Integrations
      operationId: disableIntegration
      summary: "Apaga una integración (solo super_admin)"
      x-convray-runtime-status: active-integrations-v1
      description: "Apagado inmediato. Con Telegram apagado, las alertas nuevas quedan omitidas (telegram_off) y no se acumulan."
      security:
        - adminBearer: []
      parameters:
        - name: tenant
          in: query
          required: true
          description: Slug del casino sobre el que opera la sesión de plataforma (super_admin). Sin él la API responde 404.
          schema:
            type: string
        - name: code
          in: path
          required: true
          schema:
            type: string
            enum: [sendgrid, twilio_sms, telegram]
      responses:
        "200":
          description: "Integración apagada"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Integration"
        "401":
          description: "Sesión inválida"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationError"
        "403":
          description: "La cuenta no es super_admin"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationError"
        "404":
          description: "No existe"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationError"

  /integrations/{code}/secret:
    delete:
      tags:
        - Integrations
      operationId: deleteIntegrationSecret
      summary: "Borra el secreto y apaga la integración (solo super_admin)"
      x-convray-runtime-status: active-integrations-v1
      description: "Borra el secreto guardado, apaga la integración y limpia la última prueba."
      security:
        - adminBearer: []
      parameters:
        - name: tenant
          in: query
          required: true
          description: Slug del casino sobre el que opera la sesión de plataforma (super_admin). Sin él la API responde 404.
          schema:
            type: string
        - name: code
          in: path
          required: true
          schema:
            type: string
            enum: [sendgrid, twilio_sms, telegram]
      responses:
        "200":
          description: "Integración sin secreto"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Integration"
        "401":
          description: "Sesión inválida"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationError"
        "403":
          description: "La cuenta no es super_admin"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationError"
        "404":
          description: "No existe"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationError"

  /alert-teams:
    get:
      tags:
        - Integrations
      operationId: listAlertTeams
      summary: "Equipos de alertas (solo super_admin)"
      x-convray-runtime-status: active-integrations-v1
      description: "Equipos con su estado de vinculación (pending, linked, unlinked), título del grupo y horario silencioso."
      security:
        - adminBearer: []
      parameters:
        - name: tenant
          in: query
          required: true
          description: Slug del casino sobre el que opera la sesión de plataforma (super_admin). Sin él la API responde 404.
          schema:
            type: string
      responses:
        "200":
          description: "Equipos"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AlertTeamsResponse"
        "401":
          description: "Sesión inválida"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationError"
        "403":
          description: "La cuenta no es super_admin"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationError"
        "404":
          description: "No existe"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationError"
    post:
      tags:
        - Integrations
      operationId: createAlertTeam
      summary: "Crea un equipo de alertas (solo super_admin)"
      x-convray-runtime-status: active-integrations-v1
      description: "Crea el equipo sin grupo vinculado. El nombre es único sin distinguir mayúsculas."
      security:
        - adminBearer: []
      parameters:
        - name: tenant
          in: query
          required: true
          description: Slug del casino sobre el que opera la sesión de plataforma (super_admin). Sin él la API responde 404.
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AlertTeamRequest"
      responses:
        "201":
          description: "Equipo creado"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AlertTeam"
        "401":
          description: "Sesión inválida"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationError"
        "403":
          description: "La cuenta no es super_admin"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationError"
        "404":
          description: "No existe"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationError"
        "409":
          description: "Conflicto (falta el secreto, una prueba reciente o el grupo vinculado; nombre repetido)"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationError"
        "422":
          description: "Campos inválidos (validation_failed con fields)"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationValidationError"

  /alert-teams/{id}:
    patch:
      tags:
        - Integrations
      operationId: updateAlertTeam
      summary: "Edita un equipo de alertas (solo super_admin)"
      x-convray-runtime-status: active-integrations-v1
      description: "Cambia nombre y horario silencioso (HH:MM en hora de Santiago; vacío = sin silencio). Las alertas críticas ignoran el silencio."
      security:
        - adminBearer: []
      parameters:
        - name: tenant
          in: query
          required: true
          description: Slug del casino sobre el que opera la sesión de plataforma (super_admin). Sin él la API responde 404.
          schema:
            type: string
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AlertTeamRequest"
      responses:
        "200":
          description: "Equipo actualizado"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AlertTeam"
        "401":
          description: "Sesión inválida"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationError"
        "403":
          description: "La cuenta no es super_admin"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationError"
        "404":
          description: "No existe"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationError"
        "409":
          description: "Conflicto (falta el secreto, una prueba reciente o el grupo vinculado; nombre repetido)"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationError"
        "422":
          description: "Campos inválidos (validation_failed con fields)"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationValidationError"
    delete:
      tags:
        - Integrations
      operationId: deleteAlertTeam
      summary: "Borra un equipo de alertas (solo super_admin)"
      x-convray-runtime-status: active-integrations-v1
      description: "Borra el equipo con sus rutas e historial."
      security:
        - adminBearer: []
      parameters:
        - name: tenant
          in: query
          required: true
          description: Slug del casino sobre el que opera la sesión de plataforma (super_admin). Sin él la API responde 404.
          schema:
            type: string
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "204":
          description: "Equipo borrado"
        "401":
          description: "Sesión inválida"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationError"
        "403":
          description: "La cuenta no es super_admin"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationError"
        "404":
          description: "No existe"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationError"

  /alert-teams/{id}/link-code:
    post:
      tags:
        - Integrations
      operationId: createAlertTeamLinkCode
      summary: "Enlace de vinculación de un grupo (solo super_admin)"
      x-convray-runtime-status: active-integrations-v1
      description: "Genera un código de un solo uso que vence en 15 minutos y el enlace https://t.me/<bot>?startgroup=<código>. Al agregar el bot al grupo con ese enlace, el grupo queda vinculado al equipo. Requiere haber probado el bot."
      security:
        - adminBearer: []
      parameters:
        - name: tenant
          in: query
          required: true
          description: Slug del casino sobre el que opera la sesión de plataforma (super_admin). Sin él la API responde 404.
          schema:
            type: string
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: "Enlace de vinculación"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AlertTeamLinkCode"
        "401":
          description: "Sesión inválida"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationError"
        "403":
          description: "La cuenta no es super_admin"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationError"
        "404":
          description: "No existe"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationError"
        "502":
          description: "El proveedor rechazó la operación (provider_error)"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationError"

  /alert-teams/{id}/test:
    post:
      tags:
        - Integrations
      operationId: testAlertTeam
      summary: "Mensaje de prueba al grupo de un equipo (solo super_admin)"
      x-convray-runtime-status: active-integrations-v1
      description: "Envía un mensaje de prueba al grupo vinculado. Responde 200 con ok true/false."
      security:
        - adminBearer: []
      parameters:
        - name: tenant
          in: query
          required: true
          description: Slug del casino sobre el que opera la sesión de plataforma (super_admin). Sin él la API responde 404.
          schema:
            type: string
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: "Resultado del envío"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationTestResult"
        "401":
          description: "Sesión inválida"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationError"
        "403":
          description: "La cuenta no es super_admin"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationError"
        "404":
          description: "No existe"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationError"
        "409":
          description: "Conflicto (falta el secreto, una prueba reciente o el grupo vinculado; nombre repetido)"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationError"
        "502":
          description: "El proveedor rechazó la operación (provider_error)"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationError"
        "503":
          description: "Falta la llave maestra de cifrado (integrations_encryption_unavailable)"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationError"

  /alert-routes:
    get:
      tags:
        - Integrations
      operationId: getAlertRoutes
      summary: "Rutas de alertas (solo super_admin)"
      x-convray-runtime-status: active-integrations-v1
      description: "Catálogo de tipos de alerta, equipos y las rutas activas tipo × equipo."
      security:
        - adminBearer: []
      parameters:
        - name: tenant
          in: query
          required: true
          description: Slug del casino sobre el que opera la sesión de plataforma (super_admin). Sin él la API responde 404.
          schema:
            type: string
      responses:
        "200":
          description: "Matriz de rutas"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AlertRoutesResponse"
        "401":
          description: "Sesión inválida"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationError"
        "403":
          description: "La cuenta no es super_admin"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationError"
        "404":
          description: "No existe"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationError"
    put:
      tags:
        - Integrations
      operationId: putAlertRoutes
      summary: "Reemplaza las rutas de alertas (solo super_admin)"
      x-convray-runtime-status: active-integrations-v1
      description: "Reemplaza la matriz completa de rutas tipo de alerta × equipo. routes es obligatorio (usa [] para dejarla vacía); un equipo inexistente responde 422."
      security:
        - adminBearer: []
      parameters:
        - name: tenant
          in: query
          required: true
          description: Slug del casino sobre el que opera la sesión de plataforma (super_admin). Sin él la API responde 404.
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AlertRoutesRequest"
      responses:
        "200":
          description: "Matriz de rutas"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AlertRoutesResponse"
        "401":
          description: "Sesión inválida"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationError"
        "403":
          description: "La cuenta no es super_admin"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationError"
        "404":
          description: "No existe"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationError"
        "422":
          description: "Campos inválidos (validation_failed con fields)"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationValidationError"

  /alert-deliveries:
    get:
      tags:
        - Integrations
      operationId: listAlertDeliveries
      summary: "Historial de envíos a Telegram (solo super_admin)"
      x-convray-runtime-status: active-integrations-v1
      description: "Envíos por equipo, más reciente primero: sent, grouped (agrupada en un solo mensaje), skipped (telegram_off, team_unlinked, quiet_hours, stale), failed o pending."
      security:
        - adminBearer: []
      parameters:
        - name: tenant
          in: query
          required: true
          description: Slug del casino sobre el que opera la sesión de plataforma (super_admin). Sin él la API responde 404.
          schema:
            type: string
        - name: from
          in: query
          required: false
          schema:
            type: string
            format: date-time
        - name: to
          in: query
          required: false
          schema:
            type: string
            format: date-time
        - name: team
          in: query
          required: false
          schema:
            type: string
            format: uuid
        - name: status
          in: query
          required: false
          schema:
            type: string
            enum: [pending, sent, grouped, skipped, failed]
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 500
      responses:
        "200":
          description: "Historial"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AlertDeliveriesResponse"
        "401":
          description: "Sesión inválida"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationError"
        "403":
          description: "La cuenta no es super_admin"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationError"
        "404":
          description: "No existe"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationError"
        "422":
          description: "Campos inválidos (validation_failed con fields)"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationValidationError"

  /assistant/settings:
    get:
      tags:
        - Assistant
      operationId: getAssistantSettings
      summary: Configuración de Ray (solo super_admin)
      x-convray-runtime-status: active-assistant-v1
      description: Devuelve la configuración efectiva de Ray y su origen (database | environment | none). La llave nunca se devuelve; solo last4, fecha y autor. Solo super_admin. Errores como JSON { code, message }.
      security:
        - adminBearer: []
      parameters:
        - name: tenant
          in: query
          required: false
          schema:
            type: string
      responses:
        "200":
          description: Configuración de Ray
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AssistantSettingsResponse"
        "401":
          description: Sesión inválida
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AssistantError"
        "403":
          description: La cuenta no es super_admin
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AssistantError"
    put:
      tags:
        - Assistant
      operationId: updateAssistantSettings
      summary: Actualiza la configuración de Ray (solo super_admin)
      x-convray-runtime-status: active-assistant-v1
      description: Crea o actualiza la fila única de configuración. api_key omitido conserva la llave; string vacío devuelve 400. Activar exige proveedor, modelo, llave y precios > 0 (regla 6). Devuelve el mismo cuerpo que GET.
      security:
        - adminBearer: []
      parameters:
        - name: tenant
          in: query
          required: false
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AssistantSettingsUpdateRequest"
      responses:
        "200":
          description: Configuración actualizada
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AssistantSettingsResponse"
        "400":
          description: Llave vacía (para quitarla usar DELETE)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AssistantError"
        "401":
          description: Sesión inválida
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AssistantError"
        "403":
          description: La cuenta no es super_admin
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AssistantError"
        "422":
          description: Configuración inválida (assistant_settings_invalid con fields)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AssistantSettingsInvalidError"
        "503":
          description: Falta la llave maestra de cifrado (assistant_settings_encryption_unavailable)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AssistantError"

  /assistant/settings/api-key:
    delete:
      tags:
        - Assistant
      operationId: deleteAssistantApiKey
      summary: Borra la llave de API y apaga Ray (solo super_admin)
      x-convray-runtime-status: active-assistant-v1
      description: Borra la llave guardada y deja enabled=false. Solo super_admin.
      security:
        - adminBearer: []
      parameters:
        - name: tenant
          in: query
          required: false
          schema:
            type: string
      responses:
        "204":
          description: Llave borrada
        "401":
          description: Sesión inválida
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AssistantError"
        "403":
          description: La cuenta no es super_admin
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AssistantError"
        "404":
          description: No hay configuración
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AssistantError"

  /assistant/settings/test:
    post:
      tags:
        - Assistant
      operationId: testAssistantConnection
      summary: Prueba la conexión con el proveedor (solo super_admin)
      x-convray-runtime-status: active-assistant-v1
      description: Hace una llamada mínima (1 token de salida, timeout 15 s). Sin api_key usa la guardada. Responde 200 siempre; ok true/false con error_code (unauthorized | model_not_found | timeout | provider_error). El mensaje nunca incluye la llave.
      security:
        - adminBearer: []
      parameters:
        - name: tenant
          in: query
          required: false
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AssistantTestConnectionRequest"
      responses:
        "200":
          description: Resultado de la prueba
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AssistantTestConnectionResponse"
        "401":
          description: Sesión inválida
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AssistantError"
        "403":
          description: La cuenta no es super_admin
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AssistantError"

  /assistant/settings/audit:
    get:
      tags:
        - Assistant
      operationId: getAssistantSettingsAudit
      summary: Registro de cambios de configuración (solo super_admin)
      x-convray-runtime-status: active-assistant-v1
      description: Lista cronológica de cambios (quién, cuándo, qué). Nunca incluye la llave. Solo super_admin.
      security:
        - adminBearer: []
      parameters:
        - name: tenant
          in: query
          required: false
          schema:
            type: string
        - name: limit
          in: query
          required: false
          schema:
            type: integer
      responses:
        "200":
          description: Registro de cambios
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AssistantAuditResponse"
        "401":
          description: Sesión inválida
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AssistantError"
        "403":
          description: La cuenta no es super_admin
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AssistantError"

  /assistant/budgets:
    get:
      tags:
        - Assistant
      operationId: getAssistantBudgets
      summary: Resumen de topes de gasto (solo super_admin)
      x-convray-runtime-status: active-assistant-v1
      description: Gasto y topes de plataforma, por departamento y por usuario del tenant. Solo super_admin.
      security:
        - adminBearer: []
      parameters:
        - name: tenant
          in: query
          required: false
          schema:
            type: string
      responses:
        "200":
          description: Resumen de topes
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AssistantBudgetsResponse"
        "401":
          description: Sesión inválida
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AssistantError"
        "403":
          description: La cuenta no es super_admin
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AssistantError"

  /assistant/budgets/departments/{code}:
    put:
      tags:
        - Assistant
      operationId: updateAssistantDepartmentBudget
      summary: Fija el tope de un departamento (solo super_admin)
      x-convray-runtime-status: active-assistant-v1
      description: monthly_cap_usd null vuelve a la base; shares_with comparte el tope con otra área (sin cadenas). Devuelve el resumen actualizado.
      security:
        - adminBearer: []
      parameters:
        - name: code
          in: path
          required: true
          schema:
            type: string
        - name: tenant
          in: query
          required: false
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AssistantDepartmentBudgetUpdateRequest"
      responses:
        "200":
          description: Resumen actualizado
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AssistantBudgetsResponse"
        "401":
          description: Sesión inválida
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AssistantError"
        "403":
          description: La cuenta no es super_admin
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AssistantError"
        "404":
          description: El área no existe
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AssistantError"
        "422":
          description: Cadena o compartir consigo misma
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AssistantSettingsInvalidError"

  /assistant/budgets/users/{userId}:
    put:
      tags:
        - Assistant
      operationId: updateAssistantUserBudget
      summary: Fija la excepción de tope de un usuario (solo super_admin)
      x-convray-runtime-status: active-assistant-v1
      description: monthly_cap_usd null borra la excepción (vuelve a la base). Devuelve el resumen actualizado.
      security:
        - adminBearer: []
      parameters:
        - name: userId
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: tenant
          in: query
          required: false
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AssistantUserBudgetUpdateRequest"
      responses:
        "200":
          description: Resumen actualizado
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AssistantBudgetsResponse"
        "401":
          description: Sesión inválida
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AssistantError"
        "403":
          description: La cuenta no es super_admin
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AssistantError"
        "404":
          description: El usuario no existe
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AssistantError"
        "422":
          description: Tope fuera de rango
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AssistantSettingsInvalidError"

  /assistant/conversations:
    get:
      tags:
        - Assistant
      operationId: listAssistantConversations
      summary: Lista las conversaciones del asistente
      x-convray-runtime-status: active-assistant-v1
      description: Devuelve las conversaciones de la cuenta con paginación por cursor. Solo para cuentas de empresa. Los errores se emiten como JSON { code, message }.
      security:
        - adminBearer: []
      parameters:
        - name: tenant
          in: query
          required: false
          schema:
            type: string
        - name: limit
          in: query
          required: false
          schema:
            type: integer
        - name: cursor
          in: query
          required: false
          schema:
            type: string
      responses:
        "200":
          description: Conversaciones de la cuenta
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AssistantConversationListResponse"
        "401":
          description: Sesión inválida
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AssistantError"
        "403":
          description: La cuenta no tiene acceso al asistente
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AssistantError"
    post:
      tags:
        - Assistant
      operationId: createAssistantConversation
      summary: Crea una conversación
      x-convray-runtime-status: active-assistant-v1
      description: Crea una nueva conversación del asistente. Solo para cuentas de empresa. Los errores se emiten como JSON { code, message }.
      security:
        - adminBearer: []
      parameters:
        - name: tenant
          in: query
          required: false
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AssistantCreateConversationRequest"
      responses:
        "201":
          description: Conversación creada
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AssistantConversationCreated"
        "401":
          description: Sesión inválida
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AssistantError"
        "403":
          description: La cuenta no tiene acceso al asistente
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AssistantError"
        "409":
          description: Se alcanzó el tope de conversaciones activas propias (assistant_conversation_limit); elimina una para empezar otra
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AssistantError"
        "422":
          description: Datos inválidos
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AssistantError"

  /assistant/conversations/{id}:
    get:
      tags:
        - Assistant
      operationId: getAssistantConversation
      summary: Obtiene una conversación con sus mensajes
      x-convray-runtime-status: active-assistant-v1
      description: Devuelve la conversación y su historial de mensajes. Solo para cuentas de empresa. Los errores se emiten como JSON { code, message }.
      security:
        - adminBearer: []
      parameters:
        - name: id
          in: path
          required: true
          description: Identificador de la conversación.
          schema:
            type: string
        - name: tenant
          in: query
          required: false
          schema:
            type: string
      responses:
        "200":
          description: Detalle de la conversación
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AssistantConversationDetail"
        "401":
          description: Sesión inválida
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AssistantError"
        "403":
          description: La cuenta no tiene acceso al asistente
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AssistantError"
        "404":
          description: Conversación no encontrada
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AssistantError"
    patch:
      tags:
        - Assistant
      operationId: patchAssistantConversation
      summary: Renombra o archiva una conversación
      x-convray-runtime-status: active-assistant-v1
      description: Actualiza el título o el estado de archivado de la conversación. Solo para cuentas de empresa. Los errores se emiten como JSON { code, message }.
      security:
        - adminBearer: []
      parameters:
        - name: id
          in: path
          required: true
          description: Identificador de la conversación.
          schema:
            type: string
        - name: tenant
          in: query
          required: false
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AssistantPatchConversationRequest"
      responses:
        "200":
          description: Conversación actualizada
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AssistantConversationSummary"
        "401":
          description: Sesión inválida
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AssistantError"
        "403":
          description: La cuenta no tiene acceso al asistente
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AssistantError"
        "404":
          description: Conversación no encontrada
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AssistantError"
        "422":
          description: Datos inválidos
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AssistantError"

  /assistant/conversations/{id}/messages/{messageId}/data.csv:
    get:
      tags:
        - Assistant
      operationId: downloadAssistantAnswerCsv
      summary: Descarga en CSV los datos de una respuesta de Ray
      x-convray-runtime-status: active-assistant-v1
      description: >-
        CSV con la fuente, los parámetros (período, grupos y filtros) y las cifras sin formato de cada
        consulta que Ray hizo para esa respuesta, más las tablas de la respuesta. Solo el dueño de la
        conversación. Queda registrado en Data · Exportaciones a nombre del usuario, con dataset
        'assistant' y via 'assistant' (hecho con Ray).
      security:
        - adminBearer: []
      parameters:
        - name: id
          in: path
          required: true
          description: Identificador de la conversación.
          schema:
            type: string
        - name: messageId
          in: path
          required: true
          description: Identificador de la respuesta del asistente.
          schema:
            type: string
        - name: tenant
          in: query
          required: false
          schema:
            type: string
      responses:
        "200":
          description: CSV en UTF-8 con BOM
          content:
            text/csv:
              schema:
                type: string
        "401":
          description: Sesión inválida
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AssistantError"
        "404":
          description: Conversación o respuesta no encontrada
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AssistantError"
        "422":
          description: La respuesta no usó datos de Data
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AssistantError"
  /assistant/export.csv:
    get:
      tags:
        - Assistant
      operationId: downloadAssistantExportCsv
      summary: Descarga el export completo que preparó Ray
      x-convray-runtime-status: active-assistant-v1
      description: >-
        Export CSV completo de un listado de Data (depósitos, retiros, apuestas deportivas, juegos de
        casino, jugadores, Recuperación VIP o totales por jugador) con los filtros que Ray preparó con
        preparar_export. El servidor re-valida el descriptor y llama al mismo export de la pantalla con la
        sesión del usuario: aplica sus permisos y reglas, y lo registra en Data · Exportaciones a su nombre
        con via 'assistant'.
      security:
        - adminBearer: []
      parameters:
        - name: d
          in: query
          required: true
          description: Descriptor del export (base64url de JSON con endpoint, filtros, filas y etiqueta).
          schema:
            type: string
        - name: tenant
          in: query
          required: false
          schema:
            type: string
      responses:
        "200":
          description: CSV del listado de Data
          content:
            text/csv:
              schema:
                type: string
        "401":
          description: Sesión inválida
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AssistantError"
        "422":
          description: Descriptor inválido o filtros rechazados por Data
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AssistantError"
        "503":
          description: El export desde el asistente no está disponible
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AssistantError"
  /assistant/conversations/{id}/messages:
    post:
      tags:
        - Assistant
      operationId: postAssistantMessage
      summary: Envía un mensaje y recibe la respuesta por streaming
      x-convray-runtime-status: active-assistant-v1
      description: Envía un mensaje a la conversación. Tras un preflight (que puede fallar con 403/404/422/429/503 en JSON), abre un flujo Server-Sent Events (text/event-stream) con eventos message_start, token, tool_call, tool_result, ticket_draft, done y error. Solo para cuentas de empresa.
      security:
        - adminBearer: []
      parameters:
        - name: id
          in: path
          required: true
          description: Identificador de la conversación.
          schema:
            type: string
        - name: tenant
          in: query
          required: false
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AssistantPostMessageRequest"
      responses:
        "200":
          description: Flujo de eventos SSE con la respuesta del asistente
          content:
            text/event-stream:
              schema:
                type: string
        "401":
          description: Sesión inválida
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AssistantError"
        "403":
          description: La cuenta no tiene acceso al asistente
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AssistantError"
        "404":
          description: Conversación no encontrada
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AssistantError"
        "422":
          description: Datos inválidos
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AssistantError"
        "429":
          description: Se alcanzó el tope de gasto o el límite de mensajes por hora
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AssistantError"
        "503":
          description: El asistente no está configurado
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AssistantError"

  /notifications:
    get:
      tags:
        - Notifications
      operationId: listNotifications
      summary: Lista las notificaciones de la cuenta
      x-convray-runtime-status: active-notify-v1
      description: Devuelve las notificaciones visibles (no eliminadas) y el conteo de no leídas; las eliminadas con dismiss-all no se listan ni cuentan. Transversal (sin puerta de producto), solo requiere sesión.
      security:
        - adminBearer: []
      parameters:
        - name: tenant
          in: query
          required: false
          description: Slug del tenant. Obligatorio para platform_admin.
          schema:
            type: string
        - name: unread
          in: query
          required: false
          description: "'1' para devolver solo las no leídas."
          schema:
            type: string
        - name: limit
          in: query
          required: false
          schema:
            type: integer
      responses:
        "200":
          description: Notificaciones de la cuenta
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/NotificationListResponse"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"

  /notifications/{id}/read:
    post:
      tags:
        - Notifications
      operationId: markNotificationRead
      summary: Marca una notificación como leída
      x-convray-runtime-status: active-notify-v1
      description: Marca la notificación indicada como leída. Transversal (sin puerta de producto), solo requiere sesión.
      security:
        - adminBearer: []
      parameters:
        - name: id
          in: path
          required: true
          description: Identificador de la notificación.
          schema:
            type: string
        - name: tenant
          in: query
          required: false
          schema:
            type: string
      responses:
        "204":
          description: Notificación marcada como leída
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"

  /notifications/read-all:
    post:
      tags:
        - Notifications
      operationId: markAllNotificationsRead
      summary: Marca todas las notificaciones como leídas
      x-convray-runtime-status: active-notify-v1
      description: Marca como leídas todas las notificaciones de la cuenta y devuelve cuántas se actualizaron. Transversal (sin puerta de producto), solo requiere sesión.
      security:
        - adminBearer: []
      parameters:
        - name: tenant
          in: query
          required: false
          schema:
            type: string
      responses:
        "200":
          description: Cantidad de notificaciones marcadas
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/NotificationMarkAllResponse"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"

  /notifications/dismiss-all:
    post:
      tags:
        - Notifications
      operationId: dismissAllNotifications
      summary: Elimina todas las notificaciones de la bandeja
      x-convray-runtime-status: active-notify-v1
      description: >-
        Elimina de la bandeja todas las notificaciones visibles de la cuenta en
        el tenant, leídas o no, y devuelve cuántas se eliminaron. Es un borrado
        lógico: dejan de listarse y de contar como no leídas, pero no se marcan
        como leídas ni se borran del registro. Solo alcanza las notificaciones
        propias del tenant de la sesión (o del indicado por platform_admin).
        Transversal (sin puerta de producto), solo requiere sesión.
      security:
        - adminBearer: []
      parameters:
        - name: tenant
          in: query
          required: false
          description: Slug del tenant. Obligatorio para platform_admin.
          schema:
            type: string
      responses:
        "200":
          description: Cantidad de notificaciones eliminadas
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/NotificationDismissAllResponse"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
  /viewer/oauth/{provider}/start:
    get:
      tags:
        - Community
      operationId: startViewerOAuth
      summary: Inicia el acceso OAuth de un viewer de comunidad
      x-convray-runtime-status: active-community-v1
      description: >-
        Redirige (303) a la URL de autorización del proveedor (Kick o Discord)
        para el Streamer Site indicado. Endpoint público, sin sesión. Si el
        Streamer Site existe pero el proveedor no está configurado o está
        deshabilitado, redirige de vuelta al sitio con el resultado en el query.
      security: []
      parameters:
        - name: provider
          in: path
          required: true
          schema:
            type: string
            enum:
              - kick
              - discord
        - name: streamer_slug
          in: query
          required: false
          schema:
            type: string
      responses:
        "303":
          description: Redirección al proveedor OAuth o de vuelta al Streamer Site
          headers:
            Location:
              schema:
                type: string
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
        "503":
          description: El proveedor OAuth aún no está configurado en Convray
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
  /viewer/oauth/{provider}/callback:
    get:
      tags:
        - Community
      operationId: completeViewerOAuth
      summary: Completa el acceso OAuth de un viewer de comunidad
      x-convray-runtime-status: active-community-v1
      description: >-
        Callback del proveedor OAuth. Endpoint público, sin sesión. En el caso
        normal establece la cookie de sesión viewer y redirige (303) al Streamer
        Site con el resultado. Ante un error sin slug asociado responde un
        problema JSON.
      security: []
      parameters:
        - name: provider
          in: path
          required: true
          schema:
            type: string
            enum:
              - kick
              - discord
        - name: state
          in: query
          required: false
          schema:
            type: string
        - name: code
          in: query
          required: false
          schema:
            type: string
        - name: error
          in: query
          required: false
          schema:
            type: string
      responses:
        "303":
          description: Redirección al Streamer Site con el resultado del acceso
          headers:
            Location:
              schema:
                type: string
            Set-Cookie:
              schema:
                type: string
        "400":
          description: La autorización expiró, ya fue utilizada o no coincide con el proveedor
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "502":
          description: El proveedor OAuth no está disponible
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
  /viewer/session:
    get:
      tags:
        - Community
      operationId: getViewerSession
      summary: Devuelve la sesión viewer vigente
      x-convray-runtime-status: active-community-v1
      description: >-
        Devuelve la identidad viewer y, si aplica, su membresía de comunidad.
        Requiere la cookie de sesión viewer. Acepta streamer_slug para resolver
        la membresía del sitio consultado.
      security:
        - viewerSessionCookie: []
      parameters:
        - name: streamer_slug
          in: query
          required: false
          schema:
            type: string
      responses:
        "200":
          description: Sesión viewer vigente
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ViewerSessionView"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
  /viewer/subscriptions:
    post:
      tags:
        - Community
      operationId: createViewerSubscription
      summary: Suscribe al viewer a la comunidad de un Streamer Site
      x-convray-runtime-status: active-community-v1
      description: >-
        Crea (o recupera) la suscripción de comunidad del viewer para el
        Streamer Site indicado. Requiere la cookie de sesión viewer y valida el
        Origin de la solicitud.
      security:
        - viewerSessionCookie: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ViewerSubscriptionRequest"
      responses:
        "200":
          description: Sesión viewer con la membresía resultante
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ViewerSessionView"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
        "409":
          description: La cuenta externa ya pertenece a otra identidad viewer
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /streamer-site/settings:
    get:
      tags:
        - Analytics
      operationId: getStreamerSiteSettings
      summary: Obtiene la configuración del Streamer Site del creador
      x-convray-runtime-status: active-streamer-site-v1
      description: Devuelve la configuración editable del Streamer Site de la membresía en sesión.
      security:
        - adminBearer: []
      responses:
        "200":
          description: Configuración del Streamer Site
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/StreamerSiteSettings"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
    patch:
      tags:
        - Analytics
      operationId: updateStreamerSiteSettings
      summary: Actualiza la configuración del Streamer Site
      x-convray-runtime-status: active-streamer-site-v1
      description: >-
        Actualiza la configuración del Streamer Site con control de versión
        optimista (expected_version obligatorio). Valida el Origin de la solicitud.
      security:
        - adminBearer: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateStreamerSiteSettingsRequest"
      responses:
        "200":
          description: Configuración actualizada
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/StreamerSiteSettings"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "409":
          $ref: "#/components/responses/VersionConflict"
        "422":
          $ref: "#/components/responses/ValidationFailed"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
  /me/profile:
    patch:
      tags:
        - Session
      operationId: updateMyProfile
      summary: Actualiza el perfil de la cuenta en sesión
      x-convray-runtime-status: active-phase-5
      description: Actualiza el nombre visible y las preferencias de notificación de la cuenta en sesión.
      security:
        - adminBearer: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateMyProfileRequest"
      responses:
        "200":
          description: Cuenta actualizada
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PublicUser"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "422":
          $ref: "#/components/responses/ValidationFailed"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
  /me/password:
    post:
      tags:
        - Session
      operationId: changeMyPassword
      summary: Cambia la contraseña de la cuenta en sesión
      x-convray-runtime-status: active-phase-5
      description: >-
        Cambia la contraseña verificando la contraseña actual. Rota la sesión y
        devuelve un nuevo access token; el refresh token se entrega por cookie.
      security:
        - adminBearer: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ChangePasswordRequest"
      responses:
        "200":
          description: Contraseña cambiada; nueva sesión emitida
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AuthResult"
        "401":
          $ref: "#/components/responses/InvalidCredentials"
        "422":
          $ref: "#/components/responses/ValidationFailed"
        "503":
          $ref: "#/components/responses/AuditUnavailable"
  /me/api-tokens:
    get:
      tags:
        - Session
      operationId: listMyApiTokens
      summary: Lista los tokens personales de API
      x-convray-runtime-status: active-data-v1
      description: >-
        Lista los tokens personales de API (cvr_) de la cuenta de empresa en
        sesión. Solo un JWT de sesión web accede a esta superficie; un token
        personal no puede administrar tokens.
      security:
        - adminBearer: []
      parameters:
        - name: tenant
          in: query
          required: false
          schema:
            type: string
      responses:
        "200":
          description: Tokens personales de la cuenta
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiTokenListResponse"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
    post:
      tags:
        - Session
      operationId: createMyApiToken
      summary: Emite un token personal de API
      x-convray-runtime-status: active-data-v1
      description: >-
        Emite un token personal de API (cvr_) para la superficie de datos. El
        secreto se devuelve UNA sola vez en la respuesta y no puede recuperarse
        después. Solo cuentas de empresa de un casino (o super_admin) pueden emitir.
      security:
        - adminBearer: []
      parameters:
        - name: tenant
          in: query
          required: false
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateApiTokenRequest"
      responses:
        "201":
          description: Token emitido; el secreto se devuelve una única vez
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CreateApiTokenResponse"
        "400":
          description: El cuerpo de la solicitud no es JSON válido
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /me/api-tokens/{id}:
    delete:
      tags:
        - Session
      operationId: revokeMyApiToken
      summary: Revoca un token personal de API
      x-convray-runtime-status: active-data-v1
      description: Revoca un token personal de API de la cuenta en sesión.
      security:
        - adminBearer: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
        - name: tenant
          in: query
          required: false
          schema:
            type: string
      responses:
        "204":
          description: Token revocado
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "404":
          $ref: "#/components/responses/ResourceNotFound"
  /partner/metrics:
    get:
      tags:
        - Analytics
      operationId: getPartnerMetrics
      summary: Métricas curadas del propio partner
      x-convray-runtime-status: active-apc-2
      description: >-
        Devuelve las métricas agregadas curadas del propio partner
        (casino_partner_period_metrics), paginadas por cursor, más el estado de
        ingestión. El partner sale de la sesión; no acepta parámetro de partner.
      security:
        - adminBearer: []
      parameters:
        - name: period_from
          in: query
          required: false
          description: YYYY-MM (mensual, default) o YYYY-MM-DD (día). Debe coincidir en formato con period_to.
          schema:
            type: string
        - name: period_to
          in: query
          required: false
          schema:
            type: string
        - name: cursor
          in: query
          required: false
          schema:
            type: string
        - name: limit
          in: query
          required: false
          schema:
            type: string
      responses:
        "200":
          description: Página de métricas del partner
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerMetricsPage"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /partner/data-metrics:
    get:
      tags:
        - Analytics
      operationId: getPartnerDataMetrics
      summary: Métricas reales del casino atribuidas al propio partner
      x-convray-runtime-status: active-apc-2
      description: >-
        Devuelve las métricas reales del casino atribuidas por BTAG al propio
        partner, agregadas por el rango (días civiles YYYY-MM-DD, default mes en
        curso), sin PII ni jugadores. Los montos viajan en null cuando la
        población del rango cae bajo el umbral de k-anonimato (low_population).
      security:
        - adminBearer: []
      parameters:
        - name: from
          in: query
          required: false
          schema:
            type: string
            format: date
        - name: to
          in: query
          required: false
          schema:
            type: string
            format: date
      responses:
        "200":
          description: Resumen de métricas desde Data del partner
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerDataMetricsSummary"
        "401":
          $ref: "#/components/responses/InvalidSession"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /partner-invitation-acceptances:
    post:
      tags:
        - Identity
      operationId: acceptPartnerInvitation
      summary: Acepta una invitación de traspaso de cuenta partner
      x-convray-runtime-status: active-apc-2
      description: >-
        Acepta la invitación de traspaso de cuenta partner con el token de
        invitación y la contraseña del owner. Endpoint sin sesión previa; valida
        el Origin de la solicitud. Devuelve el tenant reclamado.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PartnerInvitationAcceptanceRequest"
      responses:
        "201":
          description: Invitación aceptada; tenant reclamado
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerInvitationAcceptance"
        "401":
          description: El token o las credenciales de la invitación no son válidos
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "403":
          $ref: "#/components/responses/PermissionDenied"
        "409":
          description: La invitación ya fue utilizada o revocada
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "410":
          description: La invitación venció
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "422":
          $ref: "#/components/responses/ValidationFailed"
  /integrations/nexor/webhook:
    post:
      tags:
        - CRM
      operationId: receiveNexorWebhook
      summary: Recibe un evento webhook de Nexor
      x-convray-runtime-status: active-nexor-f2-c8
      description: >-
        Recibe un evento webhook de Nexor. Endpoint público sin sesión: la
        autenticidad se verifica con la firma HMAC-SHA256 del body crudo en el
        header X-Nexor-Signature más una defensa de timestamp. Un webhook_id
        desconocido, un duplicado o un evento sin cambios responden 204 igual que
        uno aplicado (no revela información).
      security: []
      parameters:
        - name: X-Nexor-Signature
          in: header
          required: true
          description: HMAC-SHA256 del body crudo en formato "sha256=<hex>".
          schema:
            type: string
        - name: X-Nexor-Timestamp
          in: header
          required: true
          description: Epoch en segundos; debe estar dentro de la ventana de skew permitida.
          schema:
            type: string
        - name: X-Nexor-Webhook-ID
          in: header
          required: false
          description: Identificador del webhook; si falta se toma del body.
          schema:
            type: string
        - name: X-Nexor-Event
          in: header
          required: false
          description: Tipo de evento; si falta se toma del body.
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/NexorWebhookEvent"
      responses:
        "204":
          description: Evento aceptado (o descartado sin efecto, sin revelar el motivo)
        "401":
          description: La firma o el sobre del evento no es válido
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "413":
          description: El webhook excede el límite de tamaño permitido
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
        "503":
          description: La integración de Nexor está deshabilitada o temporalmente no disponible
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemBase"
components:
  securitySchemes:
    adminBearer:
      type: http
      scheme: bearer
      bearerFormat: JWT
      x-convray-required-audience: convray-admin
      description: |
        Access token administrativo de corta duración. Debe validar firma,
        issuer, `aud=convray-admin`, expiración y estado de la sesión server-side.
    adminRefreshCookie:
      type: apiKey
      in: cookie
      name: convray_refresh
      description: |
        Credencial opaca, rotativa y revocable de una sesión administrativa;
        nunca se expone a JavaScript.
    viewerSessionCookie:
      type: apiKey
      in: cookie
      name: convray_viewer_session
      description: Sesión viewer opaca, HttpOnly, revocable y separada de la sesión administrativa.
    dataApiToken:
      type: http
      scheme: bearer
      bearerFormat: cvr
      description: |
        Token personal de API con prefijo cvr_. Autentica la superficie de Data
        (imports, lectura, conector Centrivo y administración de soporte) con el mismo
        usuario, rol y tenant que la sesión web, sujeto a límite de tasa por token.
    supportServiceToken:
      type: http
      scheme: bearer
      bearerFormat: cvs
      description: |
        Token de servicio con prefijo cvs_ y scope support.read. Autentica exclusivamente
        la API de soporte de solo lectura; un JWT o un token cvr_ nunca autentican en esta
        superficie y este token no autentica en ninguna otra.
    retentionServiceToken:
      type: http
      scheme: bearer
      bearerFormat: cvj
      description: |
        Token de máquina con prefijo cvj_, atado a un tenant y a una lista cerrada de patrones de pack, con
        UN scope según su integración: retention.read (juego-octubre, qa-retencion: health, deposits/*,
        players/* y promo/redemptions) o retention.codes (juego-codigos: health, promo/packs y
        promo/codes). Autentica exclusivamente la API de retención; un JWT, un cvr_ o un cvs_ nunca
        autentican en esta superficie y este token no autentica en ninguna otra.
  headers:
    NoStore:
      description: La respuesta contiene contexto de identidad y no puede almacenarse en cachés.
      schema:
        type: string
        const: no-store
    PrivateNoStore:
      description: La respuesta autenticada contiene contexto sensible y no puede almacenarse en cachés compartidas ni privadas.
      schema:
        type: string
        const: private, no-store
    RequestId:
      description: Identificador de correlación aceptado o generado por el servidor.
      schema:
        type: string
        minLength: 1
        maxLength: 128
    RefreshCookie:
      description: convray_refresh opaca; HttpOnly; Secure; SameSite=Lax; Path=/api/v1/auth/sessions. El entorno local puede omitir Secure.
      schema:
        type: string
    ExpiredRefreshCookie:
      description: Expira convray_refresh con Max-Age=0 y el mismo Path/SameSite usado al crearla.
      schema:
        type: string
    AnalyticsVisitorCookie:
      description: convray_visitor aleatoria; HttpOnly; Secure en entornos no locales; SameSite=Lax; Path=/api/v1/public/streamer-sites/. El valor crudo no se persiste.
      schema:
        type: string
    TrackingVisitorCookie:
      description: Cookie first-party opcional con 128 bits aleatorios; HttpOnly; Secure; SameSite=Lax; Path=/r. El valor crudo no se persiste y su sidecar pseudónimo tiene retención máxima de 90 días.
      schema:
        type: string
  parameters:
    SupportConversationId:
      name: X-Conversation-Id
      in: header
      required: true
      schema:
        type: string
        minLength: 1
        maxLength: 128
      description: Identificador de la conversación de soporte que origina la consulta; se audita con cada acceso.
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      schema:
        type: string
        minLength: 8
        maxLength: 128
    RelationshipDealId:
      name: deal_id
      in: path
      required: true
      schema:
        type: string
        format: uuid
    RelationshipLimit:
      name: limit
      in: query
      required: false
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 25
    RelationshipCursor:
      name: cursor
      in: query
      required: false
      schema:
        type: string
        format: uuid
    RelationshipStatus:
      name: status
      in: query
      required: false
      schema:
        type: string
        enum:
          - submitted
          - provisioning
          - active
          - rejected
          - cancelled
          - suspended
          - ended
    TrackingLimit:
      name: limit
      in: query
      required: false
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 25
    TrackingCursor:
      name: cursor
      in: query
      required: false
      description: UUID cursor emitido por la página APC-4 anterior.
      schema:
        type: string
        format: uuid
    TrackingDestinationId:
      name: destination_id
      in: path
      required: true
      schema:
        type: string
        format: uuid
    TrackingDestinationStatus:
      name: status
      in: query
      required: false
      schema:
        type: string
        enum:
          - draft
          - published
          - paused
          - archived
    TrackingProgramId:
      name: program_id
      in: query
      required: false
      schema:
        type: string
        format: uuid
    TrackingLinkId:
      name: link_id
      in: path
      required: true
      schema:
        type: string
        format: uuid
    TrackingDealIdQuery:
      name: deal_id
      in: query
      required: false
      schema:
        type: string
        format: uuid
    TrackingLinkStatus:
      name: status
      in: query
      required: false
      schema:
        type: string
        enum:
          - active
          - inactive
          - revoked
    OpaqueTrackingToken:
      name: opaque_token
      in: path
      required: true
      description: Identificador público aleatorio sin autoridad ni datos internos.
      schema:
        type: string
        minLength: 43
        maxLength: 43
        pattern: ^[A-Za-z0-9_-]{43}$
    MiniGameKey:
      name: game
      in: path
      required: true
      schema:
        type: string
        enum:
          - flip
          - dice
          - plinko
          - roulette
    MiniGameKeyQuery:
      name: game
      in: query
      required: false
      schema:
        type: string
        enum:
          - flip
          - dice
          - plinko
          - roulette
        default: flip
    MiniGameConfigurationId:
      name: configurationId
      in: path
      required: true
      schema:
        type: string
        format: uuid
    MiniGameRoundId:
      name: roundId
      in: path
      required: true
      schema:
        type: string
        format: uuid
    StreamerSlugQuery:
      name: streamer_slug
      in: query
      required: true
      schema:
        type: string
        pattern: ^[a-z0-9]+(?:-[a-z0-9]+)*$
    DataTagFilter:
      name: tag
      in: query
      required: false
      description: >-
        Filtro de etiqueta de jugador (PRD-50): lista separada por coma de vip, vip_diamante,
        vip_zafiro, vip_experience, ex_vip, partner_tag, staff; cada valor admite el prefijo
        no_ para negarlo. Varios valores se combinan con AND. Aplica a depósitos, retiros,
        apuestas de casino y deportes (transacciones) y al listado de jugadores; un valor fuera
        del catálogo, o el filtro en un dataset que no lo admite, responde 422.
      schema:
        type: array
        items:
          type: string
          enum:
            - vip
            - vip_diamante
            - vip_zafiro
            - vip_experience
            - ex_vip
            - partner_tag
            - staff
            - no_vip
            - no_vip_diamante
            - no_vip_zafiro
            - no_vip_experience
            - no_ex_vip
            - no_partner_tag
            - no_staff
    DataFlagFilter:
      name: flag
      in: query
      required: false
      description: >-
        Filtro de restricción de estado del jugador (PRD-50): lista separada por coma de
        self_excluded, blocked; cada valor admite el prefijo no_ para negarlo. Varios valores se
        combinan con AND. Mismos datasets que tag; un valor fuera del catálogo, o el filtro en
        un dataset que no lo admite, responde 422.
      schema:
        type: array
        items:
          type: string
          enum:
            - self_excluded
            - blocked
            - no_self_excluded
            - no_blocked
    DataIspFilter:
      name: isp
      in: query
      required: false
      description: >-
        Filtro de compañía de internet vigente del jugador (PRD-53 Fase 2): lista separada por coma de
        movistar, entel, claro, vtr, gtd, wom, mundo, starlink, vpn_relay, otro_chile, fuera_chile,
        sin_dato. Varios valores se combinan con OR (cualquiera de las compañías elegidas). Mismos
        datasets que tag/flag; un valor fuera del catálogo, o el filtro en un dataset que no lo admite,
        responde 422.
      schema:
        type: array
        items:
          type: string
          enum:
            - movistar
            - entel
            - claro
            - vtr
            - gtd
            - wom
            - mundo
            - starlink
            - vpn_relay
            - otro_chile
            - fuera_chile
            - sin_dato
    DataContactable:
      name: contactable
      in: query
      required: false
      description: >-
        "Solo contactables" (OLA 2 · lienzo Ledger). Con true limita a jugadores sin autoexclusión ni
        bloqueo y con la cuenta activa (equivale a inyectar no_self_excluded + no_blocked en flag).
        Aplica a los datasets con filtro por jugador (depósitos, retiros, apuestas, jugadores, totales).
      schema:
        type: boolean
    DataStatusExclude:
      name: status_ex
      in: query
      required: false
      description: >-
        Estados EXCLUIDOS (OLA 2 · chip rojo "−"): lista separada por coma; se descartan las filas cuyo
        status esté en el conjunto (se conservan los NULL). Espeja el filtro status; el catálogo depende
        del dataset (depósitos/retiros). Un valor fuera del catálogo responde 422.
      schema:
        type: array
        items:
          type: string
    DataStateExclude:
      name: state_ex
      in: query
      required: false
      description: >-
        Estados de cuenta (jugadores) o de la apuesta (deportes) EXCLUIDOS (OLA 2 · chip rojo "−"): lista
        separada por coma; espeja el filtro state, validado contra el mismo catálogo cerrado (422 si no).
      schema:
        type: array
        items:
          type: string
    DataMethodExclude:
      name: method_ex
      in: query
      required: false
      description: >-
        Métodos de pago EXCLUIDOS (OLA 2 · chip rojo "−"); lista separada por coma; espeja method.
      schema:
        type: array
        items:
          type: string
    DataMethodTypeExclude:
      name: method_type_ex
      in: query
      required: false
      description: >-
        Tipos de método EXCLUIDOS (OLA 2); lista separada por coma; espeja method_type (attributes).
      schema:
        type: array
        items:
          type: string
    DataDeviceExclude:
      name: device_ex
      in: query
      required: false
      description: >-
        Dispositivos EXCLUIDOS (OLA 2); lista separada por coma; espeja device.
      schema:
        type: array
        items:
          type: string
    DataCountryExclude:
      name: country_ex
      in: query
      required: false
      description: >-
        Países EXCLUIDOS (OLA 2); lista separada por coma; espeja country (attributes).
      schema:
        type: array
        items:
          type: string
    DataRegionExclude:
      name: region_ex
      in: query
      required: false
      description: >-
        Regiones EXCLUIDAS (OLA 2); lista separada por coma; espeja region.
      schema:
        type: array
        items:
          type: string
    DataProviderExclude:
      name: provider_ex
      in: query
      required: false
      description: >-
        Proveedores de casino EXCLUIDOS (OLA 2); lista separada por coma; espeja provider.
      schema:
        type: array
        items:
          type: string
    DataCategoryExclude:
      name: category_ex
      in: query
      required: false
      description: >-
        Categorías de casino EXCLUIDAS (OLA 2); lista separada por coma; espeja category.
      schema:
        type: array
        items:
          type: string
    DataGameExclude:
      name: game_ex
      in: query
      required: false
      description: >-
        Juegos EXCLUIDOS por game_id (OLA 2); lista separada por coma; espeja game/game_id.
      schema:
        type: array
        items:
          type: string
    DataTagExclude:
      name: tag_ex
      in: query
      required: false
      description: >-
        Etiquetas de jugador EXCLUIDAS (OLA 2 · chip rojo "−"): lista separada por coma; se re-expresa
        como no_<valor> del filtro tag (mismo catálogo cerrado, 422 si un valor no pertenece).
      schema:
        type: array
        items:
          type: string
          enum:
            - vip
            - vip_diamante
            - vip_zafiro
            - vip_experience
            - ex_vip
            - partner_tag
            - staff
    DataFlagExclude:
      name: flag_ex
      in: query
      required: false
      description: >-
        Restricciones de estado EXCLUIDAS (OLA 2 · chip rojo "−"): lista separada por coma; se re-expresa
        como no_<valor> del filtro flag (mismo catálogo cerrado, 422 si un valor no pertenece).
      schema:
        type: array
        items:
          type: string
          enum:
            - self_excluded
            - blocked
    DataIspExclude:
      name: isp_ex
      in: query
      required: false
      description: >-
        Compañías de internet EXCLUIDAS (OLA 2 · chip rojo "−"): lista separada por coma; se descartan los
        jugadores cuya compañía vigente esté en el conjunto. Mismo catálogo cerrado que isp (422 si no).
      schema:
        type: array
        items:
          type: string
          enum:
            - movistar
            - entel
            - claro
            - vtr
            - gtd
            - wom
            - mundo
            - starlink
            - vpn_relay
            - otro_chile
            - fuera_chile
            - sin_dato
  schemas:
    CRMVoiceRecipientRow:
      type: object
      description: |
        Fila del detalle por jugador de una campaña voice_nexor. first_name/last_name se descifran en
        memoria y nunca se persisten. Los montos van en pesos enteros.
      required:
        - external_player_ref
        - group
        - nexor_status_changes
      properties:
        external_player_ref:
          type: string
        casino_player_ref:
          type: string
        first_name:
          type: string
        last_name:
          type: string
        group:
          type: string
          enum: [send, control]
        nexor_status:
          type: string
          description: Estado mapeado del lead en Nexor.
        nexor_status_key:
          type: string
          description: Último estado crudo recibido de Nexor.
        nexor_status_changes:
          type: integer
        nexor_pushed_at:
          type: string
          format: date-time
          nullable: true
        nexor_engaged_at:
          type: string
          format: date-time
          nullable: true
        nexor_status_at:
          type: string
          format: date-time
          nullable: true
        contacted:
          type: boolean
        recontacted:
          type: boolean
          description: Aproximación (contactado con más de un cambio de estado); no es el conteo real de llamadas.
        deposited:
          type: boolean
        deposits_count:
          type: integer
        deposits_amount:
          type: integer
          description: Monto de depósitos en pesos enteros.
        ggr:
          type: integer
        first_deposit:
          type: boolean
    CRMVoiceRecipientPage:
      type: object
      required:
        - recipients
        - total
        - limit
        - offset
      properties:
        recipients:
          type: array
          items:
            $ref: "#/components/schemas/CRMVoiceRecipientRow"
        total:
          type: integer
        limit:
          type: integer
        offset:
          type: integer
        has_more:
          type: boolean
        next_offset:
          type: integer
    CRMVoiceBreakdown:
      type: object
      required:
        - status_distribution
        - reactivated_by_day
      properties:
        status_distribution:
          type: array
          items:
            type: object
            required: [key, count]
            properties:
              key:
                type: string
              count:
                type: integer
        reactivated_by_day:
          type: array
          items:
            type: object
            required: [day, count]
            properties:
              day:
                type: string
                description: Día civil YYYY-MM-DD en America/Santiago.
              count:
                type: integer
    PlinkoPayoutTable:
      type: array
      description: Numeradores de premio bruto para buckets 0..8; el denominador fijo es 50.
      minItems: 9
      maxItems: 9
      prefixItems:
        - type: integer
          const: 500
        - type: integer
          const: 152
        - type: integer
          const: 75
        - type: integer
          const: 40
        - type: integer
          const: 8
        - type: integer
          const: 40
        - type: integer
          const: 75
        - type: integer
          const: 152
        - type: integer
          const: 500
      items: false
    MiniGameConfiguration:
      type: object
      additionalProperties: false
      required:
        - id
        - game
        - status
        - environment
        - rules_version
        - configuration_version
        - min_points_cost
        - max_points_cost
        - points_increment
        - award_numerator
        - award_denominator
        - payout_model
        - payout_table
        - terms_version
        - terms_text
        - supersedes_configuration_id
        - revision
        - published_at
        - suspended_at
        - created_at
      properties:
        id:
          type: string
          format: uuid
        game:
          type: string
          enum:
            - flip
            - dice
            - plinko
            - roulette
        status:
          type: string
          enum:
            - draft
            - active
            - suspended
            - superseded
        environment:
          type: string
          const: sandbox
        rules_version:
          type: integer
          const: 1
        configuration_version:
          type: integer
          minimum: 1
        min_points_cost:
          type: integer
          minimum: 50
          maximum: 5000
          multipleOf: 50
        max_points_cost:
          type: integer
          minimum: 50
          maximum: 5000
          multipleOf: 50
        points_increment:
          type: integer
          const: 50
        award_numerator:
          type: integer
          const: 99
        award_denominator:
          type: integer
          const: 50
        payout_model:
          type: string
          enum:
            - fixed
            - plinko-buckets-v1
            - roulette-european-v1
        payout_table:
          oneOf:
            - type: "null"
            - $ref: "#/components/schemas/PlinkoPayoutTable"
        terms_version:
          type: string
          pattern: ^[A-Za-z0-9][A-Za-z0-9._-]{0,39}$
        terms_text:
          type: string
          minLength: 20
          maxLength: 10000
        supersedes_configuration_id:
          type:
            - string
            - "null"
          format: uuid
        revision:
          type: integer
          minimum: 1
        published_at:
          type:
            - string
            - "null"
          format: date-time
        suspended_at:
          type:
            - string
            - "null"
          format: date-time
        created_at:
          type: string
          format: date-time
    MiniGameConfigurationCreateInput:
      type: object
      additionalProperties: false
      required:
        - min_points_cost
        - max_points_cost
        - terms_version
        - terms_text
      properties:
        min_points_cost:
          type: integer
          minimum: 50
          maximum: 5000
          multipleOf: 50
        max_points_cost:
          type: integer
          minimum: 50
          maximum: 5000
          multipleOf: 50
        terms_version:
          type: string
          pattern: ^[A-Za-z0-9][A-Za-z0-9._-]{0,39}$
        terms_text:
          type: string
          minLength: 20
          maxLength: 10000
        supersedes_configuration_id:
          type:
            - string
            - "null"
          format: uuid
    MiniGameConfigurationPublicationInput:
      type: object
      additionalProperties: false
      required:
        - expected_revision
        - non_monetary_confirmed
      properties:
        expected_revision:
          type: integer
          minimum: 1
        non_monetary_confirmed:
          type: boolean
          const: true
    MiniGameConfigurationSuspensionInput:
      type: object
      additionalProperties: false
      required:
        - expected_revision
        - reason_code
      properties:
        expected_revision:
          type: integer
          minimum: 1
        reason_code:
          type: string
          const: owner_paused
    MiniGameOverview:
      type: object
      additionalProperties: false
      required:
        - program_symbol
        - configurations
      properties:
        program_symbol:
          type: string
          pattern: ^[A-Z0-9]{2,10}$
        configurations:
          type: array
          maxItems: 100
          items:
            $ref: "#/components/schemas/MiniGameConfiguration"
    ViewerMiniGameCatalog:
      type: object
      additionalProperties: false
      required:
        - game
        - available
        - sandbox
        - program_symbol
        - balance_points
      properties:
        game:
          type: string
          enum:
            - flip
            - dice
            - plinko
            - roulette
        available:
          type: boolean
        sandbox:
          type: boolean
          const: true
        program_symbol:
          type: string
          pattern: ^[A-Z0-9]{2,10}$
        balance_points:
          type: integer
          minimum: 0
        configuration:
          $ref: "#/components/schemas/MiniGameConfiguration"
    MiniGameRoundPrepareInput:
      type: object
      additionalProperties: false
      required:
        - streamer_slug
      properties:
        streamer_slug:
          type: string
          pattern: ^[a-z0-9]+(?:-[a-z0-9]+)*$
    MiniGamePreparedRound:
      type: object
      additionalProperties: false
      required:
        - id
        - game
        - state
        - environment
        - rules_version
        - configuration_id
        - configuration_version
        - commitment
        - min_points_cost
        - max_points_cost
        - points_increment
        - award_numerator
        - award_denominator
        - payout_model
        - payout_table
        - terms_version
        - expires_at
      properties:
        id:
          type: string
          format: uuid
        game:
          type: string
          enum:
            - flip
            - dice
            - plinko
            - roulette
        state:
          type: string
          const: prepared
        environment:
          type: string
          const: sandbox
        rules_version:
          type: integer
          const: 1
        configuration_id:
          type: string
          format: uuid
        configuration_version:
          type: integer
          minimum: 1
        commitment:
          type: string
          pattern: ^[A-Za-z0-9_-]{43}$
        min_points_cost:
          type: integer
          minimum: 50
          maximum: 5000
          multipleOf: 50
        max_points_cost:
          type: integer
          minimum: 50
          maximum: 5000
          multipleOf: 50
        points_increment:
          type: integer
          const: 50
        award_numerator:
          type: integer
          const: 99
        award_denominator:
          type: integer
          const: 50
        payout_model:
          type: string
          enum:
            - fixed
            - plinko-buckets-v1
            - roulette-european-v1
        payout_table:
          oneOf:
            - type: "null"
            - $ref: "#/components/schemas/PlinkoPayoutTable"
        terms_version:
          type: string
          pattern: ^[A-Za-z0-9][A-Za-z0-9._-]{0,39}$
        expires_at:
          type: string
          format: date-time
    MiniGameRoundPrepareResult:
      type: object
      additionalProperties: false
      required:
        - round
        - replayed
      properties:
        round:
          $ref: "#/components/schemas/MiniGamePreparedRound"
        replayed:
          type: boolean
    MiniGameRoundPlayInput:
      type: object
      additionalProperties: false
      required:
        - streamer_slug
        - selection
        - points_cost
        - client_seed
        - accepted_terms
        - accepted_terms_version
      properties:
        streamer_slug:
          type: string
          pattern: ^[a-z0-9]+(?:-[a-z0-9]+)*$
        selection:
          type: string
          enum:
            - heads
            - tails
            - under
            - over
            - drop
        points_cost:
          type: integer
          minimum: 50
          maximum: 5000
          multipleOf: 50
        client_seed:
          type: string
          pattern: ^[A-Za-z0-9_-]{43}$
        accepted_terms:
          type: boolean
          const: true
        accepted_terms_version:
          type: string
          pattern: ^[A-Za-z0-9][A-Za-z0-9._-]{0,39}$
    MiniGameProofContext:
      type: object
      additionalProperties: false
      required:
        - round_id
        - tenant_id
        - viewer_id
        - configuration_id
        - rules_version
        - configuration_version
      properties:
        round_id:
          type: string
          format: uuid
        tenant_id:
          type: string
          format: uuid
        viewer_id:
          type: string
          format: uuid
        configuration_id:
          type: string
          format: uuid
        rules_version:
          type: integer
          const: 1
        configuration_version:
          type: integer
          minimum: 1
    MiniGameProof:
      type: object
      additionalProperties: false
      required:
        - algorithm
        - commitment
        - server_seed
        - client_seed
        - digest
        - context
        - verified
      properties:
        algorithm:
          type: string
          enum:
            - flip-v1
            - dice-v1
            - plinko-v1
            - roulette-v1
        commitment:
          type: string
          pattern: ^[A-Za-z0-9_-]{43}$
        server_seed:
          type: string
          pattern: ^[A-Za-z0-9_-]{43}$
        client_seed:
          type: string
          pattern: ^[A-Za-z0-9_-]{43}$
        digest:
          type: string
          pattern: ^[A-Za-z0-9_-]{43}$
        context:
          $ref: "#/components/schemas/MiniGameProofContext"
        verified:
          type: boolean
          const: true
    MiniGameSettledRound:
      type: object
      additionalProperties: false
      required:
        - id
        - game
        - state
        - environment
        - selection
        - outcome
        - won
        - points_cost
        - points_awarded
        - balance_points
        - proof
        - settled_at
      properties:
        id:
          type: string
          format: uuid
        game:
          type: string
          enum:
            - flip
            - dice
            - plinko
            - roulette
        state:
          type: string
          const: settled
        environment:
          type: string
          const: sandbox
        selection:
          type: string
          enum:
            - heads
            - tails
            - under
            - over
            - drop
        outcome:
          oneOf:
            - type: string
              enum:
                - heads
                - tails
            - type: string
              pattern: ^(0|[1-9][0-9]{0,3})$
        won:
          type: boolean
        points_cost:
          type: integer
          minimum: 50
          maximum: 5000
          multipleOf: 50
        points_awarded:
          type: integer
          minimum: 0
          maximum: 50000
        balance_points:
          type: integer
          minimum: 0
        proof:
          $ref: "#/components/schemas/MiniGameProof"
        settled_at:
          type: string
          format: date-time
    MiniGameRoundPlayResult:
      type: object
      additionalProperties: false
      required:
        - round
        - replayed
      properties:
        round:
          $ref: "#/components/schemas/MiniGameSettledRound"
        replayed:
          type: boolean
    StoreReward:
      type: object
      additionalProperties: false
      required:
        - id
        - name
        - description
        - image_url
        - points_cost
        - stock_total
        - stock_remaining
        - redemption_count
        - max_per_viewer
        - eligibility_type
        - fulfillment_type
        - sandbox
        - terms_version
        - terms_text
        - valid_from
        - valid_until
        - status
        - version
        - published_at
        - created_at
        - updated_at
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
          minLength: 3
          maxLength: 100
        description:
          type: string
          minLength: 3
          maxLength: 1000
        image_url:
          type:
            - string
            - "null"
          format: uri
          pattern: ^https://
        points_cost:
          type: integer
          minimum: 1
          maximum: 1000000000
        stock_total:
          type: integer
          minimum: 1
          maximum: 1000000
        stock_remaining:
          type: integer
          minimum: 0
          maximum: 1000000
        redemption_count:
          type: integer
          minimum: 0
        max_per_viewer:
          type: integer
          minimum: 1
          maximum: 1000
        eligibility_type:
          type: string
          const: active_member
        fulfillment_type:
          type: string
          const: sandbox
        sandbox:
          type: boolean
          const: true
        terms_version:
          type: string
          pattern: ^[A-Za-z0-9][A-Za-z0-9._-]{0,39}$
        terms_text:
          type: string
          minLength: 20
          maxLength: 10000
        valid_from:
          type: string
          format: date-time
        valid_until:
          type: string
          format: date-time
        status:
          type: string
          enum:
            - draft
            - published
            - archived
        version:
          type: integer
          minimum: 1
        published_at:
          type:
            - string
            - "null"
          format: date-time
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    StoreRewardCreateInput:
      type: object
      additionalProperties: false
      required:
        - name
        - description
        - points_cost
        - stock_total
        - max_per_viewer
        - terms_version
        - terms_text
        - valid_from
        - valid_until
      properties:
        name:
          type: string
          minLength: 3
          maxLength: 100
        description:
          type: string
          minLength: 3
          maxLength: 1000
        image_url:
          type:
            - string
            - "null"
          format: uri
          pattern: ^https://
        points_cost:
          type: integer
          minimum: 1
          maximum: 1000000000
        stock_total:
          type: integer
          minimum: 1
          maximum: 1000000
        max_per_viewer:
          type: integer
          minimum: 1
          maximum: 1000
        terms_version:
          type: string
          pattern: ^[A-Za-z0-9][A-Za-z0-9._-]{0,39}$
        terms_text:
          type: string
          minLength: 20
          maxLength: 10000
        valid_from:
          type: string
          format: date-time
        valid_until:
          type: string
          format: date-time
    StoreOrder:
      type: object
      additionalProperties: false
      required:
        - id
        - reward_id
        - reward_name
        - recipient_alias
        - quantity
        - points_debited
        - balance_after
        - status
        - fulfillment_type
        - sandbox
        - accepted_terms_version
        - territory_scope
        - provider
        - provider_reference
        - version
        - created_at
        - fulfilled_at
      properties:
        id:
          type: string
          format: uuid
        reward_id:
          type: string
          format: uuid
        reward_name:
          type: string
          minLength: 3
          maxLength: 100
        recipient_alias:
          type: string
          minLength: 2
          maxLength: 48
        quantity:
          type: integer
          const: 1
        points_debited:
          type: integer
          minimum: 0
          maximum: 1000000000
        balance_after:
          type: integer
          minimum: 0
        status:
          type: string
          enum:
            - pending
            - reserved
            - fulfilled
            - failed
            - cancelled
            - refunded
        fulfillment_type:
          type: string
          const: sandbox
        sandbox:
          type: boolean
          const: true
        accepted_terms_version:
          type: string
          pattern: ^[A-Za-z0-9][A-Za-z0-9._-]{0,39}$
        territory_scope:
          type: string
          const: sandbox
        provider:
          oneOf:
            - type: string
              const: convray_sandbox
            - type: "null"
        provider_reference:
          type:
            - string
            - "null"
          maxLength: 200
        version:
          type: integer
          minimum: 1
        created_at:
          type: string
          format: date-time
        fulfilled_at:
          type:
            - string
            - "null"
          format: date-time
    StoreOverview:
      type: object
      additionalProperties: false
      required:
        - program_symbol
        - rewards
        - orders
      properties:
        program_symbol:
          type: string
          pattern: ^[A-Z0-9]{2,10}$
        rewards:
          type: array
          maxItems: 100
          items:
            $ref: "#/components/schemas/StoreReward"
        orders:
          type: array
          maxItems: 100
          items:
            $ref: "#/components/schemas/StoreOrder"
    StoreOrderCollection:
      type: object
      additionalProperties: false
      required:
        - orders
      properties:
        orders:
          type: array
          maxItems: 100
          items:
            $ref: "#/components/schemas/StoreOrder"
    StoreRedemptionInput:
      type: object
      additionalProperties: false
      required:
        - streamer_slug
        - reward_id
        - accepted_terms
        - accepted_terms_version
      properties:
        streamer_slug:
          type: string
          pattern: ^[a-z0-9][a-z0-9-]{2,39}$
        reward_id:
          type: string
          format: uuid
        accepted_terms:
          type: boolean
          const: true
        accepted_terms_version:
          type: string
          pattern: ^[A-Za-z0-9][A-Za-z0-9._-]{0,39}$
    StoreRedemptionResult:
      type: object
      additionalProperties: false
      required:
        - order
        - replayed
      properties:
        order:
          $ref: "#/components/schemas/StoreOrder"
        replayed:
          type: boolean
    Giveaway:
      type: object
      additionalProperties: false
      required:
        - id
        - title
        - description
        - prize_type
        - prize_description
        - prize_points_amount
        - terms_text
        - terms_url
        - starts_at
        - ends_at
        - winners_count
        - entry_points_cost
        - max_entries_per_viewer
        - terms_version
        - status
        - version
        - published_at
        - created_at
        - updated_at
        - results_finalized
        - drawn_at
        - winners
        - metrics
      properties:
        id:
          type: string
          format: uuid
        title:
          type: string
          minLength: 3
          maxLength: 100
        description:
          type: string
          minLength: 3
          maxLength: 1000
        prize_type:
          type: string
          const: points
        prize_description:
          type: string
          minLength: 3
          maxLength: 160
        prize_points_amount:
          type: integer
          minimum: 1
          maximum: 1000000000
        terms_text:
          type:
            - string
            - "null"
          minLength: 20
          maxLength: 10000
        terms_url:
          type:
            - string
            - "null"
          format: uri
          pattern: ^https://
        starts_at:
          type: string
          format: date-time
        ends_at:
          type: string
          format: date-time
        winners_count:
          type: integer
          minimum: 1
          maximum: 100
        entry_points_cost:
          type: integer
          minimum: 0
          maximum: 1000000000
        max_entries_per_viewer:
          type: integer
          minimum: 1
          maximum: 1000
        terms_version:
          type: string
          pattern: ^[A-Za-z0-9][A-Za-z0-9._-]{0,39}$
        status:
          type: string
          enum:
            - draft
            - published
            - closed
            - cancelled
        version:
          type: integer
          minimum: 1
        published_at:
          type:
            - string
            - "null"
          format: date-time
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        results_finalized:
          type: boolean
        drawn_at:
          type:
            - string
            - "null"
          format: date-time
        winners:
          type: array
          maxItems: 100
          items:
            $ref: "#/components/schemas/GiveawayWinner"
        metrics:
          $ref: "#/components/schemas/GiveawayMetrics"
    GiveawayMetrics:
      type: object
      additionalProperties: false
      required:
        - entries_count
        - participants_count
        - winners_selected
        - entry_points_collected
        - prize_points_awarded
      properties:
        entries_count:
          type: integer
          minimum: 0
        participants_count:
          type: integer
          minimum: 0
        winners_selected:
          type: integer
          minimum: 0
        entry_points_collected:
          type: integer
          minimum: 0
        prize_points_awarded:
          type: integer
          minimum: 0
    GiveawayCreateInput:
      type: object
      additionalProperties: false
      required:
        - title
        - description
        - prize_description
        - prize_points_amount
        - starts_at
        - ends_at
        - winners_count
        - entry_points_cost
        - max_entries_per_viewer
        - terms_version
      anyOf:
        - required:
            - terms_text
        - required:
            - terms_url
      properties:
        title:
          type: string
          minLength: 3
          maxLength: 100
        description:
          type: string
          minLength: 3
          maxLength: 1000
        prize_description:
          type: string
          minLength: 3
          maxLength: 160
        prize_points_amount:
          type: integer
          minimum: 1
          maximum: 1000000000
        terms_text:
          type:
            - string
            - "null"
          minLength: 20
          maxLength: 10000
        terms_url:
          type:
            - string
            - "null"
          format: uri
          pattern: ^https://
        starts_at:
          type: string
          format: date-time
        ends_at:
          type: string
          format: date-time
        winners_count:
          type: integer
          minimum: 1
          maximum: 100
        entry_points_cost:
          type: integer
          minimum: 0
          maximum: 1000000000
          default: 0
        max_entries_per_viewer:
          type: integer
          minimum: 1
          maximum: 1000
          default: 1
        terms_version:
          type: string
          pattern: ^[A-Za-z0-9][A-Za-z0-9._-]{0,39}$
          default: v1
    GiveawayCollection:
      type: object
      additionalProperties: false
      required:
        - giveaways
      properties:
        wager_races:
          type: array
          maxItems: 2
          description: La carrera vigente y la última cerrada.
          items:
            $ref: "#/components/schemas/PublicWagerRace"
        giveaways:
          type: array
          maxItems: 100
          items:
            $ref: "#/components/schemas/Giveaway"
    GiveawayWinner:
      type: object
      additionalProperties: false
      required:
        - rank
        - public_alias
        - public_reference
      properties:
        rank:
          type: integer
          minimum: 1
          maximum: 100
        public_alias:
          type: string
          minLength: 2
          maxLength: 48
        public_reference:
          type: string
          pattern: ^[A-Z0-9]{8}$
    GiveawayDrawResult:
      type: object
      additionalProperties: false
      required:
        - giveaway_id
        - results_finalized
        - replayed
        - drawn_at
        - winners
      properties:
        giveaway_id:
          type: string
          format: uuid
        results_finalized:
          type: boolean
          const: true
        replayed:
          type: boolean
        drawn_at:
          type: string
          format: date-time
        winners:
          type: array
          maxItems: 100
          items:
            $ref: "#/components/schemas/GiveawayWinner"
    PublicGiveaway:
      type: object
      additionalProperties: false
      required:
        - id
        - title
        - description
        - prize_description
        - prize_points_amount
        - terms_text
        - terms_url
        - starts_at
        - ends_at
        - winners_count
        - entry_points_cost
        - max_entries_per_viewer
        - terms_version
        - status
        - closed_at
        - results_finalized
        - winners
      properties:
        id:
          type: string
          format: uuid
        title:
          type: string
          minLength: 3
          maxLength: 100
        description:
          type: string
          minLength: 3
          maxLength: 1000
        prize_description:
          type: string
          minLength: 3
          maxLength: 160
        prize_points_amount:
          type: integer
          minimum: 1
          maximum: 1000000000
        terms_text:
          type:
            - string
            - "null"
          minLength: 20
          maxLength: 10000
        terms_url:
          type:
            - string
            - "null"
          format: uri
          pattern: ^https://
        starts_at:
          type: string
          format: date-time
        ends_at:
          type: string
          format: date-time
        winners_count:
          type: integer
          minimum: 1
          maximum: 100
        entry_points_cost:
          type: integer
          minimum: 0
          maximum: 1000000000
        max_entries_per_viewer:
          type: integer
          minimum: 1
          maximum: 1000
        terms_version:
          type: string
          pattern: ^[A-Za-z0-9][A-Za-z0-9._-]{0,39}$
        status:
          type: string
          enum:
            - scheduled
            - active
            - closed
        closed_at:
          type:
            - string
            - "null"
          format: date-time
        results_finalized:
          type: boolean
        winners:
          type: array
          maxItems: 100
          items:
            $ref: "#/components/schemas/GiveawayWinner"
    GiveawayEntry:
      type: object
      additionalProperties: false
      required:
        - id
        - giveaway_id
        - entry_number
        - points_debited
        - balance_after
        - status
        - terms_version
        - territory_code
        - created_at
      properties:
        id:
          type: string
          format: uuid
        giveaway_id:
          type: string
          format: uuid
        entry_number:
          type: integer
          minimum: 1
        points_debited:
          type: integer
          minimum: 0
          maximum: 1000000000
        balance_after:
          type: integer
          minimum: 0
        status:
          type: string
          enum:
            - registered
            - eligible
            - excluded
            - refunded
        terms_version:
          type: string
          pattern: ^[A-Za-z0-9][A-Za-z0-9._-]{0,39}$
        territory_code:
          type: string
          pattern: ^[A-Z]{2}$
        created_at:
          type: string
          format: date-time
    GiveawayEntryCreateInput:
      type: object
      additionalProperties: false
      required:
        - streamer_slug
        - giveaway_id
        - accepted_terms
        - accepted_terms_version
        - age_confirmed
      properties:
        streamer_slug:
          type: string
          pattern: ^[a-z0-9][a-z0-9-]{2,39}$
        giveaway_id:
          type: string
          format: uuid
        accepted_terms:
          type: boolean
          const: true
        accepted_terms_version:
          type: string
          pattern: ^[A-Za-z0-9][A-Za-z0-9._-]{0,39}$
        age_confirmed:
          type: boolean
          const: true
    GiveawayEntryResult:
      type: object
      additionalProperties: false
      required:
        - entry
        - replayed
      properties:
        entry:
          $ref: "#/components/schemas/GiveawayEntry"
        replayed:
          type: boolean
    GiveawayEntryCollection:
      type: object
      additionalProperties: false
      required:
        - entries
        - results
      properties:
        entries:
          type: array
          maxItems: 100
          items:
            $ref: "#/components/schemas/GiveawayEntry"
        results:
          type: array
          maxItems: 100
          items:
            $ref: "#/components/schemas/ViewerGiveawayResult"
    ViewerGiveawayResult:
      type: object
      additionalProperties: false
      required:
        - giveaway_id
        - status
        - winner_rank
        - prize_points_awarded
      properties:
        giveaway_id:
          type: string
          format: uuid
        status:
          type: string
          enum:
            - pending
            - won
            - not_selected
        winner_rank:
          type:
            - integer
            - "null"
          minimum: 1
          maximum: 100
        prize_points_awarded:
          type: integer
          minimum: 0
          maximum: 1000000000
    PublicCommunity:
      type: object
      additionalProperties: false
      required:
        - program_name
        - program_symbol
        - status
        - viewer_auth_providers
      properties:
        program_name:
          type: string
          minLength: 2
          maxLength: 40
        program_symbol:
          type: string
          pattern: ^[A-Z0-9]{2,10}$
        status:
          type: string
          enum:
            - active
            - paused
        viewer_auth_providers:
          type: array
          uniqueItems: true
          minItems: 1
          maxItems: 2
          items:
            type: string
            enum:
              - kick
              - discord
    PublicSocialLinks:
      type: object
      additionalProperties: false
      required:
        - kick
        - discord
        - youtube
        - twitch
        - instagram
        - x
      properties:
        kick:
          type:
            - string
            - "null"
          format: uri
        discord:
          type:
            - string
            - "null"
          format: uri
        youtube:
          type:
            - string
            - "null"
          format: uri
        twitch:
          type:
            - string
            - "null"
          format: uri
        instagram:
          type:
            - string
            - "null"
          format: uri
        x:
          type:
            - string
            - "null"
          format: uri
    PublicYouTubeVideo:
      type: object
      additionalProperties: false
      required:
        - id
        - title
        - channel_title
        - thumbnail_url
        - url
        - published_at
        - duration_seconds
      properties:
        id:
          type: string
          minLength: 6
          maxLength: 32
        title:
          type: string
          minLength: 1
          maxLength: 500
        channel_title:
          type: string
          minLength: 1
          maxLength: 200
        thumbnail_url:
          type: string
          format: uri
          pattern: ^https://
        url:
          type: string
          format: uri
          pattern: ^https://www\.youtube\.com/watch
        published_at:
          type: string
          format: date-time
        duration_seconds:
          type: integer
          minimum: 0
    PublicYouTubeCatalog:
      type: object
      additionalProperties: false
      required:
        - channel_id
        - channel_title
        - channel_url
        - last_synced_at
        - videos
        - shorts
      properties:
        channel_id:
          type: string
          minLength: 1
          maxLength: 128
        channel_title:
          type: string
          minLength: 1
          maxLength: 200
        channel_url:
          type: string
          format: uri
          pattern: ^https://www\.youtube\.com/channel/
        last_synced_at:
          type:
            - string
            - "null"
          format: date-time
        videos:
          type: array
          maxItems: 6
          items:
            $ref: "#/components/schemas/PublicYouTubeVideo"
        shorts:
          type: array
          maxItems: 6
          items:
            $ref: "#/components/schemas/PublicYouTubeVideo"
    PublicStreamerSite:
      type: object
      additionalProperties: false
      required:
        - slug
        - display_name
        - tagline
        - bio
        - creator_color
        - social_links
        - slot_codes
        - community
        - giveaways
        - wager_races
        - mini_games
        - store
        - youtube
      properties:
        slug:
          type: string
          pattern: ^[a-z0-9]+(?:-[a-z0-9]+)*$
        display_name:
          type: string
          minLength: 1
          maxLength: 80
        tagline:
          type: string
          maxLength: 160
        bio:
          type: string
          maxLength: 1000
        creator_color:
          type: string
          pattern: ^#[0-9A-F]{6}$
        social_links:
          $ref: "#/components/schemas/PublicSocialLinks"
        slot_codes:
          type: array
          minItems: 6
          maxItems: 6
          items:
            $ref: "#/components/schemas/PublicStreamerSiteSlotCode"
        community:
          oneOf:
            - $ref: "#/components/schemas/PublicCommunity"
            - type: "null"
        giveaways:
          type: array
          maxItems: 2
          items:
            $ref: "#/components/schemas/PublicGiveaway"
        mini_games:
          type: array
          maxItems: 8
          items:
            $ref: "#/components/schemas/PublicMiniGame"
        store:
          oneOf:
            - $ref: "#/components/schemas/PublicStore"
            - type: "null"
        youtube:
          oneOf:
            - $ref: "#/components/schemas/PublicYouTubeCatalog"
            - type: "null"
    StreamerSiteEventInput:
      oneOf:
        - type: object
          additionalProperties: false
          required:
            - event_id
            - event_type
            - surface
          properties:
            event_id:
              type: string
              format: uuid
            event_type:
              type: string
              const: page_view
            surface:
              type: string
              enum:
                - home
                - how_it_works
                - games
                - game_flip
                - game_dice
                - game_plinko
                - game_roulette
        - type: object
          additionalProperties: false
          required:
            - event_id
            - event_type
            - surface
            - target_type
            - target_key
          properties:
            event_id:
              type: string
              format: uuid
            event_type:
              type: string
              const: click
            surface:
              type: string
              enum:
                - home
                - how_it_works
                - games
                - game_flip
                - game_dice
                - game_plinko
                - game_roulette
            target_type:
              type: string
              pattern: ^[a-z][a-z0-9_]{1,31}$
            target_key:
              type: string
              minLength: 1
              maxLength: 128
              pattern: ^[A-Za-z0-9][A-Za-z0-9._:-]*$
    StreamerSiteEventReceipt:
      type: object
      additionalProperties: false
      required:
        - accepted
        - replayed
      properties:
        accepted:
          type: boolean
          const: true
        replayed:
          type: boolean
    StreamerSiteAnalytics:
      type: object
      additionalProperties: false
      required:
        - period
        - web
        - top_interactions
        - mini_games
        - conversions
      properties:
        period:
          $ref: "#/components/schemas/StreamerSiteAnalyticsPeriod"
        web:
          $ref: "#/components/schemas/StreamerSiteWebAnalytics"
        top_interactions:
          type: array
          maxItems: 10
          items:
            $ref: "#/components/schemas/StreamerSiteInteractionAnalytics"
        mini_games:
          type: array
          maxItems: 8
          items:
            $ref: "#/components/schemas/StreamerSiteMiniGameAnalytics"
        conversions:
          $ref: "#/components/schemas/StreamerSiteConversionAnalytics"
    StreamerSiteAnalyticsPeriod:
      type: object
      additionalProperties: false
      required:
        - key
        - start_at
        - end_at
      properties:
        key:
          type: string
          enum:
            - 7d
            - 30d
            - month
        start_at:
          type: string
          format: date-time
        end_at:
          type: string
          format: date-time
    StreamerSiteWebAnalytics:
      type: object
      additionalProperties: false
      required:
        - impressions
        - unique_visitors
        - clicks
        - unique_clicks
        - ctr
        - unique_ctr
      properties:
        impressions:
          type: integer
          minimum: 0
        unique_visitors:
          type: integer
          minimum: 0
        clicks:
          type: integer
          minimum: 0
        unique_clicks:
          type: integer
          minimum: 0
        ctr:
          type:
            - number
            - "null"
          minimum: 0
          description: Clicks divididos por impresiones; null sin impresiones.
        unique_ctr:
          type:
            - number
            - "null"
          minimum: 0
          description: Visitantes con click divididos por visitantes; null sin visitantes.
    StreamerSiteInteractionAnalytics:
      type: object
      additionalProperties: false
      required:
        - target_type
        - target_key
        - clicks
        - unique_clicks
      properties:
        target_type:
          type: string
          pattern: ^[a-z][a-z0-9_]{1,31}$
        target_key:
          type: string
          minLength: 1
          maxLength: 128
          pattern: ^[A-Za-z0-9][A-Za-z0-9._:-]*$
        clicks:
          type: integer
          minimum: 0
        unique_clicks:
          type: integer
          minimum: 0
    StreamerSiteMiniGameAnalytics:
      type: object
      additionalProperties: false
      required:
        - game
        - plays
        - unique_players
        - points_played
        - points_awarded
      properties:
        game:
          type: string
          enum:
            - flip
            - dice
            - plinko
            - roulette
        plays:
          type: integer
          minimum: 0
        unique_players:
          type: integer
          minimum: 0
        points_played:
          type: integer
          minimum: 0
        points_awarded:
          type: integer
          minimum: 0
    StreamerSiteConversionAnalytics:
      type: object
      additionalProperties: false
      required:
        - community_subscriptions
        - giveaway_entries
        - store_redemptions
      properties:
        community_subscriptions:
          type: integer
          minimum: 0
        giveaway_entries:
          type: integer
          minimum: 0
        store_redemptions:
          type: integer
          minimum: 0
    PublicStreamerSiteSlotCode:
      type: object
      additionalProperties: false
      required:
        - slot_key
        - code
      properties:
        slot_key:
          type: string
          enum:
            - frutas
            - faraon
            - bonanza
            - dragon
            - trebol
            - cofre
        code:
          type: string
          pattern: ^[A-Za-z0-9][A-Za-z0-9._-]{2,63}$
    PublicStore:
      type: object
      additionalProperties: false
      required:
        - sandbox
        - rewards
      properties:
        sandbox:
          type: boolean
          const: true
        rewards:
          type: array
          maxItems: 100
          items:
            $ref: "#/components/schemas/PublicStoreReward"
    PublicMiniGame:
      type: object
      additionalProperties: false
      required:
        - game
        - environment
        - rules_version
        - min_points_cost
        - max_points_cost
        - points_increment
        - award_numerator
        - award_denominator
        - payout_model
        - payout_table
        - configuration_version
      properties:
        game:
          type: string
          enum:
            - flip
            - dice
            - plinko
            - roulette
        environment:
          type: string
          const: sandbox
        rules_version:
          type: integer
          const: 1
        min_points_cost:
          type: integer
          minimum: 50
          maximum: 5000
          multipleOf: 50
        max_points_cost:
          type: integer
          minimum: 50
          maximum: 5000
          multipleOf: 50
        points_increment:
          type: integer
          const: 50
        award_numerator:
          type: integer
          const: 99
        award_denominator:
          type: integer
          const: 50
        payout_model:
          type: string
          enum:
            - fixed
            - plinko-buckets-v1
            - roulette-european-v1
        payout_table:
          oneOf:
            - type: "null"
            - $ref: "#/components/schemas/PlinkoPayoutTable"
        configuration_version:
          type: integer
          minimum: 1
    PublicStoreReward:
      type: object
      additionalProperties: false
      required:
        - id
        - name
        - description
        - image_url
        - points_cost
        - stock_remaining
        - max_per_viewer
        - fulfillment_type
        - sandbox
        - terms_version
        - terms_text
        - valid_from
        - valid_until
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
          minLength: 3
          maxLength: 100
        description:
          type: string
          minLength: 3
          maxLength: 1000
        image_url:
          type:
            - string
            - "null"
          format: uri
          pattern: ^https://
        points_cost:
          type: integer
          minimum: 1
          maximum: 1000000000
        stock_remaining:
          type: integer
          minimum: 0
        max_per_viewer:
          type: integer
          minimum: 1
          maximum: 1000
        fulfillment_type:
          type: string
          const: sandbox
        sandbox:
          type: boolean
          const: true
        terms_version:
          type: string
          pattern: ^[A-Za-z0-9][A-Za-z0-9._-]{0,39}$
        terms_text:
          type: string
          minLength: 20
          maxLength: 10000
        valid_from:
          type: string
          format: date-time
        valid_until:
          type: string
          format: date-time
    KickEventType:
      type: string
      enum:
        - chat.message.sent
        - channel.followed
        - channel.subscription.renewal
        - channel.subscription.gifts
        - channel.subscription.new
        - channel.reward.redemption.updated
        - livestream.status.updated
        - livestream.metadata.updated
        - moderation.banned
        - kicks.gifted
    KickChannel:
      type: object
      additionalProperties: false
      required:
        - broadcaster_user_id
        - slug
        - username
      properties:
        broadcaster_user_id:
          type: string
          pattern: ^[1-9][0-9]*$
        slug:
          type: string
          minLength: 1
          maxLength: 64
        username:
          type: string
          minLength: 1
          maxLength: 100
        profile_picture_url:
          type: string
          format: uri
          pattern: ^https://
    KickEventSubscription:
      type: object
      additionalProperties: false
      required:
        - id
        - event_type
        - version
        - status
      properties:
        id:
          type: string
          minLength: 8
          maxLength: 128
        event_type:
          $ref: "#/components/schemas/KickEventType"
        version:
          type: integer
          const: 1
        status:
          type: string
          enum:
            - active
            - error
        error_code:
          type: string
          pattern: ^[a-z0-9_]{1,64}$
    KickIntegrationStatus:
      type: object
      additionalProperties: false
      required:
        - provider
        - status
        - configuration_ready
        - scopes
        - subscriptions
        - chat_points_enabled
      properties:
        provider:
          type: string
          const: kick
        status:
          type: string
          enum:
            - disconnected
            - connected
            - error
        configuration_ready:
          type: boolean
        channel:
          $ref: "#/components/schemas/KickChannel"
        scopes:
          type: array
          uniqueItems: true
          items:
            type: string
            enum:
              - user:read
              - channel:read
        subscriptions:
          type: array
          maxItems: 10
          items:
            $ref: "#/components/schemas/KickEventSubscription"
        chat_points_enabled:
          type: boolean
        connected_at:
          type: string
          format: date-time
        last_event_at:
          type: string
          format: date-time
        last_error_code:
          type: string
          pattern: ^[a-z0-9_]{1,64}$
    KickAuthorizationInput:
      type: object
      additionalProperties: false
      required:
        - enable_chat_points
      properties:
        enable_chat_points:
          type: boolean
          description: Consentimiento explícito para activar la regla base de 5 puntos con intervalo de 60 segundos.
    KickAuthorizationStart:
      type: object
      additionalProperties: false
      required:
        - authorization_url
        - expires_at
      properties:
        authorization_url:
          type: string
          format: uri
          pattern: ^https://id\.kick\.com/oauth/authorize
        expires_at:
          type: string
          format: date-time
    OverlayEventType:
      type: string
      enum:
        - community.viewer_joined
        - community.code_published
        - store.reward_redeemed
        - giveaway.won
        - achievement.unlocked
        - milestone.reached
    OverlaySettings:
      type: object
      additionalProperties: false
      required:
        - enabled_event_types
        - version
        - updated_at
      properties:
        enabled_event_types:
          type: array
          uniqueItems: true
          maxItems: 5
          items:
            $ref: "#/components/schemas/OverlayEventType"
        overlay_path:
          type: string
          pattern: ^/overlay/[a-z0-9-]+$
        version:
          type: integer
          minimum: 1
        updated_at:
          type: string
          format: date-time
    OverlaySettingsUpdate:
      type: object
      additionalProperties: false
      required:
        - enabled_event_types
        - expected_version
      properties:
        enabled_event_types:
          type: array
          uniqueItems: true
          maxItems: 5
          items:
            $ref: "#/components/schemas/OverlayEventType"
        expected_version:
          type: integer
          minimum: 1
    CasinoConnection:
      type: object
      additionalProperties: false
      required:
        - id
        - display_name
        - slug
        - source
        - status
        - currency
        - ingest_secret_hint
        - version
      properties:
        id:
          type: string
          format: uuid
        display_name:
          type: string
          minLength: 2
          maxLength: 60
        slug:
          type: string
          pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*$"
          minLength: 2
          maxLength: 40
          description: Identificador estable de la conexión. No cambia después de crearla.
        source:
          type: string
          enum: [webhook, manual, native]
          description: >-
            De dónde vienen los números. 'native' lo asigna Convray cuando ese casino
            además es tenant de la plataforma; no se puede pedir desde el panel.
        status:
          type: string
          enum: [active, paused, error]
        currency:
          type: string
          pattern: "^[A-Z]{3}$"
        ingest_secret_hint:
          type: string
          minLength: 4
          maxLength: 12
          description: Últimos caracteres del secreto, para reconocer cuál se configuró dónde.
        referral_url:
          type: string
          format: uri
          nullable: true
          description: Link de afiliado del streamer con este casino.
        referral_token:
          type: string
          nullable: true
          pattern: "^[A-Za-z0-9_-]{43}$"
          description: Token opaco del link medido. El link público es /go/{token}.
        referral_click_param:
          type: string
          nullable: true
          pattern: "^[A-Za-z][A-Za-z0-9_]{0,63}$"
          description: Cómo llama el casino al identificador de click. Si falta, se redirige sin agregar nada.
        last_event_at:
          type: string
          format: date-time
          nullable: true
        last_error_code:
          type: string
          nullable: true
        version:
          type: integer
          minimum: 1
    CreatedCasinoConnection:
      allOf:
        - $ref: "#/components/schemas/CasinoConnection"
        - type: object
          required:
            - ingest_secret
          properties:
            ingest_secret:
              type: string
              description: >-
                Secreto con el que el casino firma cada evento. Se entrega una sola vez,
                al crear la conexión, y después queda cifrado en reposo.
    CasinoConnectionCreate:
      type: object
      additionalProperties: false
      required:
        - display_name
        - source
        - currency
      properties:
        display_name:
          type: string
          minLength: 2
          maxLength: 60
        slug:
          type: string
          pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*$"
          minLength: 2
          maxLength: 40
          description: Opcional; si falta se deriva del nombre visible.
        source:
          type: string
          enum: [webhook, manual]
        currency:
          type: string
          pattern: "^[A-Z]{3}$"
    CasinoConnectionMetrics:
      type: object
      additionalProperties: false
      required:
        - connection_id
        - display_name
        - currency
        - status
        - has_referral_link
        - clicks
        - unique_visitors
        - wager_total_minor
        - active_players
      properties:
        connection_id:
          type: string
          format: uuid
        display_name:
          type: string
          maxLength: 60
        currency:
          type: string
          pattern: "^[A-Z]{3}$"
        status:
          type: string
          enum: [active, paused, error]
        has_referral_link:
          type: boolean
          description: Sin link medido configurado los clicks no se cuentan.
        clicks:
          type: integer
          minimum: 0
        unique_visitors:
          type: integer
          minimum: 0
          description: Personas distintas dentro de esta conexión, no comparables entre conexiones.
        wager_total_minor:
          type: integer
          minimum: 0
          description: Volumen apostado en la unidad menor de la moneda de la conexión.
        active_players:
          type: integer
          minimum: 0
        last_event_at:
          type: string
          format: date-time
          nullable: true
    CasinoDailyPoint:
      type: object
      additionalProperties: false
      required:
        - date
        - clicks
        - wager_total_minor
      properties:
        date:
          type: string
          format: date
        clicks:
          type: integer
          minimum: 0
        wager_total_minor:
          type: integer
          minimum: 0
    CasinoTopPlayer:
      type: object
      additionalProperties: false
      required:
        - player_alias
        - currency
        - wager_total_minor
        - bets
      properties:
        player_alias:
          type: string
          maxLength: 48
        currency:
          type: string
          pattern: "^[A-Z]{3}$"
        wager_total_minor:
          type: integer
          minimum: 0
        bets:
          type: integer
          minimum: 0
    CasinoMetrics:
      type: object
      additionalProperties: false
      required:
        - window_days
        - connections
        - clicks
        - daily
        - top_players
      properties:
        window_days:
          type: integer
          enum: [7, 30, 90]
        connections:
          type: array
          items:
            $ref: "#/components/schemas/CasinoConnectionMetrics"
        clicks:
          type: integer
          minimum: 0
          description: Único total agregado; el resto sólo tiene sentido por conexión.
        daily:
          type: array
          description: >-
            Un punto por día de la ventana, en UTC, incluidos los días sin actividad:
            una serie a la que le faltan los ceros convierte dos picos aislados en
            tráfico sostenido.
          items:
            $ref: "#/components/schemas/CasinoDailyPoint"
        top_players:
          type: array
          maxItems: 8
          items:
            $ref: "#/components/schemas/CasinoTopPlayer"
    CasinoReferralConfigure:
      type: object
      additionalProperties: false
      required:
        - referral_url
        - expected_version
      properties:
        referral_url:
          type: string
          format: uri
          maxLength: 2048
          description: HTTPS al puerto estándar, sin credenciales ni fragmento, que resuelva a una red pública.
        referral_click_param:
          type: string
          pattern: "^[A-Za-z][A-Za-z0-9_]{0,63}$"
          description: Opcional. Si el casino sabe recibir un identificador de click, cómo lo llama.
        expected_version:
          type: integer
          minimum: 1
    CasinoWagerEvent:
      type: object
      additionalProperties: false
      required:
        - external_event_id
        - external_player_ref
        - player_alias
        - wagered_amount_minor
        - currency
        - occurred_at
      properties:
        external_event_id:
          type: string
          minLength: 1
          maxLength: 120
          description: Identificador del evento en el casino. Es la clave de idempotencia.
        external_player_ref:
          type: string
          minLength: 1
          maxLength: 120
          description: Referencia estable del jugador en el casino. No hace falta que tenga cuenta en Convray.
        player_alias:
          type: string
          minLength: 1
          maxLength: 48
          description: Nombre visible del jugador. Se publica enmascarado en el leaderboard.
        wagered_amount_minor:
          type: integer
          minimum: 1
          maximum: 100000000000
          description: Monto apostado en la unidad menor y entera de la moneda.
        currency:
          type: string
          pattern: "^[A-Z]{3}$"
          description: Tiene que coincidir con la moneda de la conexión.
        occurred_at:
          type: string
          format: date-time
    CasinoWagerBatch:
      type: object
      additionalProperties: false
      required:
        - events
      properties:
        events:
          type: array
          minItems: 1
          maxItems: 500
          items:
            $ref: "#/components/schemas/CasinoWagerEvent"
    CasinoWagerIngestResult:
      type: object
      additionalProperties: false
      required:
        - accepted
        - duplicated
      properties:
        accepted:
          type: integer
          minimum: 0
          description: Eventos que Convray no tenía y quedaron guardados.
        duplicated:
          type: integer
          minimum: 0
          description: Eventos cuyo external_event_id ya estaba registrado.
    CasinoConnectionUpdate:
      type: object
      additionalProperties: false
      required:
        - display_name
        - status
        - expected_version
      properties:
        display_name:
          type: string
          minLength: 2
          maxLength: 60
        status:
          type: string
          enum: [active, paused]
          description: "El estado 'error' lo escribe la ingesta, no el panel."
        expected_version:
          type: integer
          minimum: 1
    WagerRacePrize:
      type: object
      additionalProperties: false
      required:
        - rank
        - amount_minor
        - description
      description: >-
        Lo que el casino ofrece por un puesto. Necesita monto, descripción, o ambos:
        un puesto premiado que no ofrece nada no debería estar en la tabla.
      properties:
        rank:
          type: integer
          minimum: 1
          maximum: 20
        amount_minor:
          type: integer
          minimum: 0
          maximum: 100000000000
          description: Monto en la unidad menor y entera de la moneda de la conexión.
        description:
          type: string
          maxLength: 120
          description: "Lo que no es plata: giros, bonos, entradas. Puede ir vacío."
    WagerRaceStanding:
      type: object
      additionalProperties: false
      required:
        - rank
        - external_player_ref
        - public_alias
        - wagered_total
        - prize_amount_minor
        - prize_description
      properties:
        rank:
          type: integer
          minimum: 1
        external_player_ref:
          type: string
          maxLength: 120
          description: >-
            Referencia del jugador tal como la conoce el casino. Sólo la ve el dueño
            de la comunidad; nunca sale por la superficie pública.
        public_alias:
          type: string
          maxLength: 48
        wagered_total:
          type: integer
          minimum: 1
        prize_amount_minor:
          type: integer
          minimum: 0
        prize_description:
          type: string
          maxLength: 120
    WagerRace:
      type: object
      additionalProperties: false
      description: >-
        Una carrera compite por lo apostado en un casino conectado y el premio lo pone
        ese casino. Convray congela la clasificación al vencer la ventana y no acredita
        puntos por ella.
      required:
        - id
        - connection_id
        - title
        - status
        - currency
        - starts_at
        - ends_at
        - minimum_wager
        - prizes
        - standings
        - version
      properties:
        id:
          type: string
          format: uuid
        connection_id:
          type: string
          format: uuid
          description: Casino conectado sobre el que se corre. No cambia después de crearla.
        title:
          type: string
          minLength: 3
          maxLength: 80
        status:
          type: string
          enum: [draft, active, closed]
        currency:
          type: string
          pattern: "^[A-Z]{3}$"
          description: Se hereda de la conexión; el panel no la elige.
        starts_at:
          type: string
          format: date-time
        ends_at:
          type: string
          format: date-time
        minimum_wager:
          type: integer
          minimum: 1
          description: Mínimo apostado en el casino, en unidad menor, para entrar en la clasificación.
        prizes:
          type: array
          maxItems: 20
          items:
            $ref: "#/components/schemas/WagerRacePrize"
        standings:
          type: array
          description: Clasificación congelada al cerrar. Vacía mientras la carrera no cierre.
          items:
            $ref: "#/components/schemas/WagerRaceStanding"
        version:
          type: integer
          minimum: 1
        closed_at:
          type: string
          format: date-time
          nullable: true
    WagerRaceCreate:
      type: object
      additionalProperties: false
      required:
        - connection_id
        - title
        - starts_at
        - ends_at
        - minimum_wager
        - prizes
      properties:
        connection_id:
          type: string
          format: uuid
        title:
          type: string
          minLength: 3
          maxLength: 80
        starts_at:
          type: string
          format: date-time
        ends_at:
          type: string
          format: date-time
        minimum_wager:
          type: integer
          minimum: 1
          maximum: 100000000000
        prizes:
          type: array
          minItems: 1
          maxItems: 20
          items:
            $ref: "#/components/schemas/WagerRacePrize"
    WagerRaceUpdate:
      type: object
      additionalProperties: false
      required:
        - title
        - status
        - starts_at
        - ends_at
        - minimum_wager
        - prizes
        - expected_version
      properties:
        title:
          type: string
          minLength: 3
          maxLength: 80
        status:
          type: string
          enum: [draft, active]
          description: Cerrar no es una transición que el panel pueda pedir.
        starts_at:
          type: string
          format: date-time
        ends_at:
          type: string
          format: date-time
        minimum_wager:
          type: integer
          minimum: 1
          maximum: 100000000000
        prizes:
          type: array
          minItems: 1
          maxItems: 20
          items:
            $ref: "#/components/schemas/WagerRacePrize"
        expected_version:
          type: integer
          minimum: 1
    PublicWagerRace:
      type: object
      additionalProperties: false
      required:
        - id
        - title
        - status
        - currency
        - casino_name
        - join_path
        - starts_at
        - ends_at
        - minimum_wager
        - prizes
        - standings
      properties:
        id:
          type: string
          format: uuid
        title:
          type: string
        status:
          type: string
          enum: [active, closed]
        currency:
          type: string
          pattern: "^[A-Z]{3}$"
        casino_name:
          type: string
          maxLength: 60
        join_path:
          type: string
          pattern: "^(|/go/[A-Za-z0-9_-]{43})$"
          description: >-
            Ruta interna del link medido. Va vacía si el streamer todavía no lo
            configuró, y en ese caso la carrera se muestra sin CTA. Nunca es una URL
            absoluta: eso convertiría la página en un redirect abierto.
        starts_at:
          type: string
          format: date-time
        ends_at:
          type: string
          format: date-time
        minimum_wager:
          type: integer
          minimum: 1
        prizes:
          type: array
          items:
            $ref: "#/components/schemas/WagerRacePrize"
        standings:
          type: array
          description: En una carrera activa se calcula en vivo; en una cerrada sale de la foto congelada.
          items:
            $ref: "#/components/schemas/PublicWagerRaceStanding"
    PublicWagerRaceStanding:
      type: object
      additionalProperties: false
      description: >-
        Fila pública del leaderboard. El alias va enmascarado y la referencia del
        jugador en el casino nunca sale por esta superficie.
      required:
        - rank
        - public_alias
        - wagered_total
        - prize_amount_minor
        - prize_description
      properties:
        rank:
          type: integer
          minimum: 1
        public_alias:
          type: string
          maxLength: 48
        wagered_total:
          type: integer
          minimum: 1
        prize_amount_minor:
          type: integer
          minimum: 0
        prize_description:
          type: string
          maxLength: 120
    ChatCodeSettings:
      type: object
      additionalProperties: false
      required:
        - enabled
        - max_amount
      properties:
        enabled:
          type: boolean
          description: Nace en false; emitir puntos desde un mensaje es autoridad concedida explícitamente.
        max_amount:
          type: integer
          minimum: 1
          maximum: 100000
          description: Techo por código emitido al aire, más bajo que el del panel.
    ChatCodeSettingsUpdate:
      type: object
      additionalProperties: false
      required:
        - enabled
        - max_amount
      properties:
        enabled:
          type: boolean
        max_amount:
          type: integer
          minimum: 1
          maximum: 100000
    RedemptionCode:
      type: object
      additionalProperties: false
      required:
        - id
        - code
        - display_name
        - amount
        - redeemed_count
        - status
        - version
        - created_at
      properties:
        id:
          type: string
          format: uuid
        code:
          type: string
          pattern: ^[A-Z0-9][A-Z0-9-]{2,31}$
        display_name:
          type: string
          minLength: 2
          maxLength: 80
        amount:
          type: integer
          minimum: 1
        max_redemptions:
          type: integer
          nullable: true
          minimum: 1
        redeemed_count:
          type: integer
          minimum: 0
        status:
          type: string
          enum: [active, disabled]
        valid_from:
          type: string
          format: date-time
          nullable: true
        valid_until:
          type: string
          format: date-time
          nullable: true
        version:
          type: integer
          minimum: 1
        created_at:
          type: string
          format: date-time
    RedemptionCodeCreate:
      type: object
      additionalProperties: false
      required:
        - code
        - display_name
        - amount
      properties:
        code:
          type: string
          minLength: 3
          maxLength: 32
          description: Se normaliza a mayúsculas. Sólo letras, dígitos y guiones simples, para dictarlo al aire sin ambigüedad.
        display_name:
          type: string
          minLength: 2
          maxLength: 80
        amount:
          type: integer
          minimum: 1
          maximum: 1000000
        max_redemptions:
          type: integer
          nullable: true
          minimum: 1
          maximum: 1000000
        valid_from:
          type: string
          format: date-time
          nullable: true
        valid_until:
          type: string
          format: date-time
          nullable: true
    RedemptionCodeUpdate:
      type: object
      additionalProperties: false
      required:
        - display_name
        - status
        - expected_version
      properties:
        display_name:
          type: string
          minLength: 2
          maxLength: 80
        status:
          type: string
          enum: [active, disabled]
        max_redemptions:
          type: integer
          nullable: true
          minimum: 1
          maximum: 1000000
        valid_from:
          type: string
          format: date-time
          nullable: true
        valid_until:
          type: string
          format: date-time
          nullable: true
        expected_version:
          type: integer
          minimum: 1
    RedemptionCodeRedeem:
      type: object
      additionalProperties: false
      required:
        - streamer_slug
        - code
      properties:
        streamer_slug:
          type: string
          pattern: ^[a-z0-9]+(?:-[a-z0-9]+)*$
        code:
          type: string
          minLength: 3
          maxLength: 32
    RedemptionCodeRedeemResult:
      type: object
      additionalProperties: false
      required:
        - code
        - display_name
        - amount
        - balance
        - replayed
      properties:
        code:
          type: string
        display_name:
          type: string
        amount:
          type: integer
          minimum: 1
        balance:
          type: integer
          minimum: 0
        replayed:
          type: boolean
          description: True cuando el viewer ya había canjeado el código y no se acreditó de nuevo.
    PointRule:
      type: object
      additionalProperties: false
      required:
        - id
        - name
        - source_type
        - source_status
        - status
        - amount
        - frequency_seconds
        - version
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
          minLength: 2
          maxLength: 100
        source_type:
          type: string
          description: Fuente de evidencia que alimenta la regla.
        source_status:
          type: string
          enum: [available, unavailable, degraded, suspended]
          description: Una regla sólo puede activarse si su fuente está available.
        status:
          type: string
          enum: [draft, active, paused, archived]
        amount:
          type: integer
          minimum: 1
          description: Puntos acreditados por evento válido.
        frequency_seconds:
          type: integer
          minimum: 0
          description: Ventana mínima entre acreditaciones al mismo viewer. Cero acredita cada evento.
        per_viewer_limit:
          type: integer
          nullable: true
          minimum: 1
        global_limit:
          type: integer
          nullable: true
          minimum: 1
        valid_from:
          type: string
          format: date-time
          nullable: true
        valid_until:
          type: string
          format: date-time
          nullable: true
        version:
          type: integer
          minimum: 1
    PointRuleUpdate:
      type: object
      additionalProperties: false
      required:
        - name
        - status
        - amount
        - frequency_seconds
        - expected_version
      properties:
        name:
          type: string
          minLength: 2
          maxLength: 100
        status:
          type: string
          enum: [draft, active, paused]
          description: El retiro definitivo (archived) no se expone acá porque afecta el historial acreditado.
        amount:
          type: integer
          minimum: 1
          maximum: 1000000
        frequency_seconds:
          type: integer
          minimum: 0
          maximum: 86400
        per_viewer_limit:
          type: integer
          nullable: true
          minimum: 1
          maximum: 100000000
        global_limit:
          type: integer
          nullable: true
          minimum: 1
          maximum: 1000000000
        valid_from:
          type: string
          format: date-time
          nullable: true
        valid_until:
          type: string
          format: date-time
          nullable: true
        expected_version:
          type: integer
          minimum: 1
    ConvrayOverlayEvent:
      type: object
      additionalProperties: false
      required:
        - id
        - event_type
        - display_text
        - occurred_at
      properties:
        id:
          type: string
          format: uuid
        event_type:
          $ref: "#/components/schemas/OverlayEventType"
        actor_alias:
          type: string
          minLength: 2
          maxLength: 48
        actor_avatar_url:
          type: string
          format: uri
          pattern: ^https://
        display_text:
          type: string
          minLength: 1
          maxLength: 240
        occurred_at:
          type: string
          format: date-time
    ConvrayOverlayFeed:
      type: object
      additionalProperties: false
      required:
        - server_time
        - events
      properties:
        server_time:
          type: string
          format: date-time
        events:
          type: array
          maxItems: 20
          items:
            $ref: "#/components/schemas/ConvrayOverlayEvent"
    YouTubeChannel:
      type: object
      additionalProperties: false
      required:
        - id
        - title
      properties:
        id:
          type: string
          minLength: 1
        title:
          type: string
          minLength: 1
        handle:
          type: string
        thumbnail_url:
          type: string
          format: uri
    YouTubeIntegrationStatus:
      type: object
      additionalProperties: false
      required:
        - provider
        - status
        - configuration_ready
        - scopes
        - video_count
        - short_count
        - sync_in_progress
      properties:
        provider:
          type: string
          const: youtube
        status:
          type: string
          enum:
            - disconnected
            - connected
            - error
        configuration_ready:
          type: boolean
        channel:
          $ref: "#/components/schemas/YouTubeChannel"
        scopes:
          type: array
          uniqueItems: true
          items:
            type: string
        connected_at:
          type: string
          format: date-time
        last_synced_at:
          type: string
          format: date-time
        next_sync_at:
          type: string
          format: date-time
        last_error_code:
          type: string
        video_count:
          type: integer
          minimum: 0
        short_count:
          type: integer
          minimum: 0
        sync_in_progress:
          type: boolean
    YouTubeAuthorizationStart:
      type: object
      additionalProperties: false
      required:
        - authorization_url
        - expires_at
      properties:
        authorization_url:
          type: string
          format: uri
          pattern: ^https://accounts\\.google\\.com/
        expires_at:
          type: string
          format: date-time
    CasinoOnboardingInput:
      type: object
      additionalProperties: false
      required:
        - display_name
        - slug
        - legal_entity_ref
        - document_ref
        - contract_effective_from
        - owner_email
        - invitation_expires_at
        - reason
      properties:
        display_name:
          type: string
          minLength: 2
          maxLength: 120
        slug:
          type: string
          minLength: 3
          maxLength: 63
          pattern: ^[a-z0-9]+(?:-[a-z0-9]+)*$
        legal_entity_ref:
          type: string
          minLength: 3
          maxLength: 120
        document_ref:
          type: string
          minLength: 3
          maxLength: 240
        contract_effective_from:
          type: string
          format: date-time
        contract_effective_until:
          type:
            - string
            - "null"
          format: date-time
        owner_email:
          type: string
          format: email
          maxLength: 254
        invitation_expires_at:
          type: string
          format: date-time
        reason:
          type: string
          minLength: 10
          maxLength: 500
    CasinoContractSummary:
      type: object
      additionalProperties: false
      required:
        - id
        - status
        - effective_from
        - effective_until
      properties:
        id:
          type: string
          format: uuid
        status:
          type: string
          const: active
        effective_from:
          type: string
          format: date-time
        effective_until:
          type:
            - string
            - "null"
          format: date-time
    CasinoInvitationSummary:
      type: object
      additionalProperties: false
      required:
        - id
        - email
        - role
        - expires_at
        - token
      properties:
        id:
          type: string
          format: uuid
        email:
          type: string
          format: email
        role:
          type: string
          enum:
            - owner
            - member
    CasinoTeamMember:
      type: object
      additionalProperties: false
      required: [membership_id, email, role, status, version, created_at]
      properties:
        membership_id:
          type: string
          format: uuid
        email:
          type: string
          format: email
        role:
          type: string
          enum: [owner, member]
        status:
          type: string
          enum: [active, suspended, revoked]
        version:
          type: integer
          minimum: 1
        created_at:
          type: string
          format: date-time
    CasinoTeamInvitation:
      type: object
      additionalProperties: false
      required: [id, email, role, status, expires_at, version, created_at]
      properties:
        id:
          type: string
          format: uuid
        email:
          type: string
          format: email
        role:
          type: string
          const: member
        status:
          type: string
          enum: [pending, accepted, revoked, expired]
        expires_at:
          type: string
          format: date-time
        version:
          type: integer
          minimum: 1
        created_at:
          type: string
          format: date-time
        token:
          type: string
          minLength: 80
          writeOnly: true
          description: Solo está presente en la respuesta inmediata de creación; la web oficial lo incorpora al fragmento de un enlace de un solo uso.
        email_sent:
          type: boolean
          description: Solo al crear o regenerar. true si la invitación se envió por correo; false si no hay proveedor o el envío falló (compartir el enlace a mano).
    CasinoTeam:
      type: object
      additionalProperties: false
      required: [members, invitations]
      properties:
        members:
          type: array
          maxItems: 100
          items:
            $ref: "#/components/schemas/CasinoTeamMember"
        invitations:
          type: array
          maxItems: 100
          items:
            $ref: "#/components/schemas/CasinoTeamInvitation"
    CasinoTeamInvitationCreateInput:
      type: object
      additionalProperties: false
      required: [email, expires_at]
      properties:
        email:
          type: string
          format: email
          maxLength: 254
        expires_at:
          type: string
          format: date-time
    CasinoTeamMutationInput:
      type: object
      additionalProperties: false
      required: [expected_version, reason]
      properties:
        expected_version:
          type: integer
          minimum: 1
        reason:
          type: string
          minLength: 3
          maxLength: 240
    CasinoTeamMemberUpdateInput:
      type: object
      additionalProperties: false
      required: [status, expected_version, reason]
      properties:
        status:
          type: string
          enum: [active, suspended, revoked]
        expected_version:
          type: integer
          minimum: 1
        reason:
          type: string
          minLength: 3
          maxLength: 240
        expires_at:
          type: string
          format: date-time
        token:
          type: string
          minLength: 80
          writeOnly: true
          description: Secreto devuelto una sola vez; nunca se persiste en claro.
    CasinoOnboardingResult:
      type: object
      additionalProperties: false
      required:
        - casino
        - contract
        - landing_id
        - invitation
      properties:
        casino:
          $ref: "#/components/schemas/CasinoTenantSummary"
        contract:
          $ref: "#/components/schemas/CasinoContractSummary"
        landing_id:
          type: string
          format: uuid
        invitation:
          $ref: "#/components/schemas/CasinoInvitationSummary"
    Deal:
      type: object
      additionalProperties: false
      required:
        - id
        - code
        - name
        - commercial_model
        - currency
        - cpa_amount_minor
        - revshare_bps
        - status
        - valid_from
        - valid_until
        - version
        - casino
        - streamer
      properties:
        id:
          type: string
          format: uuid
        code:
          type: string
        name:
          type: string
        commercial_model:
          type: string
          enum:
            - cpa
            - revshare
            - hybrid
        currency:
          type: string
          pattern: ^[A-Z]{3}$
        cpa_amount_minor:
          type:
            - integer
            - "null"
          minimum: 0
        revshare_bps:
          type:
            - integer
            - "null"
          minimum: 0
          maximum: 10000
        status:
          type: string
          enum:
            - draft
            - active
            - suspended
            - ended
        valid_from:
          type: string
          format: date-time
        valid_until:
          type:
            - string
            - "null"
          format: date-time
        version:
          type: integer
          minimum: 1
        casino:
          $ref: "#/components/schemas/CasinoTenantSummary"
        streamer:
          $ref: "#/components/schemas/PartnerTenantSummary"
    DealList:
      type: object
      additionalProperties: false
      required:
        - items
      properties:
        items:
          type: array
          maxItems: 100
          items:
            $ref: "#/components/schemas/Deal"
    CasinoInvitationAcceptanceInput:
      type: object
      additionalProperties: false
      required:
        - token
        - password
      properties:
        token:
          type: string
          minLength: 80
          writeOnly: true
        password:
          type: string
          minLength: 12
          maxLength: 72
          writeOnly: true
    CasinoInvitationAcceptance:
      type: object
      additionalProperties: false
      required:
        - user_id
        - tenant_id
        - tenant_slug
        - role
      properties:
        user_id:
          type: string
          format: uuid
        tenant_id:
          type: string
          format: uuid
        tenant_slug:
          type: string
          pattern: ^[a-z0-9]+(?:-[a-z0-9]+)*$
        role:
          type: string
          const: owner
    CasinoLandingSettings:
      type: object
      additionalProperties: false
      required:
        - id
        - slug
        - display_name
        - headline
        - description
        - accent_color
        - publication_status
        - has_unpublished_changes
        - version
        - published_at
        - updated_at
      properties:
        id:
          type: string
          format: uuid
        slug:
          type: string
          pattern: ^[a-z0-9]+(?:-[a-z0-9]+)*$
        display_name:
          type: string
          minLength: 1
          maxLength: 80
        headline:
          type: string
          maxLength: 160
        description:
          type: string
          maxLength: 1200
        accent_color:
          type: string
          pattern: ^#[0-9A-F]{6}$
        publication_status:
          type: string
          enum:
            - draft
            - published
            - unpublished
        has_unpublished_changes:
          type: boolean
        version:
          type: integer
          minimum: 1
        published_at:
          type:
            - string
            - "null"
          format: date-time
        updated_at:
          type: string
          format: date-time
    CasinoLandingUpdateInput:
      type: object
      additionalProperties: false
      required:
        - expected_version
      properties:
        display_name:
          type: string
          minLength: 1
          maxLength: 80
        headline:
          type: string
          maxLength: 160
        description:
          type: string
          maxLength: 1200
        accent_color:
          type: string
          pattern: ^#[0-9A-F]{6}$
        publication_status:
          type: string
          enum:
            - draft
            - published
            - unpublished
        expected_version:
          type: integer
          minimum: 1
    CasinoDashboard:
      type: object
      additionalProperties: false
      required:
        - casino
        - landing
        - programs
        - offers
        - deals
      properties:
        casino:
          $ref: "#/components/schemas/CasinoTenantSummary"
        landing:
          $ref: "#/components/schemas/CasinoLandingSettings"
        programs:
          $ref: "#/components/schemas/AffiliateProgramList"
        offers:
          $ref: "#/components/schemas/OfferList"
        deals:
          $ref: "#/components/schemas/DealList"
    AffiliateProgram:
      type: object
      additionalProperties: false
      required:
        - id
        - code
        - name
        - brand_name
        - country_code
        - currency
        - status
        - terms_version
        - terms_url
        - terms_summary
        - version
        - created_at
        - updated_at
      properties:
        id:
          type: string
          format: uuid
        code:
          type: string
          minLength: 3
          maxLength: 63
          pattern: ^[a-z0-9]+(?:-[a-z0-9]+)*$
        name:
          type: string
          minLength: 2
          maxLength: 120
        brand_name:
          type: string
          minLength: 2
          maxLength: 120
        country_code:
          type: string
          pattern: ^[A-Z]{2}$
        currency:
          type: string
          pattern: ^[A-Z]{3}$
        status:
          type: string
          enum:
            - draft
            - published
            - paused
            - archived
        terms_version:
          type: string
          minLength: 3
          maxLength: 80
        terms_url:
          type: string
          format: uri
          maxLength: 500
          pattern: ^https://
        terms_summary:
          type: string
          minLength: 10
          maxLength: 1200
        version:
          type: integer
          minimum: 1
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    AffiliateProgramList:
      type: object
      additionalProperties: false
      required:
        - items
      properties:
        items:
          type: array
          maxItems: 100
          items:
            $ref: "#/components/schemas/AffiliateProgram"
    AffiliateProgramCreateInput:
      type: object
      additionalProperties: false
      required:
        - code
        - name
        - brand_name
        - country_code
        - currency
        - terms_version
        - terms_url
        - terms_summary
      properties:
        code:
          type: string
          minLength: 3
          maxLength: 63
          pattern: ^[a-z0-9]+(?:-[a-z0-9]+)*$
        name:
          type: string
          minLength: 2
          maxLength: 120
        brand_name:
          type: string
          minLength: 2
          maxLength: 120
        country_code:
          type: string
          pattern: ^[A-Z]{2}$
        currency:
          type: string
          pattern: ^[A-Z]{3}$
        status:
          type: string
          enum:
            - draft
            - published
            - paused
            - archived
          default: draft
        terms_version:
          type: string
          minLength: 3
          maxLength: 80
        terms_url:
          type: string
          format: uri
          maxLength: 500
          pattern: ^https://
        terms_summary:
          type: string
          minLength: 10
          maxLength: 1200
    AffiliateProgramUpdateInput:
      type: object
      additionalProperties: false
      required:
        - expected_version
      properties:
        name:
          type: string
          minLength: 2
          maxLength: 120
        brand_name:
          type: string
          minLength: 2
          maxLength: 120
        country_code:
          type: string
          pattern: ^[A-Z]{2}$
        currency:
          type: string
          pattern: ^[A-Z]{3}$
        status:
          type: string
          enum:
            - draft
            - published
            - paused
            - archived
        terms_version:
          type: string
          minLength: 3
          maxLength: 80
        terms_url:
          type: string
          format: uri
          maxLength: 500
          pattern: ^https://
        terms_summary:
          type: string
          minLength: 10
          maxLength: 1200
        expected_version:
          type: integer
          minimum: 1
    OfferVersion:
      type: object
      additionalProperties: false
      required:
        - id
        - offer_id
        - version_number
        - commercial_model
        - cpa_amount_minor
        - revshare_bps
        - revshare_basis
        - currency
        - qualification_event
        - criteria_summary
        - terms_version
        - terms_url
        - valid_from
        - valid_until
        - published_at
        - created_at
      properties:
        id:
          type: string
          format: uuid
        offer_id:
          type: string
          format: uuid
        version_number:
          type: integer
          minimum: 1
        commercial_model:
          type: string
          enum:
            - cpa
            - revshare
            - hybrid
        cpa_amount_minor:
          type:
            - integer
            - "null"
          minimum: 0
        revshare_bps:
          type:
            - integer
            - "null"
          minimum: 1
          maximum: 10000
          description: Puntos base sobre NGR; 3000 equivale a 30%.
        revshare_basis:
          type:
            - string
            - "null"
          enum:
            - ngr
            - null
        currency:
          type: string
          pattern: ^[A-Z]{3}$
        qualification_event:
          type: string
          const: player.first_deposit
        criteria_summary:
          type: string
          minLength: 10
          maxLength: 1000
        terms_version:
          type: string
          minLength: 3
          maxLength: 80
        terms_url:
          type: string
          format: uri
          maxLength: 500
          pattern: ^https://
        valid_from:
          type: string
          format: date-time
        valid_until:
          type:
            - string
            - "null"
          format: date-time
        published_at:
          type:
            - string
            - "null"
          format: date-time
        created_at:
          type: string
          format: date-time
    Offer:
      type: object
      additionalProperties: false
      required:
        - id
        - program_id
        - code
        - name
        - status
        - current_published_version_id
        - version
        - created_at
        - updated_at
        - versions
      properties:
        id:
          type: string
          format: uuid
        program_id:
          type: string
          format: uuid
        code:
          type: string
          minLength: 3
          maxLength: 63
          pattern: ^[a-z0-9]+(?:-[a-z0-9]+)*$
        name:
          type: string
          minLength: 2
          maxLength: 120
        status:
          type: string
          enum:
            - draft
            - published
            - paused
            - archived
        current_published_version_id:
          type:
            - string
            - "null"
          format: uuid
        version:
          type: integer
          minimum: 1
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        versions:
          type: array
          items:
            $ref: "#/components/schemas/OfferVersion"
    OfferList:
      type: object
      additionalProperties: false
      required:
        - items
      properties:
        items:
          type: array
          maxItems: 100
          items:
            $ref: "#/components/schemas/Offer"
    OfferCreateInput:
      type: object
      additionalProperties: false
      required:
        - program_id
        - code
        - name
      properties:
        program_id:
          type: string
          format: uuid
        code:
          type: string
          minLength: 3
          maxLength: 63
          pattern: ^[a-z0-9]+(?:-[a-z0-9]+)*$
        name:
          type: string
          minLength: 2
          maxLength: 120
    OfferVersionCreateInput:
      type: object
      additionalProperties: false
      required:
        - commercial_model
        - criteria_summary
        - terms_version
        - terms_url
        - valid_from
      properties:
        commercial_model:
          type: string
          enum:
            - cpa
            - revshare
            - hybrid
        cpa_amount_minor:
          type:
            - integer
            - "null"
          minimum: 0
        revshare_bps:
          type:
            - integer
            - "null"
          minimum: 1
          maximum: 10000
          description: Puntos base contractuales sobre NGR.
        criteria_summary:
          type: string
          minLength: 10
          maxLength: 1000
        terms_version:
          type: string
          minLength: 3
          maxLength: 80
        terms_url:
          type: string
          format: uri
          maxLength: 500
          pattern: ^https://
        valid_from:
          type: string
          format: date-time
        valid_until:
          type:
            - string
            - "null"
          format: date-time
      oneOf:
        - title: CPA
          required:
            - cpa_amount_minor
          properties:
            commercial_model:
              const: cpa
            cpa_amount_minor:
              type: integer
              minimum: 0
            revshare_bps:
              type: "null"
        - title: RevShare sobre NGR
          required:
            - revshare_bps
          properties:
            commercial_model:
              const: revshare
            cpa_amount_minor:
              type: "null"
            revshare_bps:
              type: integer
              minimum: 1
              maximum: 10000
        - title: Hybrid CPA más RevShare sobre NGR
          required:
            - cpa_amount_minor
            - revshare_bps
          properties:
            commercial_model:
              const: hybrid
            cpa_amount_minor:
              type: integer
              minimum: 0
            revshare_bps:
              type: integer
              minimum: 1
              maximum: 10000
    PublicAffiliateProgram:
      type: object
      additionalProperties: false
      required:
        - casino_slug
        - display_name
        - headline
        - description
        - accent_color
        - program_code
        - program_name
        - brand_name
        - country_code
        - currency
        - terms_version
        - terms_url
        - terms_summary
        - published_at
      properties:
        casino_slug:
          type: string
          pattern: ^[a-z0-9]+(?:-[a-z0-9]+)*$
        display_name:
          type: string
          minLength: 1
          maxLength: 80
        headline:
          type: string
          maxLength: 160
        description:
          type: string
          maxLength: 1200
        accent_color:
          type: string
          pattern: ^#[0-9A-F]{6}$
        program_code:
          type: string
          minLength: 3
          maxLength: 63
          pattern: ^[a-z0-9]+(?:-[a-z0-9]+)*$
        program_name:
          type: string
          minLength: 2
          maxLength: 120
        brand_name:
          type: string
          minLength: 2
          maxLength: 120
        country_code:
          type: string
          pattern: ^[A-Z]{2}$
        currency:
          type: string
          pattern: ^[A-Z]{3}$
        terms_version:
          type: string
          minLength: 3
          maxLength: 80
        terms_url:
          type: string
          format: uri
          maxLength: 500
          pattern: ^https://
        terms_summary:
          type: string
          minLength: 10
          maxLength: 1200
        published_at:
          type: string
          format: date-time
    PublicOffer:
      type: object
      additionalProperties: false
      required:
        - code
        - name
        - version_number
        - commercial_model
        - cpa_amount_minor
        - revshare_bps
        - revshare_basis
        - currency
        - qualification_event
        - criteria_summary
        - terms_version
        - terms_url
        - valid_from
        - valid_until
        - published_at
      properties:
        code:
          type: string
          minLength: 3
          maxLength: 63
          pattern: ^[a-z0-9]+(?:-[a-z0-9]+)*$
        name:
          type: string
          minLength: 2
          maxLength: 120
        version_number:
          type: integer
          minimum: 1
        commercial_model:
          type: string
          enum:
            - cpa
            - revshare
            - hybrid
        cpa_amount_minor:
          type:
            - integer
            - "null"
          minimum: 0
        revshare_bps:
          type:
            - integer
            - "null"
          minimum: 1
          maximum: 10000
          description: Puntos base contractuales sobre NGR.
        revshare_basis:
          type:
            - string
            - "null"
          enum:
            - ngr
            - null
        currency:
          type: string
          pattern: ^[A-Z]{3}$
        qualification_event:
          type: string
          const: player.first_deposit
        criteria_summary:
          type: string
          minLength: 10
          maxLength: 1000
        terms_version:
          type: string
          minLength: 3
          maxLength: 80
        terms_url:
          type: string
          format: uri
          maxLength: 500
          pattern: ^https://
        valid_from:
          type: string
          format: date-time
        valid_until:
          type:
            - string
            - "null"
          format: date-time
        published_at:
          type: string
          format: date-time
    PublicOfferList:
      type: object
      additionalProperties: false
      required:
        - items
      properties:
        items:
          type: array
          maxItems: 100
          items:
            $ref: "#/components/schemas/PublicOffer"
    PartnerRegistrationInput:
      type: object
      additionalProperties: false
      required:
        - email
        - password
        - partner_slug
        - display_name
        - country_code
        - terms_version
        - accepted_terms
      properties:
        email:
          type: string
          format: email
          maxLength: 254
        password:
          type: string
          minLength: 12
          maxLength: 72
          writeOnly: true
        partner_slug:
          type: string
          description: >-
            Identificador público del partner: único en toda la plataforma e inmutable después del alta. Además del formato, se rechazan con error de validación en partner_slug los identificadores reservados para rutas de Convray (por ejemplo login, dashboard, docs, tracking o una sección del dashboard del casino), con el mensaje "Ese identificador está reservado para rutas de Convray; elige otro."
          minLength: 3
          maxLength: 63
          pattern: ^[a-z0-9]+(?:-[a-z0-9]+)*$
        display_name:
          type: string
          minLength: 2
          maxLength: 120
        country_code:
          type: string
          pattern: ^[A-Z]{2}$
        terms_version:
          type: string
          const: partner-registration-2026-07-21
        accepted_terms:
          type: boolean
          const: true
        creator_enabled:
          type: boolean
          default: false
          description: Habilita la extensión creator histórica sin cambiar el tipo partner.
    PartnerChannelInput:
      type: object
      additionalProperties: false
      required:
        - channel_type
        - url
      properties:
        channel_type:
          type: string
          enum:
            - website
            - streaming
            - social
            - community
            - media_buying
            - email
        url:
          type: string
          format: uri
          maxLength: 500
          pattern: ^https://
        audience_size:
          type:
            - integer
            - "null"
          minimum: 0
    PartnerChannel:
      type: object
      additionalProperties: false
      required:
        - id
        - channel_type
        - url
        - audience_size
        - verification_status
      properties:
        id:
          type: string
          format: uuid
        channel_type:
          type: string
          enum:
            - website
            - streaming
            - social
            - community
            - media_buying
            - email
        url:
          type: string
          format: uri
          maxLength: 500
          pattern: ^https://
        audience_size:
          type:
            - integer
            - "null"
          minimum: 0
        verification_status:
          type: string
          enum:
            - declared
            - verified
            - rejected
    PartnerProfile:
      type: object
      additionalProperties: false
      required:
        - tenant_id
        - slug
        - display_name
        - status
        - country_code
        - description
        - website_url
        - terms_version
        - terms_accepted_at
        - email_verified_at
        - creator_enabled
        - operating_model
        - home_casino_tenant_id
        - version
        - updated_at
        - channels
      properties:
        tenant_id:
          type: string
          format: uuid
        slug:
          type: string
          minLength: 3
          maxLength: 63
          pattern: ^[a-z0-9]+(?:-[a-z0-9]+)*$
        display_name:
          type: string
          minLength: 2
          maxLength: 120
        status:
          type: string
          enum:
            - pending_verification
            - active
            - suspended
            - closed
        country_code:
          type:
            - string
            - "null"
          pattern: ^[A-Z]{2}$
        description:
          type: string
          maxLength: 500
        website_url:
          oneOf:
            - type: string
              format: uri
              maxLength: 500
              pattern: ^https://
            - type: "null"
        terms_version:
          type:
            - string
            - "null"
          minLength: 3
          maxLength: 80
        terms_accepted_at:
          oneOf:
            - type: string
              format: date-time
            - type: "null"
        email_verified_at:
          oneOf:
            - type: string
              format: date-time
            - type: "null"
        creator_enabled:
          type: boolean
          readOnly: true
          description: Capability histórica habilitada durante el registro partner.
        operating_model:
          type: string
          readOnly: true
          enum:
            - global
            - managed_exclusive
          description: "`managed_exclusive` fija el workspace a un casino principal sin deshabilitar creator."
        home_casino_tenant_id:
          type:
            - string
            - "null"
          format: uuid
          readOnly: true
          description: "Casino principal inmutable cuando `operating_model` es `managed_exclusive`."
        version:
          type: integer
          minimum: 1
        updated_at:
          type: string
          format: date-time
        channels:
          type: array
          maxItems: 20
          items:
            $ref: "#/components/schemas/PartnerChannel"
    PartnerProfileUpdateInput:
      type: object
      additionalProperties: false
      required:
        - expected_version
      properties:
        display_name:
          type: string
          minLength: 2
          maxLength: 120
        country_code:
          type: string
          pattern: ^[A-Z]{2}$
        description:
          type: string
          maxLength: 500
        website_url:
          type: string
          maxLength: 500
          pattern: ^(|https://)
          description: URL HTTPS o cadena vacía para eliminarla.
        channels:
          type: array
          maxItems: 20
          items:
            $ref: "#/components/schemas/PartnerChannelInput"
        expected_version:
          type: integer
          minimum: 1
    RelationshipParty:
      type: object
      additionalProperties: false
      required:
        - id
        - slug
        - display_name
      properties:
        id:
          type: string
          format: uuid
        slug:
          type: string
          pattern: ^[a-z0-9]+(?:-[a-z0-9]+)*$
        display_name:
          type: string
          minLength: 1
          maxLength: 120
    RelationshipProgramSnapshot:
      type: object
      additionalProperties: false
      required:
        - id
        - code
        - name
        - brand_name
        - country_code
        - currency
        - version
        - terms_version
        - terms_url
        - terms_summary
      properties:
        id:
          type: string
          format: uuid
        code:
          type: string
          pattern: ^[a-z0-9]+(?:-[a-z0-9]+)*$
        name:
          type: string
          minLength: 2
          maxLength: 120
        brand_name:
          type: string
          minLength: 2
          maxLength: 120
        country_code:
          type: string
          pattern: ^[A-Z]{2}$
        currency:
          type: string
          pattern: ^[A-Z]{3}$
        version:
          type: integer
          minimum: 1
        terms_version:
          type: string
          minLength: 3
          maxLength: 80
        terms_url:
          type: string
          format: uri
          pattern: ^https://
        terms_summary:
          type: string
          minLength: 10
          maxLength: 1200
    RelationshipOfferSnapshot:
      type: object
      additionalProperties: false
      required:
        - id
        - version_id
        - code
        - name
        - version_number
        - commercial_model
        - cpa_amount_minor
        - revshare_bps
        - revshare_basis
        - currency
        - qualification_event
        - criteria_summary
        - terms_version
        - terms_url
        - valid_from
        - valid_until
      properties:
        id:
          type: string
          format: uuid
        version_id:
          type: string
          format: uuid
        code:
          type: string
          pattern: ^[a-z0-9]+(?:-[a-z0-9]+)*$
        name:
          type: string
          minLength: 2
          maxLength: 120
        version_number:
          type: integer
          minimum: 1
        commercial_model:
          type: string
          enum:
            - cpa
            - revshare
            - hybrid
        cpa_amount_minor:
          type:
            - integer
            - "null"
          minimum: 0
        revshare_bps:
          type:
            - integer
            - "null"
          minimum: 1
          maximum: 10000
        revshare_basis:
          type:
            - string
            - "null"
          const: ngr
        currency:
          type: string
          pattern: ^[A-Z]{3}$
        qualification_event:
          type: string
          const: player.first_deposit
        criteria_summary:
          type: string
          minLength: 10
          maxLength: 1000
        terms_version:
          type: string
          minLength: 3
          maxLength: 80
        terms_url:
          type: string
          format: uri
          pattern: ^https://
        valid_from:
          type: string
          format: date-time
        valid_until:
          type:
            - string
            - "null"
          format: date-time
      allOf:
        - if:
            properties:
              commercial_model:
                const: cpa
          then:
            properties:
              cpa_amount_minor:
                type: integer
                minimum: 0
              revshare_bps:
                type: "null"
              revshare_basis:
                type: "null"
        - if:
            properties:
              commercial_model:
                const: revshare
          then:
            properties:
              cpa_amount_minor:
                type: "null"
              revshare_bps:
                type: integer
                minimum: 1
                maximum: 10000
              revshare_basis:
                type: string
                const: ngr
        - if:
            properties:
              commercial_model:
                const: hybrid
          then:
            properties:
              cpa_amount_minor:
                type: integer
                minimum: 0
              revshare_bps:
                type: integer
                minimum: 1
                maximum: 10000
              revshare_basis:
                type: string
                const: ngr
    RelationshipProviderMapping:
      type: object
      additionalProperties: false
      required:
        - id
        - provisioning_level
        - mapping_type
        - mapping_value
        - created_at
      properties:
        id:
          type: string
          format: uuid
        provisioning_level:
          type: string
          enum:
            - l0
            - l1
        mapping_type:
          type: string
          enum:
            - manual_code
            - sub_id
            - promo_code
            - affiliate_id
        mapping_value:
          type: string
          minLength: 1
          maxLength: 160
          pattern: ^[A-Za-z0-9][A-Za-z0-9._:@/-]{0,159}$
        created_at:
          type: string
          format: date-time
    RelationshipDealReference:
      type: object
      additionalProperties: false
      required:
        - id
        - status
      properties:
        id:
          type: string
          format: uuid
        status:
          type: string
          enum:
            - submitted
            - provisioning
            - active
            - suspended
    PartnerProgramCandidate:
      type: object
      additionalProperties: false
      required:
        - casino
        - program
        - offer
        - can_apply
        - eligibility_reason
        - existing_deal
      properties:
        casino:
          $ref: "#/components/schemas/RelationshipParty"
        program:
          $ref: "#/components/schemas/RelationshipProgramSnapshot"
        offer:
          $ref: "#/components/schemas/RelationshipOfferSnapshot"
        can_apply:
          type: boolean
        eligibility_reason:
          type:
            - string
            - "null"
          enum:
            - partner_not_active
            - open_deal_exists
            - managed_exclusive_other_casino
            - null
        existing_deal:
          oneOf:
            - $ref: "#/components/schemas/RelationshipDealReference"
            - type: "null"
    PartnerDealCreateInput:
      type: object
      additionalProperties: false
      required:
        - offer_version_id
        - accepted_terms
      properties:
        offer_version_id:
          type: string
          format: uuid
        accepted_terms:
          type: boolean
          const: true
    PartnerDealCancelInput:
      type: object
      additionalProperties: false
      required:
        - expected_version
        - reason
      properties:
        expected_version:
          type: integer
          minimum: 1
        reason:
          type: string
          minLength: 10
          maxLength: 500
    RelationshipMappingInput:
      type: object
      additionalProperties: false
      required:
        - mapping_type
        - mapping_value
      properties:
        mapping_type:
          type: string
          enum:
            - manual_code
            - sub_id
            - promo_code
            - affiliate_id
        mapping_value:
          type: string
          minLength: 1
          maxLength: 160
          pattern: ^[A-Za-z0-9][A-Za-z0-9._:@/-]{0,159}$
    CasinoDealTransitionInput:
      type: object
      additionalProperties: false
      required:
        - status
        - expected_version
        - reason
      properties:
        status:
          type: string
          enum:
            - provisioning
            - active
            - rejected
            - suspended
            - ended
        expected_version:
          type: integer
          minimum: 1
        reason:
          type: string
          minLength: 10
          maxLength: 500
        provisioning_level:
          type:
            - string
            - "null"
          enum:
            - l0
            - l1
            - null
        provider_mapping:
          oneOf:
            - $ref: "#/components/schemas/RelationshipMappingInput"
            - type: "null"
    RelationshipDeal:
      type: object
      additionalProperties: false
      required:
        - id
        - status
        - provisioning_level
        - status_reason
        - status_changed_at
        - accepted_at
        - version
        - created_at
        - updated_at
        - partner
        - casino
        - program
        - offer
        - provider_mapping
        - allowed_actions
      properties:
        id:
          type: string
          format: uuid
        status:
          type: string
          enum:
            - submitted
            - provisioning
            - active
            - rejected
            - cancelled
            - suspended
            - ended
        provisioning_level:
          type:
            - string
            - "null"
          enum:
            - l0
            - l1
            - null
        status_reason:
          type: string
          minLength: 10
          maxLength: 500
        status_changed_at:
          type: string
          format: date-time
        accepted_at:
          type: string
          format: date-time
        version:
          type: integer
          minimum: 1
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        partner:
          $ref: "#/components/schemas/RelationshipParty"
        casino:
          $ref: "#/components/schemas/RelationshipParty"
        program:
          $ref: "#/components/schemas/RelationshipProgramSnapshot"
        offer:
          $ref: "#/components/schemas/RelationshipOfferSnapshot"
        provider_mapping:
          oneOf:
            - $ref: "#/components/schemas/RelationshipProviderMapping"
            - type: "null"
        allowed_actions:
          type: array
          uniqueItems: true
          items:
            type: string
            enum:
              - cancel
              - approve
              - reject
              - activate
              - suspend
              - resume
              - end
    RelationshipDealPage:
      type: object
      additionalProperties: false
      required:
        - items
        - next_cursor
        - limit
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/RelationshipDeal"
        next_cursor:
          type:
            - string
            - "null"
          format: uuid
        limit:
          type: integer
          minimum: 1
          maximum: 100
    TrackingDestination:
      type: object
      additionalProperties: false
      required:
        - id
        - program_id
        - code
        - description
        - status
        - current_published_version_id
        - version
        - created_at
        - updated_at
      properties:
        id:
          type: string
          format: uuid
        program_id:
          type: string
          format: uuid
        code:
          type: string
          minLength: 3
          maxLength: 63
          pattern: ^[a-z0-9]+(?:-[a-z0-9]+)*$
        description:
          type: string
          minLength: 3
          maxLength: 500
        status:
          type: string
          enum:
            - draft
            - published
            - paused
            - archived
        current_published_version_id:
          type:
            - string
            - "null"
          format: uuid
        version:
          type: integer
          minimum: 1
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    TrackingParameterTemplate:
      type: object
      additionalProperties:
        type: string
        minLength: 1
        maxLength: 160
        pattern: ^(\{(?:click_id|provider_account)\}|[A-Za-z0-9._:@/-]{1,160})$
      minProperties: 1
      maxProperties: 20
      propertyNames:
        pattern: ^[A-Za-z][A-Za-z0-9_]{0,63}$
      not:
        type: object
        additionalProperties:
          type: string
          pattern: ^[A-Za-z0-9._:@/-]{1,160}$
    TrackingAllowedHostname:
      type: string
      minLength: 3
      maxLength: 253
      pattern: ^(?=.{1,253}$)(?![0-9.]+$)(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)(?:\.(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?))+$
    TrackingDestinationVersionCreateInput:
      type: object
      additionalProperties: false
      required:
        - target_url
        - allowed_hosts
        - parameter_template
        - valid_from
      properties:
        target_url:
          type: string
          format: uri
          minLength: 12
          maxLength: 2048
          pattern: ^https://
        allowed_hosts:
          type: array
          minItems: 1
          maxItems: 20
          uniqueItems: true
          items:
            $ref: "#/components/schemas/TrackingAllowedHostname"
        parameter_template:
          $ref: "#/components/schemas/TrackingParameterTemplate"
        valid_from:
          type: string
          format: date-time
        valid_until:
          type:
            - string
            - "null"
          format: date-time
    TrackingDestinationCreateInput:
      type: object
      additionalProperties: false
      required:
        - program_id
        - code
        - description
        - initial_version
      properties:
        program_id:
          type: string
          format: uuid
        code:
          type: string
          minLength: 3
          maxLength: 63
          pattern: ^[a-z0-9]+(?:-[a-z0-9]+)*$
        description:
          type: string
          minLength: 3
          maxLength: 500
        initial_version:
          $ref: "#/components/schemas/TrackingDestinationVersionCreateInput"
    TrackingDestinationVersion:
      type: object
      additionalProperties: false
      required:
        - id
        - destination_id
        - version_number
        - target_url
        - allowed_hosts
        - parameter_template
        - valid_from
        - created_at
      properties:
        id:
          type: string
          format: uuid
        destination_id:
          type: string
          format: uuid
        version_number:
          type: integer
          minimum: 1
        target_url:
          type: string
          format: uri
          maxLength: 2048
          pattern: ^https://
        allowed_hosts:
          type: array
          minItems: 1
          maxItems: 20
          uniqueItems: true
          items:
            $ref: "#/components/schemas/TrackingAllowedHostname"
        parameter_template:
          $ref: "#/components/schemas/TrackingParameterTemplate"
        valid_from:
          type: string
          format: date-time
        valid_until:
          type:
            - string
            - "null"
          format: date-time
        created_at:
          type: string
          format: date-time
    TrackingDestinationDetail:
      type: object
      additionalProperties: false
      required:
        - destination
        - current_published_version
      properties:
        destination:
          $ref: "#/components/schemas/TrackingDestination"
        current_published_version:
          oneOf:
            - $ref: "#/components/schemas/TrackingDestinationVersion"
            - type: "null"
    TrackingDestinationTransitionInput:
      type: object
      additionalProperties: false
      required:
        - status
        - reason
        - expected_version
      properties:
        status:
          type: string
          enum:
            - published
            - paused
            - archived
        published_version_id:
          type:
            - string
            - "null"
          format: uuid
        reason:
          type: string
          minLength: 10
          maxLength: 500
        expected_version:
          type: integer
          minimum: 1
    TrackingDestinationPage:
      type: object
      additionalProperties: false
      required:
        - items
        - next_cursor
        - limit
      properties:
        items:
          type: array
          maxItems: 100
          items:
            $ref: "#/components/schemas/TrackingDestination"
        next_cursor:
          type:
            - string
            - "null"
          format: uuid
        limit:
          type: integer
          minimum: 1
          maximum: 100
    TrackingDestinationVersionPage:
      type: object
      additionalProperties: false
      required:
        - items
        - next_cursor
        - limit
      properties:
        items:
          type: array
          maxItems: 100
          items:
            $ref: "#/components/schemas/TrackingDestinationVersion"
        next_cursor:
          type:
            - string
            - "null"
          format: uuid
        limit:
          type: integer
          minimum: 1
          maximum: 100
    TrackingLinkCreateInput:
      type: object
      additionalProperties: false
      required:
        - destination_id
      properties:
        destination_id:
          type: string
          format: uuid
        label:
          type: string
          minLength: 1
          maxLength: 120
    TrackingLinkTransitionInput:
      type: object
      additionalProperties: false
      required:
        - action
        - reason
        - expected_version
      properties:
        action:
          type: string
          enum:
            - pause
            - reactivate
            - revoke
        reason:
          type: string
          minLength: 10
          maxLength: 500
        expected_version:
          type: integer
          minimum: 1
    TrackingLink:
      type: object
      additionalProperties: false
      required:
        - id
        - deal_id
        - destination_id
        - token
        - public_url
        - label
        - resolved_destination_version_id
        - status
        - version
        - created_at
        - updated_at
      properties:
        id:
          type: string
          format: uuid
        deal_id:
          type: string
          format: uuid
        destination_id:
          type: string
          format: uuid
        token:
          type: string
          minLength: 43
          maxLength: 43
          pattern: ^[A-Za-z0-9_-]{43}$
        public_url:
          type: string
          format: uri
          maxLength: 512
          pattern: ^https://[^?#]+/r/[A-Za-z0-9_-]{43}$
        label:
          type:
            - string
            - "null"
          maxLength: 120
        resolved_destination_version_id:
          type: string
          format: uuid
        status:
          type: string
          enum:
            - active
            - inactive
            - revoked
        version:
          type: integer
          minimum: 1
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    TrackingLinkPage:
      type: object
      additionalProperties: false
      required:
        - items
        - next_cursor
        - limit
      properties:
        items:
          type: array
          maxItems: 100
          items:
            $ref: "#/components/schemas/TrackingLink"
        next_cursor:
          type:
            - string
            - "null"
          format: uuid
        limit:
          type: integer
          minimum: 1
          maximum: 100
    EmailOTPChallenge:
      type: object
      additionalProperties: false
      required: [status, challenge_token, expires_in, new_code_after, email_hint]
      properties:
        status:
          type: string
          const: email_otp_required
        challenge_token:
          type: string
          minLength: 38
          maxLength: 128
          description: Token opaco del desafío (id.secreto); la base solo guarda su SHA-256.
        expires_in:
          type: integer
          minimum: 0
          description: Segundos hasta que vence el código.
        new_code_after:
          type: integer
          minimum: 0
          description: Segundos antes de poder pedir otro código.
        email_hint:
          type: string
          description: Correo enmascarado al que se envió el código (p. ej. an***@dominio.com).
    EmailOTPVerifyInput:
      type: object
      additionalProperties: false
      required: [challenge_token, code]
      properties:
        challenge_token:
          type: string
          minLength: 1
          maxLength: 128
        code:
          type: string
          pattern: "^[0-9]{6}$"
          writeOnly: true
        trust_device:
          type: boolean
          default: false
    EmailOTPNewCodeInput:
      type: object
      additionalProperties: false
      required: [challenge_token]
      properties:
        challenge_token:
          type: string
          minLength: 1
          maxLength: 128
    LoginInput:
      type: object
      additionalProperties: false
      required:
        - email
        - password
      properties:
        email:
          type: string
          format: email
          maxLength: 254
        password:
          type: string
          minLength: 1
          maxLength: 72
          writeOnly: true
    ActiveMembershipSelectionInput:
      type: object
      additionalProperties: false
      required:
        - active_membership_id
      properties:
        active_membership_id:
          type: string
          format: uuid
          description: ID opaco de una membresía activa devuelta por /me.
    RuntimeUser:
      type: object
      additionalProperties: false
      required:
        - id
        - email
        - roles
        - principal_mode
        - tenant_id
        - tenant_slug
        - tenant_kind
        - creator_enabled
        - active_membership_id
      properties:
        id:
          type: string
          format: uuid
        email:
          type: string
          format: email
        roles:
          type: array
          minItems: 1
          maxItems: 1
          items:
            type: string
            enum:
              - partner
              - casino
              - convray_admin
              - super_admin
        principal_mode:
          type: string
          enum:
            - tenant_member
            - platform_admin
        tenant_id:
          type:
            - string
            - "null"
          format: uuid
        tenant_slug:
          type:
            - string
            - "null"
          minLength: 1
          maxLength: 80
        tenant_kind:
          type:
            - string
            - "null"
          enum:
            - partner
            - casino
            - null
        partner_status:
          type: string
          enum:
            - pending_verification
            - active
            - suspended
            - closed
        creator_enabled:
          type: boolean
        active_membership_id:
          type:
            - string
            - "null"
          format: uuid
        creator_disabled_reason:
          type: string
          description: Motivo por el que la extensión creator quedó deshabilitada sin negar el login (hoy `subscription_expired`). Solo viaja cuando hay motivo.
        display_name:
          type: string
          description: Nombre visible de la persona. Se omite cuando no lo definió.
        casino_slug:
          type:
            - string
            - "null"
          description: Slug del casino que gestiona la membresía activa (partner managed_exclusive); null en cualquier otro caso.
        notification_preferences:
          $ref: "#/components/schemas/NotificationPreferences"
        memberships:
          type: array
          uniqueItems: true
          items:
            $ref: "#/components/schemas/PublicMembership"
        products:
          type: array
          description: Accesos por producto de la membresía activa (`[]` cuando no hay).
          items:
            $ref: "#/components/schemas/ProductGrant"
        platform_admin:
          type: boolean
          description: true para la sesión de un super_admin con acceso de administrador a los cuatro productos.
        company_domains:
          type: array
          items:
            type: string
        company_account:
          type: boolean
        platform_admin_available:
          type: boolean
          description: true si la persona tiene una asignación de plataforma activa y puede cambiar a modo administrador.
    PublicMembership:
      type: object
      additionalProperties: false
      required:
        - id
        - role
        - status
        - tenant_id
        - tenant_slug
        - tenant_display_name
        - tenant_kind
        - creator_enabled
        - team_role_code
        - team_role_label
      properties:
        id:
          type: string
          format: uuid
        role:
          type: string
          enum:
            - owner
            - member
        status:
          type: string
          const: active
        tenant_id:
          type: string
          format: uuid
        tenant_slug:
          type: string
          minLength: 1
          maxLength: 80
        tenant_display_name:
          type: string
          minLength: 1
          maxLength: 120
        tenant_kind:
          type: string
          enum:
            - partner
            - casino
        partner_status:
          type: string
          enum:
            - pending_verification
            - active
            - suspended
            - closed
        creator_enabled:
          type: boolean
        casino_slug:
          type:
            - string
            - "null"
          description: Slug del casino que gestiona a este partner (managed_exclusive); null para casinos y partners independientes.
        team_role_code:
          type:
            - string
            - "null"
          description: |
            Rol de equipo de la membresía (por ejemplo `finanzas`). null para el owner y para
            un miembro con accesos editados a mano ("Personalizado"). Siempre presente.
        team_role_label:
          type:
            - string
            - "null"
          description: |
            Etiqueta del rol de equipo (por ejemplo "Finanzas"). Solo se resuelve para la
            membresía del tenant activo; en las demás, y en modo plataforma, llega null.
    TenantSummary:
      type: object
      additionalProperties: false
      required:
        - id
        - kind
        - slug
        - display_name
      properties:
        id:
          type: string
          format: uuid
        kind:
          type: string
          enum:
            - partner
            - casino
        slug:
          type: string
          minLength: 1
          maxLength: 80
        display_name:
          type: string
          minLength: 1
          maxLength: 120
    PartnerTenantSummary:
      allOf:
        - $ref: "#/components/schemas/TenantSummary"
        - type: object
          properties:
            kind:
              type: string
              const: partner
    CasinoTenantSummary:
      allOf:
        - $ref: "#/components/schemas/TenantSummary"
        - type: object
          properties:
            kind:
              type: string
              const: casino
    AuthResponse:
      type: object
      additionalProperties: false
      required:
        - user
        - access_token
        - token_type
        - expires_in
      properties:
        user:
          $ref: "#/components/schemas/RuntimeUser"
        access_token:
          type: string
          description: JWT corto con aud=convray-admin ligado a la sesión devuelta.
        token_type:
          type: string
          const: Bearer
        expires_in:
          type: integer
          const: 900
    FieldError:
      type: object
      additionalProperties: false
      required:
        - field
        - code
        - message
      properties:
        field:
          type: string
        code:
          type: string
        message:
          type: string
    ProblemBase:
      type: object
      description: Problem Details conforme a RFC 9457; no incluye stack traces, nombres de tablas, secretos ni PII completa.
      additionalProperties: false
      required:
        - type
        - title
        - status
        - detail
        - instance
        - code
        - request_id
      properties:
        type:
          type: string
          format: uri-reference
        title:
          type: string
        status:
          type: integer
          minimum: 400
          maximum: 599
        detail:
          type: string
        instance:
          type: string
          format: uri-reference
        code:
          type: string
        request_id:
          type: string
          minLength: 1
        trace_id:
          type: string
        errors:
          type: array
          items:
            $ref: "#/components/schemas/FieldError"
    InvalidCredentialsProblem:
      allOf:
        - $ref: "#/components/schemas/ProblemBase"
        - type: object
          properties:
            status:
              type: integer
              const: 401
            code:
              type: string
              enum:
                - invalid_credentials
                - invalid_casino_invitation
    InvalidSessionProblem:
      allOf:
        - $ref: "#/components/schemas/ProblemBase"
        - type: object
          properties:
            status:
              type: integer
              const: 401
            code:
              type: string
              enum:
                - invalid_session
                - viewer_session_invalid
    PermissionDeniedProblem:
      allOf:
        - $ref: "#/components/schemas/ProblemBase"
        - type: object
          properties:
            status:
              type: integer
              const: 403
            code:
              type: string
              const: permission_denied
    PartnerNotActiveProblem:
      allOf:
        - $ref: "#/components/schemas/ProblemBase"
        - type: object
          properties:
            type:
              type: string
              const: https://api.convray.com/problems/partner-not-active
            title:
              type: string
              const: Partner no activo
            status:
              type: integer
              const: 403
            detail:
              type: string
              const: El partner debe estar activo para aplicar a una oferta.
            code:
              type: string
              const: partner_not_active
    ManagedExclusiveCasinoProblem:
      allOf:
        - $ref: "#/components/schemas/ProblemBase"
        - type: object
          properties:
            type:
              type: string
              const: https://api.convray.com/problems/managed-exclusive-casino
            title:
              type: string
              const: Casino administrado
            status:
              type: integer
              const: 403
            detail:
              type: string
              const: Este partner opera exclusivamente con el casino asignado por su organización.
            code:
              type: string
              const: managed_exclusive_casino
    PublicCasinoRegistrationForbiddenProblem:
      allOf:
        - $ref: "#/components/schemas/ProblemBase"
        - type: object
          properties:
            status:
              type: integer
              const: 403
            code:
              type: string
              const: public_casino_registration_forbidden
    PublicRegistrationClosedProblem:
      allOf:
        - $ref: "#/components/schemas/ProblemBase"
        - type: object
          properties:
            type:
              type: string
              const: https://api.convray.com/problems/public-registration-closed
            title:
              type: string
              const: El registro público está cerrado
            status:
              type: integer
              const: 403
            detail:
              type: string
              const: Convray está operando un piloto privado. Las cuentas existentes pueden iniciar sesión normalmente.
            code:
              type: string
              const: public_registration_closed
    CSRFRejectedProblem:
      allOf:
        - $ref: "#/components/schemas/ProblemBase"
        - type: object
          properties:
            status:
              type: integer
              const: 403
            code:
              type: string
              const: csrf_rejected
    ResourceNotFoundProblem:
      allOf:
        - $ref: "#/components/schemas/ProblemBase"
        - type: object
          properties:
            status:
              type: integer
              const: 404
            code:
              type: string
              pattern: ^(resource|[a-z][a-z0-9_]*_resource|[a-z][a-z0-9_]*)_not_found$
    ValidationFailedProblem:
      allOf:
        - $ref: "#/components/schemas/ProblemBase"
        - type: object
          properties:
            status:
              type: integer
              const: 422
            code:
              type: string
              const: validation_failed
    RateLimitedProblem:
      allOf:
        - $ref: "#/components/schemas/ProblemBase"
        - type: object
          properties:
            status:
              type: integer
              const: 429
            code:
              type: string
              const: rate_limited
    AuditUnavailableProblem:
      allOf:
        - $ref: "#/components/schemas/ProblemBase"
        - type: object
          properties:
            status:
              type: integer
              const: 503
            code:
              type: string
              const: audit_unavailable
    IdempotencyConflictProblem:
      allOf:
        - $ref: "#/components/schemas/ProblemBase"
        - type: object
          properties:
            status:
              type: integer
              const: 409
            code:
              type: string
              const: idempotency_key_conflict
    PartnerRegistrationConflictProblem:
      allOf:
        - $ref: "#/components/schemas/ProblemBase"
        - type: object
          properties:
            status:
              type: integer
              const: 409
            code:
              type: string
              enum:
                - email_taken
                - partner_slug_taken
    RelationshipRuleViolationProblem:
      allOf:
        - $ref: "#/components/schemas/ProblemBase"
        - type: object
          properties:
            status:
              type: integer
              const: 422
            code:
              type: string
              const: validation_failed
    TrackingLinkNotFoundProblem:
      allOf:
        - $ref: "#/components/schemas/ProblemBase"
        - type: object
          properties:
            status:
              type: integer
              const: 404
            code:
              type: string
              const: tracking_link_not_found
    TrackingRedirectUnavailableProblem:
      allOf:
        - $ref: "#/components/schemas/ProblemBase"
        - type: object
          properties:
            status:
              type: integer
              const: 503
            code:
              type: string
              enum:
                - tracking_destination_unavailable
                - click_persistence_unavailable
    YouTubeForbiddenProblem:
      allOf:
        - $ref: "#/components/schemas/ProblemBase"
        - type: object
          properties:
            status:
              type: integer
              const: 403
            code:
              type: string
              const: youtube_forbidden
    YouTubeNotConfiguredProblem:
      allOf:
        - $ref: "#/components/schemas/ProblemBase"
        - type: object
          properties:
            status:
              type: integer
              const: 503
            code:
              type: string
              const: youtube_not_configured
    DataImportListResponse:
      type: object
      required:
        - batches
      properties:
        batches:
          type: array
          items:
            $ref: "#/components/schemas/DataBatchSummary"
    DataImportResponse:
      type: object
      required:
        - batch
      properties:
        batch:
          $ref: "#/components/schemas/DataBatch"
    DataImportPreviewResponse:
      type: object
      required:
        - quality
      properties:
        quality:
          $ref: "#/components/schemas/DataImportQuality"
    DataBatchSummary:
      type: object
      required:
        - id
        - dataset
        - file_name
        - source
        - status
        - rows_read
        - rows_accepted
        - rows_rejected
        - rows_upserted
        - parser_version
        - progress_pct
        - files_total
        - files_done
        - warnings
        - created_at
      properties:
        id:
          type: string
        dataset:
          type: string
        file_name:
          type: string
        file_sha256:
          type: [string, "null"]
        source:
          type: string
          description: Origen del lote (manual o connector).
        status:
          type: string
        rows_read:
          type: integer
        rows_accepted:
          type: integer
        rows_rejected:
          type: integer
        rows_upserted:
          type: integer
        parser_version:
          type: integer
        progress_pct:
          type: integer
        files_total:
          type: integer
        files_done:
          type: integer
        current_file:
          type: [string, "null"]
        period_from:
          type: [string, "null"]
          format: date
        period_to:
          type: [string, "null"]
          format: date
        error:
          type: [string, "null"]
        warnings:
          type: array
          items:
            type: string
        quality:
          type: [object, "null"]
          description: Bloque de calidad del lote (null si no se calculó).
        uploaded_by_user_id:
          type: [string, "null"]
        uploaded_by_email:
          type: [string, "null"]
        created_at:
          type: string
          format: date-time
        processing_started_at:
          type: [string, "null"]
          format: date-time
        finished_at:
          type: [string, "null"]
          format: date-time
    DataBatch:
      allOf:
        - $ref: "#/components/schemas/DataBatchSummary"
        - type: object
          required:
            - rejects
          properties:
            rejects:
              type: array
              items:
                $ref: "#/components/schemas/DataRejectEntry"
    DataRejectEntry:
      type: object
      required:
        - line
        - reason
      properties:
        line:
          type: integer
        reason:
          type: string
    DataImportQuality:
      type: object
      required:
        - dataset
        - columns
        - content_range
        - coverage
        - warnings
      properties:
        dataset:
          type: string
        detected_dataset:
          type: [string, "null"]
        columns:
          $ref: "#/components/schemas/DataImportQualityColumns"
        content_range:
          $ref: "#/components/schemas/DataImportQualityContentRange"
        coverage:
          $ref: "#/components/schemas/DataImportQualityCoverage"
        suspicious_filter:
          oneOf:
            - $ref: "#/components/schemas/DataImportQualitySuspiciousFilter"
            - type: "null"
        warnings:
          type: array
          items:
            type: string
    DataImportQualityColumns:
      type: object
      required:
        - missing_required
        - missing_optional
        - new
      properties:
        missing_required:
          type: array
          items:
            type: string
        missing_optional:
          type: array
          items:
            type: string
        new:
          type: array
          items:
            type: string
    DataImportQualityContentRange:
      type: object
      properties:
        from:
          type: [string, "null"]
        to:
          type: [string, "null"]
        rows:
          type: [integer, "null"]
    DataImportQualityCoverage:
      type: object
      required:
        - verdict
      properties:
        last_loaded_at:
          type: [string, "null"]
        verdict:
          type: string
          description: all_covered, partially_covered, gap_before o new.
        new_rows_estimate:
          type: [integer, "null"]
        status_changes_estimate:
          type: [integer, "null"]
    DataImportQualitySuspiciousFilter:
      type: object
      required:
        - field
        - single_value
      properties:
        field:
          type: string
        single_value:
          type: string
    DataImportQualityBlockProblem:
      allOf:
        - $ref: "#/components/schemas/ProblemBase"
        - type: object
          properties:
            quality:
              $ref: "#/components/schemas/DataImportQuality"
    DataDatasetListResponse:
      type: object
      required:
        - datasets
      properties:
        datasets:
          type: array
          items:
            $ref: "#/components/schemas/DataDataset"
    DataDataset:
      type: object
      required:
        - code
        - label
        - area
        - required_columns
        - optional_columns
        - natural_key
        - columns
        - freshness
      properties:
        code:
          type: string
        label:
          type: string
        area:
          type: string
        required_columns:
          type: array
          items:
            type: string
        optional_columns:
          type: array
          items:
            type: string
        natural_key:
          type: array
          items:
            type: string
        columns:
          type: array
          items:
            $ref: "#/components/schemas/DataDatasetColumn"
        freshness:
          $ref: "#/components/schemas/DataDatasetFreshness"
    DataDatasetColumn:
      type: object
      required:
        - key
        - label
        - kind
        - filterable
        - default_visible
      properties:
        key:
          type: string
        label:
          type: string
        kind:
          type: string
        filterable:
          type: boolean
        default_visible:
          type: boolean
    DataDatasetFreshness:
      type: object
      required:
        - batches_applied
      properties:
        last_applied_at:
          type: [string, "null"]
          format: date-time
        last_period_to:
          type: [string, "null"]
          format: date
        batches_applied:
          type: integer
    DataDatasetDetectRequest:
      type: object
      required:
        - header
      properties:
        header:
          type: array
          items:
            type: string
      additionalProperties: false
    DataDatasetDetection:
      type: object
      required:
        - dataset
        - confidence
        - missing_required
      properties:
        dataset:
          type: string
        confidence:
          type: number
        missing_required:
          type: array
          items:
            type: string
    DataExportListResponse:
      type: object
      required:
        - exports
      properties:
        exports:
          type: array
          items:
            $ref: "#/components/schemas/DataExportLogEntry"
    DataExportLogEntry:
      type: object
      required:
        - id
        - dataset
        - kind
        - filters
        - row_count
        - file_name
        - created_at
      properties:
        id:
          type: string
        dataset:
          type: string
        kind:
          type: string
        user_email:
          type: [string, "null"]
        filters:
          type: object
          description: Filtros efectivos serializados de la exportación.
        row_count:
          type: integer
        file_name:
          type: string
        created_at:
          type: string
          format: date-time
        via:
          type: string
          enum:
            - screen
            - assistant
          description: Origen de la exportación; assistant = hecha con Ray (la marca la pone el servidor).
        human_id:
          type: string
          description: Identificador legible EX-AAAAMMDD-NNNN.
        state:
          type: string
          enum:
            - requested
            - generating
            - downloaded
            - link_expired
            - failed
            - revoked
          description: Estado efectivo de la exportación (revoked si fue revocada en el registro).
        byte_size:
          type: integer
          description: Tamaño del archivo en bytes; 0 si no se conoce.
        column_count:
          type: integer
          description: Cantidad de columnas del archivo; 0 si no se conoce.
        includes_player:
          type: boolean
          description: El conjunto exportado incluye una referencia de jugador.
        includes_pii:
          type: boolean
          description: El conjunto exportado incluye datos personales.
        device:
          type: [string, "null"]
          description: Dispositivo resumido del User-Agent (p. ej. "Chrome · Windows").
        location:
          type: [string, "null"]
          description: Ubicación aproximada (país de CF-IPCountry); null si no hay fuente.
        link_expires_at:
          type: [string, "null"]
          format: date-time
          description: Vencimiento del enlace firmado; siempre null hoy (descarga directa).
    DataExportEvent:
      type: object
      required:
        - kind
        - created_at
      properties:
        kind:
          type: string
          enum:
            - requested
            - generated
            - downloaded
            - redownloaded
            - revoked
            - failed
        detail:
          type: [string, "null"]
        created_at:
          type: string
          format: date-time
    DataExportDetailResponse:
      type: object
      required:
        - export
        - events
      properties:
        export:
          $ref: "#/components/schemas/DataExportLogEntry"
        events:
          type: array
          items:
            $ref: "#/components/schemas/DataExportEvent"
    DataExportKpisResponse:
      type: object
      required:
        - exports_this_month
        - exports_prev_month
        - exports_delta_pct
        - rows_this_month
        - with_pii
        - distinct_exporters
        - exporters_with_access
      properties:
        exports_this_month:
          type: integer
        exports_prev_month:
          type: integer
        exports_delta_pct:
          type: number
        rows_this_month:
          type: integer
        with_pii:
          type: integer
        distinct_exporters:
          type: integer
        exporters_with_access:
          type: integer
          description: Personas del tenant con acceso a Data (denominador "de N con acceso"); 0 si no se pudo resolver.
    DataExportRepeatResponse:
      type: object
      required:
        - dataset
        - filters
      properties:
        dataset:
          type: string
        filters:
          type: object
          description: Filtros guardados con los que re-ejecutar la exportación.
    CreateDataExportRequest:
      type: object
      properties:
        dataset:
          type: string
        kind:
          type: string
          description: Tipo de exportación; por defecto selection.
        row_count:
          type: integer
        file_name:
          type: string
        filters:
          type: object
          description: Filtros efectivos aplicados a la exportación.
        byte_size:
          type: integer
          description: Tamaño del CSV en bytes (opcional; lo conoce la selección del navegador).
        column_count:
          type: integer
          description: Cantidad de columnas del CSV (opcional).
      additionalProperties: false
    DataQualityAlertListResponse:
      type: object
      required:
        - items
        - total
        - open_total
        - can_admin
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/DataQualityAlert"
        total:
          type: integer
        open_total:
          type: integer
        can_admin:
          type: boolean
    DataQualityAlertResponse:
      type: object
      required:
        - alert
      properties:
        alert:
          $ref: "#/components/schemas/DataQualityAlert"
    DataQualityAlert:
      type: object
      required:
        - id
        - dataset_code
        - kind
        - severity
        - detail
        - status
        - created_at
      properties:
        id:
          type: string
        dataset_code:
          type: string
        kind:
          type: string
        severity:
          type: string
          description: info, warning o critical.
        range_from:
          type: [string, "null"]
          format: date-time
        range_to:
          type: [string, "null"]
          format: date-time
        detail:
          type: object
          description: Detalle específico de la alerta.
        instruction:
          type: [string, "null"]
        owner_user_id:
          type: [string, "null"]
        owner_email:
          type: [string, "null"]
        status:
          type: string
          description: open, resolved o dismissed.
        created_at:
          type: string
          format: date-time
        resolved_at:
          type: [string, "null"]
          format: date-time
    PatchDataQualityAlertRequest:
      type: object
      required:
        - status
      properties:
        status:
          type: string
          description: Nuevo estado (open, resolved o dismissed).
      additionalProperties: false
    DataQualityCoverageResponse:
      type: object
      required:
        - datasets
        - open_alerts_total
      properties:
        datasets:
          type: array
          items:
            $ref: "#/components/schemas/DataQualityCoverageEntry"
        open_alerts_total:
          type: integer
    DataQualityCoverageEntry:
      type: object
      required:
        - dataset_code
        - label
        - loaded_today
        - has_open_gap
        - open_alerts
      properties:
        dataset_code:
          type: string
        label:
          type: string
        last_loaded_at:
          type: [string, "null"]
          format: date-time
        last_batch_at:
          type: [string, "null"]
          format: date-time
        last_batch_by:
          type: [string, "null"]
        loaded_today:
          type: boolean
        has_open_gap:
          type: boolean
        open_alerts:
          type: integer
    DataQualityOwnersResponse:
      type: object
      required:
        - owners
        - candidates
        - can_edit
      properties:
        owners:
          type: array
          items:
            $ref: "#/components/schemas/DataDatasetOwner"
        candidates:
          type: array
          items:
            $ref: "#/components/schemas/DataOwnerCandidate"
        can_edit:
          type: boolean
    DataDatasetOwner:
      type: object
      required:
        - dataset_code
        - user_id
      properties:
        dataset_code:
          type: string
        user_id:
          type: string
        email:
          type: [string, "null"]
    DataOwnerCandidate:
      type: object
      required:
        - user_id
        - email
      properties:
        user_id:
          type: string
        email:
          type: string
        display_name:
          type: [string, "null"]
    PutDataQualityOwnersRequest:
      type: object
      required:
        - owners
      properties:
        owners:
          type: array
          items:
            type: object
            required:
              - dataset_code
            properties:
              dataset_code:
                type: string
              user_id:
                type: [string, "null"]
            additionalProperties: false
      additionalProperties: false
    DataAffiliateCatalogResponse:
      type: object
      required:
        - catalog
      properties:
        catalog:
          $ref: "#/components/schemas/DataAffiliateCatalogPage"
    DataAffiliateCatalogPage:
      type: object
      required:
        - items
        - total
        - limit
        - offset
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/DataAffiliateCatalogEntry"
        total:
          type: integer
        limit:
          type: integer
        offset:
          type: integer
    DataAffiliateCatalogEntry:
      type: object
      required:
        - affiliate_id
        - username
        - status
        - players_count
        - imported_at
        - data_registered
        - data_ftd
        - data_deposits_paid_amount_minor
      properties:
        affiliate_id:
          type: integer
          format: int64
        username:
          type: string
        status:
          type: string
        players_count:
          type: integer
          format: int64
        source_created_at:
          type: [string, "null"]
          format: date-time
        imported_at:
          type: string
          format: date-time
        mapped_partner_slug:
          type: [string, "null"]
        mapped_partner_name:
          type: [string, "null"]
        data_registered:
          type: integer
          format: int64
        data_ftd:
          type: integer
          format: int64
        data_deposits_paid_amount_minor:
          type: integer
          format: int64
    DataAffiliateCatalogImportResponse:
      type: object
      required:
        - result
      properties:
        result:
          $ref: "#/components/schemas/DataAffiliateCatalogImportResult"
    DataAffiliateCatalogImportResult:
      type: object
      required:
        - rows_read
        - rows_inserted
        - rows_updated
        - rows_rejected
        - rejects
      properties:
        rows_read:
          type: integer
        rows_inserted:
          type: integer
        rows_updated:
          type: integer
        rows_rejected:
          type: integer
        rejects:
          type: array
          items:
            $ref: "#/components/schemas/DataAffiliateCatalogReject"
    DataAffiliateCatalogReject:
      type: object
      required:
        - line
        - reason
      properties:
        line:
          type: integer
        reason:
          type: string
    # ── Overview (franja global de KPIs) ─────────────────────────────────────
    DataOverviewRange:
      type: object
      properties:
        from:
          type: string
        to:
          type: string
    DataOverviewSegments:
      type: object
      description: Claves de segmentación siempre presentes; null hasta que exista segmentación.
      properties:
        vip:
          type: [integer, "null"]
          format: int64
        ex_vip:
          type: [integer, "null"]
          format: int64
        self_excluded:
          type: [integer, "null"]
          format: int64
        partners:
          type: [integer, "null"]
          format: int64
        partners_distinct:
          type: [integer, "null"]
          format: int64
        staff:
          type: [integer, "null"]
          format: int64
    DataOverviewProratedMonth:
      type: object
      properties:
        month:
          type: string
        days_in_range:
          type: integer
        days_in_month:
          type: integer
    DataOverviewSources:
      type: object
      properties:
        days_total:
          type: integer
        days_from_data:
          type: integer
        days_from_totals:
          type: integer
        days_mixed:
          type: integer
        prorated_months:
          type: array
          items:
            $ref: "#/components/schemas/DataOverviewProratedMonth"
    DataOverview:
      type: object
      description: Respuesta de GET /data/overview. Montos en unidad menor (CLP). Ratios fracción 0..1 con 4 decimales.
      properties:
        range:
          $ref: "#/components/schemas/DataOverviewRange"
        deposits_paid_minor:
          type: integer
          format: int64
        deposits_paid_count:
          type: integer
          format: int64
        withdrawals_paid_minor:
          type: integer
          format: int64
        net_cash_minor:
          type: integer
          format: int64
        casino_ggr_minor:
          type: integer
          format: int64
        sport_ggr_minor:
          type: integer
          format: int64
        ggr_minor:
          type: integer
          format: int64
        bet_amount_minor:
          type: integer
          format: int64
        casino_bet_amount_minor:
          type: integer
          format: int64
        casino_win_amount_minor:
          type: integer
          format: int64
        sport_bet_amount_minor:
          type: integer
          format: int64
        sport_win_amount_minor:
          type: integer
          format: int64
        bonus_bet_amount_minor:
          type: integer
          format: int64
        bonus_ggr_minor:
          type: integer
          format: int64
        deposit_attempts_count:
          type: integer
          format: int64
        deposit_effectiveness_pct:
          type: [number, "null"]
        avg_deposit_minor:
          type: integer
          format: int64
        registered_count:
          type: integer
          format: int64
        active_depositors:
          type: integer
          format: int64
        active_bettors:
          type: integer
          format: int64
        active_players:
          type: integer
          format: int64
        ftd_count:
          type: integer
          format: int64
        ftd_new_count:
          type: integer
          format: int64
        ftd_prior_registered_count:
          type: integer
          format: int64
        ftd_basis:
          type: string
        ftd_history_from:
          type: [string, "null"]
        hold_global_pct:
          type: number
        ggr_margin_pct:
          type: number
        ggr_hold_pct:
          type: number
        net_cash_pct:
          type: number
        segments:
          $ref: "#/components/schemas/DataOverviewSegments"
        sources:
          $ref: "#/components/schemas/DataOverviewSources"
        generated_at:
          type: string
    # ── Resumen (mensual) ────────────────────────────────────────────────────
    DataResumenKPI:
      type: object
      description: KPI del mes con sus comparaciones; todos los campos admiten null.
      properties:
        actual:
          type: [number, "null"]
        projected:
          type: [number, "null"]
        prev_month_full:
          type: [number, "null"]
        prev_month_same_span:
          type: [number, "null"]
        delta_vs_prev_span_pct:
          type: [number, "null"]
    DataResumenCoverageLastLoaded:
      type: object
      properties:
        deposits:
          type: [string, "null"]
        withdrawals:
          type: [string, "null"]
        casino_bets:
          type: [string, "null"]
        sport_bets:
          type: [string, "null"]
        players:
          type: [string, "null"]
    DataResumenCoverage:
      type: object
      properties:
        deposits_from:
          type: [string, "null"]
        bets_from:
          type: [string, "null"]
        last_loaded_day:
          $ref: "#/components/schemas/DataResumenCoverageLastLoaded"
    DataResumenNeededPerDay:
      type: object
      properties:
        deposit_amount_minor:
          type: [number, "null"]
        net_cash_minor:
          type: [number, "null"]
        ggr_minor:
          type: [number, "null"]
    DataResumenDailyCumulative:
      type: object
      properties:
        net_cash_minor:
          type: integer
          format: int64
        ggr_minor:
          type: integer
          format: int64
        deposit_amount_minor:
          type: integer
          format: int64
    DataResumenDailyRow:
      type: object
      properties:
        day:
          type: string
        complete:
          type: boolean
        weekend:
          type: boolean
        source:
          type: string
        deposit_amount_minor:
          type: integer
          format: int64
        deposit_count:
          type: integer
          format: int64
        withdraw_amount_minor:
          type: integer
          format: int64
        withdraw_count:
          type: integer
          format: int64
        net_cash_minor:
          type: integer
          format: int64
        ggr_minor:
          type: integer
          format: int64
        bet_amount_minor:
          type: integer
          format: int64
        hold_pct:
          type: [number, "null"]
        registered:
          type: integer
          format: int64
        ftd:
          type: integer
          format: int64
        active_depositors:
          type: integer
          format: int64
        cumulative:
          $ref: "#/components/schemas/DataResumenDailyCumulative"
        casino_bet_amount_minor:
          type: integer
          format: int64
        casino_win_amount_minor:
          type: integer
          format: int64
        sport_bet_amount_minor:
          type: integer
          format: int64
        sport_win_amount_minor:
          type: integer
          format: int64
        deposit_attempts_count:
          type: integer
          format: int64
        active_players:
          type: integer
          format: int64
        bonus_bet_amount_minor:
          type: [integer, "null"]
          format: int64
        bonus_ggr_minor:
          type: [integer, "null"]
          format: int64
        bonus_basis:
          type: string
    DataResumenMonthlyDelta:
      type: object
      properties:
        net_cash:
          type: [number, "null"]
        ggr:
          type: [number, "null"]
        deposit_amount:
          type: [number, "null"]
    DataResumenReconciliation:
      type: object
      properties:
        deposits_diff_pct:
          type: [number, "null"]
        ggr_diff_pct:
          type: [number, "null"]
    DataResumenMonthlyRow:
      type: object
      properties:
        month:
          type: string
        is_current:
          type: boolean
        projected:
          type: boolean
        source:
          type: string
        deposit_amount_minor:
          type: integer
          format: int64
        withdraw_amount_minor:
          type: integer
          format: int64
        net_cash_minor:
          type: integer
          format: int64
        ggr_minor:
          type: integer
          format: int64
        bet_amount_minor:
          type: integer
          format: int64
        hold_pct:
          type: [number, "null"]
        registered:
          type: integer
          format: int64
        ftd:
          type: integer
          format: int64
        active_depositors:
          type: integer
          format: int64
        delta_pct:
          $ref: "#/components/schemas/DataResumenMonthlyDelta"
        reconciliation:
          oneOf:
            - $ref: "#/components/schemas/DataResumenReconciliation"
            - type: "null"
        casino_bet_amount_minor:
          type: integer
          format: int64
        casino_win_amount_minor:
          type: integer
          format: int64
        sport_bet_amount_minor:
          type: integer
          format: int64
        sport_win_amount_minor:
          type: integer
          format: int64
        deposit_attempts_count:
          type: integer
          format: int64
        active_players:
          type: integer
          format: int64
        bonus_bet_amount_minor:
          type: [integer, "null"]
          format: int64
        bonus_ggr_minor:
          type: [integer, "null"]
          format: int64
        bonus_basis:
          type: string
    DataResumenVertical:
      type: object
      properties:
        bets_count:
          type: integer
          format: int64
        bet_amount_minor:
          type: integer
          format: int64
        win_amount_minor:
          type: integer
          format: int64
        ggr_minor:
          type: integer
          format: int64
        hold_pct:
          type: [number, "null"]
        players:
          type: integer
          format: int64
        bonus_bet_amount_minor:
          type: integer
          format: int64
        bonus_ggr_minor:
          type: integer
          format: int64
    DataResumenVerticals:
      type: object
      properties:
        casino:
          $ref: "#/components/schemas/DataResumenVertical"
        sport:
          $ref: "#/components/schemas/DataResumenVertical"
        share:
          type: object
          properties:
            casino_ggr_pct:
              type: [number, "null"]
            sport_ggr_pct:
              type: [number, "null"]
    DataResumen:
      type: object
      description: Respuesta de GET /data/resumen. Montos en unidad menor (CLP).
      properties:
        month:
          type: string
        today:
          type: string
        days_in_month:
          type: integer
        complete_days:
          type: integer
        projection_factor:
          type: [number, "null"]
        coverage:
          $ref: "#/components/schemas/DataResumenCoverage"
        kpis:
          type: object
          additionalProperties:
            $ref: "#/components/schemas/DataResumenKPI"
        needed_per_day:
          $ref: "#/components/schemas/DataResumenNeededPerDay"
        daily:
          type: array
          items:
            $ref: "#/components/schemas/DataResumenDailyRow"
        monthly:
          type: array
          items:
            $ref: "#/components/schemas/DataResumenMonthlyRow"
        verticals:
          $ref: "#/components/schemas/DataResumenVerticals"
        ftd_basis:
          type: string
        ftd_history_from:
          type: [string, "null"]
    # ── Depósitos (summary / series / breakdowns) ────────────────────────────
    DataDepositSummaryKPIs:
      type: object
      properties:
        paid_count:
          type: integer
          format: int64
        paid_amount_minor:
          type: integer
          format: int64
        depositors:
          type: integer
          format: int64
        avg_ticket_minor:
          type: integer
          format: int64
        failed_count:
          type: integer
          format: int64
        failure_rate:
          type: number
        first_time_depositors:
          type: integer
          format: int64
    DataDepositSeriesPoint:
      type: object
      properties:
        day:
          type: string
        paid_count:
          type: integer
          format: int64
        paid_amount_minor:
          type: integer
          format: int64
        failed_count:
          type: integer
          format: int64
        depositors:
          type: integer
          format: int64
    DataAmountBreakdown:
      type: object
      description: Desglose por método o dispositivo (solo depósitos/retiros pagados).
      properties:
        key:
          type: string
        paid_count:
          type: integer
          format: int64
        paid_amount_minor:
          type: integer
          format: int64
    DataStatusBreakdown:
      type: object
      description: Desglose por estado (todas las filas del estado).
      properties:
        key:
          type: string
        count:
          type: integer
          format: int64
        amount_minor:
          type: integer
          format: int64
    DataDepositFreshness:
      type: object
      properties:
        last_applied_at:
          type: [string, "null"]
          format: date-time
        last_period_to:
          type: [string, "null"]
        batches_applied:
          type: integer
          format: int64
    DataDepositSummary:
      type: object
      properties:
        currency:
          type: string
        kpis:
          $ref: "#/components/schemas/DataDepositSummaryKPIs"
        series:
          type: array
          items:
            $ref: "#/components/schemas/DataDepositSeriesPoint"
        by_method:
          type: array
          items:
            $ref: "#/components/schemas/DataAmountBreakdown"
        by_device:
          type: array
          items:
            $ref: "#/components/schemas/DataAmountBreakdown"
        by_status:
          type: array
          items:
            $ref: "#/components/schemas/DataStatusBreakdown"
        freshness:
          $ref: "#/components/schemas/DataDepositFreshness"
    # ── Depósitos: insights ──────────────────────────────────────────────────
    DataInsightsRange:
      type: object
      properties:
        from:
          type: string
        to:
          type: string
        days_with_data:
          type: integer
    DataMethodApproval:
      type: object
      properties:
        method:
          type: string
        total:
          type: integer
          format: int64
        paid:
          type: integer
          format: int64
        failed:
          type: integer
          format: int64
        other:
          type: integer
          format: int64
        approval_rate:
          type: number
    DataHourFailure:
      type: object
      properties:
        hour:
          type: integer
        total:
          type: integer
          format: int64
        failed:
          type: integer
          format: int64
        failed_rate:
          type: number
    DataWeekdayFailure:
      type: object
      properties:
        weekday:
          type: integer
        total:
          type: integer
          format: int64
        failed:
          type: integer
          format: int64
        failed_rate:
          type: number
    DataRetryInsights:
      type: object
      properties:
        failed_players:
          type: integer
          format: int64
        recovered_30m:
          type: integer
          format: int64
        recovered_24h:
          type: integer
          format: int64
        abandoned_24h:
          type: integer
          format: int64
        recovery_rate_30m:
          type: number
        recovery_rate_24h:
          type: number
    DataDayPlayers:
      type: object
      properties:
        day:
          type: string
        players:
          type: integer
          format: int64
    DataWeekPlayers:
      type: object
      properties:
        week_start:
          type: string
        players:
          type: integer
          format: int64
    DataMonthPlayers:
      type: object
      properties:
        month:
          type: string
        players:
          type: integer
          format: int64
    DataActiveDepositors:
      type: object
      properties:
        daily:
          type: array
          items:
            $ref: "#/components/schemas/DataDayPlayers"
        weekly:
          type: array
          items:
            $ref: "#/components/schemas/DataWeekPlayers"
        monthly:
          type: array
          items:
            $ref: "#/components/schemas/DataMonthPlayers"
    DataNewReturningDay:
      type: object
      properties:
        day:
          type: string
        new:
          type: integer
          format: int64
        returning:
          type: integer
          format: int64
    DataNewVsReturning:
      type: object
      properties:
        new:
          type: integer
          format: int64
        returning:
          type: integer
          format: int64
        by_day:
          type: array
          items:
            $ref: "#/components/schemas/DataNewReturningDay"
    DataFrequencyBucket:
      type: object
      properties:
        bucket:
          type: string
        players:
          type: integer
          format: int64
        share:
          type: number
    DataARPUDeposits:
      type: object
      properties:
        paid_amount_minor:
          type: integer
          format: int64
        active_players:
          type: integer
          format: int64
        arpu_minor:
          type: integer
          format: int64
    DataMixEntry:
      type: object
      properties:
        key:
          type: string
        count:
          type: integer
          format: int64
        amount_minor:
          type: integer
          format: int64
        count_share:
          type: number
        amount_share:
          type: number
    DataDepositMix:
      type: object
      properties:
        by_device:
          type: array
          items:
            $ref: "#/components/schemas/DataMixEntry"
        by_method:
          type: array
          items:
            $ref: "#/components/schemas/DataMixEntry"
    DataDailyAverage:
      type: object
      properties:
        avg_count:
          type: number
        avg_amount_minor:
          type: integer
          format: int64
        month:
          type: [string, "null"]
        days_in_month:
          type: [integer, "null"]
        days_elapsed:
          type: [integer, "null"]
        projection_count:
          type: [integer, "null"]
          format: int64
        projection_amount_minor:
          type: [integer, "null"]
          format: int64
        trend_7d_amount_minor:
          type: [integer, "null"]
          format: int64
    DataDepositInsights:
      type: object
      properties:
        range:
          $ref: "#/components/schemas/DataInsightsRange"
        approval_by_method:
          type: array
          items:
            $ref: "#/components/schemas/DataMethodApproval"
        failed_by_hour:
          type: array
          items:
            $ref: "#/components/schemas/DataHourFailure"
        failed_by_weekday:
          type: array
          items:
            $ref: "#/components/schemas/DataWeekdayFailure"
        retries:
          $ref: "#/components/schemas/DataRetryInsights"
        active_depositors:
          $ref: "#/components/schemas/DataActiveDepositors"
        new_vs_returning:
          $ref: "#/components/schemas/DataNewVsReturning"
        frequency:
          type: array
          items:
            $ref: "#/components/schemas/DataFrequencyBucket"
        arpu_deposits:
          $ref: "#/components/schemas/DataARPUDeposits"
        mix:
          $ref: "#/components/schemas/DataDepositMix"
        daily_average:
          $ref: "#/components/schemas/DataDailyAverage"
        real_effectiveness:
          $ref: "#/components/schemas/DataRealEffectiveness"
    DataRealEffectiveness:
      type: object
      description: >-
        Efectividad real de los depósitos (Depósitos · Indicadores, corte 1). Sale de la tabla cruda de
        depósitos con los filtros de rango, método, dispositivo y jugador (ignora status). Estados:
        pagado = paid, fallido = failed, rechazado = declined, cancelado = cancelled_by_operator,
        abierto = initialized, pending, pay_pending o in_process. Atascado = abierto con
        created_at <= as_of - stuck_minutes (justo a los 10 minutos ya es atascado); abierto joven =
        abierto con created_at > as_of - stuck_minutes. Efectividad informada =
        paid / (paid + failed + declined); efectividad real =
        paid / (paid + failed + declined + atascados). Las tasas son una fracción de 0 a 1 con 4
        decimales, o null sin denominador. Montos en unidad mínima. Día y hora en America/Santiago. Se
        recalcula con una caché propia de 30 segundos.
      properties:
        as_of:
          type: string
          format: date-time
          description: Instante del servidor (RFC 3339, UTC, al segundo) al que corresponden los atascados.
        stuck_minutes:
          type: integer
          description: Minutos abiertos desde los que un depósito se considera atascado (10).
        floor_rate:
          type: number
          description: >-
            Piso de la alerta de efectividad por método (floor_pct / 100); 0,5 si el casino no tiene
            ajustes de alertas.
        totals:
          $ref: "#/components/schemas/DataRealEffectivenessTotals"
        stuck_now:
          $ref: "#/components/schemas/DataRealEffectivenessOpen"
        stale_open:
          $ref: "#/components/schemas/DataRealEffectivenessStale"
        by_method:
          type: array
          description: >-
            Una fila por método de pago, ordenada por intentos descendente. Los depósitos sin
            payment_method salen como "Sin método". Un método con atascados hoy y sin intentos en el
            rango también sale (attempts 0, tasas null).
          items:
            $ref: "#/components/schemas/DataRealEffectivenessMethod"
        by_hour:
          type: array
          description: Una fila por (método, hora local 0 a 23) con intentos mayores que cero.
          items:
            $ref: "#/components/schemas/DataRealEffectivenessHour"
        daily:
          type: array
          description: Un elemento por día del rango, con ceros y tasas null en los días sin intentos.
          items:
            $ref: "#/components/schemas/DataRealEffectivenessDay"
    DataRealEffectivenessTotals:
      type: object
      description: >-
        Conteos y montos del rango y los filtros aplicados. attempts cuenta todos los estados. El
        embudo y el ticket promedio (paid_amount_minor / paid) se derivan de acá.
      properties:
        attempts:
          type: integer
          format: int64
        paid:
          type: integer
          format: int64
        failed:
          type: integer
          format: int64
        declined:
          type: integer
          format: int64
        cancelled:
          type: integer
          format: int64
        open_young:
          type: integer
          format: int64
          description: Abiertos con menos de 10 minutos al as_of.
        stuck:
          type: integer
          format: int64
          description: Abiertos con 10 minutos o más al as_of (sin tope de edad).
        paid_amount_minor:
          type: integer
          format: int64
        failed_amount_minor:
          type: integer
          format: int64
        declined_amount_minor:
          type: integer
          format: int64
        stuck_amount_minor:
          type: integer
          format: int64
        unpaid_amount_minor:
          type: integer
          format: int64
          description: Monto no cobrado = failed + declined + atascados.
        reported_rate:
          type: [number, "null"]
          description: Efectividad informada paid / (paid + failed + declined); null sin denominador.
        real_rate:
          type: [number, "null"]
          description: Efectividad real paid / (paid + failed + declined + atascados); null sin denominador.
    DataRealEffectivenessOpen:
      type: object
      description: >-
        Depósitos abiertos atascados ahora (edad entre 10 minutos y 48 horas al as_of). No depende del
        rango de fechas; sí de los demás filtros.
      properties:
        count:
          type: integer
          format: int64
        amount_minor:
          type: integer
          format: int64
    DataRealEffectivenessStale:
      type: object
      description: >-
        Depósitos abiertos viejos (created_at < as_of - older_than_hours). No depende del rango de
        fechas; sí de los demás filtros.
      properties:
        count:
          type: integer
          format: int64
        amount_minor:
          type: integer
          format: int64
        older_than_hours:
          type: integer
          description: Horas desde las que un abierto es viejo (48).
    DataRealEffectivenessMethod:
      type: object
      properties:
        payment_method:
          type: string
          description: Método de pago; "Sin método" cuando el depósito no trae payment_method.
        attempts:
          type: integer
          format: int64
        paid:
          type: integer
          format: int64
        failed:
          type: integer
          format: int64
        declined:
          type: integer
          format: int64
        stuck:
          type: integer
          format: int64
        reported_rate:
          type: [number, "null"]
        real_rate:
          type: [number, "null"]
        stuck_now:
          type: integer
          format: int64
          description: Atascados ahora del método (10 minutos a 48 horas); no depende del rango.
        alert_open:
          type: boolean
          description: Hay un episodio abierto de la alerta de efectividad por método.
    DataRealEffectivenessHour:
      type: object
      properties:
        payment_method:
          type: string
        hour:
          type: integer
          description: Hora local de Santiago (0 a 23).
        attempts:
          type: integer
          format: int64
        paid:
          type: integer
          format: int64
        failed:
          type: integer
          format: int64
        declined:
          type: integer
          format: int64
        stuck:
          type: integer
          format: int64
        real_rate:
          type: [number, "null"]
    DataRealEffectivenessDay:
      type: object
      properties:
        day:
          type: string
          description: Día local de Santiago (YYYY-MM-DD).
        attempts:
          type: integer
          format: int64
        paid:
          type: integer
          format: int64
        failed:
          type: integer
          format: int64
        declined:
          type: integer
          format: int64
        stuck:
          type: integer
          format: int64
        reported_rate:
          type: [number, "null"]
        real_rate:
          type: [number, "null"]
        alert_opened:
          type: boolean
          description: Empezó algún episodio de alerta de efectividad ese día (respeta el filtro de método).
    # ── PII y transacciones de depósito ──────────────────────────────────────
    DataDepositPII:
      type: object
      description: PII de la transacción; null salvo rol admin con clave del CRM.
      properties:
        username:
          type: string
        first_name:
          type: string
        last_name:
          type: string
        card_number:
          type: string
    # ── Badges de jugador (PRD-50) ────────────────────────────────────────────
    PlayerFlags:
      type: object
      description: >-
        Badges del jugador de la fila (PRD-50): etiquetas vigentes de Centrivo
        (vip/vip_level/vip_experience/partner_tag/staff), ex_vip y las restricciones de estado que
        ya trae Players (self_excluded/blocked/attention). Ruling del owner (2026-09-24):
        "Experiencia VIP" SUMA a VIP, así que vip es true también para Experiencia VIP y vip_level
        puede ser "experience" (precedencia Diamante > Zafiro > Experiencia). ex_vip = tuvo alguna
        etiqueta VIP (vip o vip_experience) y hoy no tiene ninguna vigente. Se agrega a toda fila por
        jugador de los listados de Data; un jugador sin sincronización de etiquetas ni snapshot en
        Players trae todos los campos en false/null.
      properties:
        vip:
          type: boolean
        vip_level:
          type: [string, "null"]
          enum:
            - diamante
            - zafiro
            - experience
            - null
        vip_experience:
          type: boolean
        ex_vip:
          type: boolean
        partner_tag:
          type: boolean
        staff:
          type: boolean
        self_excluded:
          type: boolean
        blocked:
          type: boolean
        attention:
          type: boolean
        self_excluded_at:
          type: [string, "null"]
          format: date-time
          description: >-
            Fecha de la autoexclusión más reciente (UTC). Sólo viene si el jugador está autoexcluido hoy
            y ya se sincronizó su fecha; null mientras no haya (la UI muestra "Fecha pendiente de
            sincronizar").
        self_exclusion_ends_at:
          type: [string, "null"]
          format: date-time
          description: Fin de la limitación (UTC); null si es indefinida o el panel no lo trae.
        self_exclusion_indefinite:
          type: boolean
          description: La autoexclusión no tiene fin (indefinida).
        self_exclusion_applied_by:
          type: [string, "null"]
          enum:
            - player
            - operator
            - unknown
            - null
          description: Quién aplicó la autoexclusión, normalizado. Nunca el nombre del operador (PII).
        self_exclusion_date_status:
          type: [string, "null"]
          enum:
            - synced
            - no_history
            - pending
            - null
          description: >-
            Estado de la fecha cuando el jugador está autoexcluido hoy (PRD-50, arreglo 2026-09-24):
            "synced" (fecha sincronizada), "no_history" (no tiene historial de juego responsable en
            Centrivo: la UI muestra "Sin fecha en Centrivo") o "pending" (aún no consultado / pendiente
            de sincronizar). null si el jugador no está autoexcluido. La API vieja no lo manda (ausente).
    PlayerIsp:
      type: [object, "null"]
      description: >-
        Compañía de internet VIGENTE del jugador (PRD-53 Fase 2): el ISP de la observación con
        last_seen_at más reciente. operator es el operador agrupado (Movistar, Entel, VTR, ...); kind es
        el tipo de conexión (fijo/movil/fijo_movil/empresas/satelital/tunel/otro); last_seen_at es la
        última vez que se lo vio conectado desde ese operador; operators_count es cuántos operadores
        distintos se le vieron. null cuando no hay observación. La IP NUNCA se guarda ni viaja.
      properties:
        operator:
          type: string
        kind:
          type: string
        last_seen_at:
          type: string
          format: date-time
        operators_count:
          type: integer
          format: int64
    PlayerFlagsResponse:
      type: object
      description: >-
        Respuesta del endpoint transversal GET /players/flags (PRD-50 Fase 6): un mapa de Player ID
        (external_player_ref) a sus PlayerFlags. Solo aparecen las refs con al menos una marca; una
        ref pedida que no está en el mapa no tiene badges. Sin PII.
      required:
        - flags
      properties:
        flags:
          type: object
          additionalProperties:
            $ref: "#/components/schemas/PlayerFlags"
    DataDepositTransactionRow:
      type: object
      properties:
        id:
          type: string
        created_at:
          type: string
          format: date-time
        external_player_ref:
          type: string
        deposit_ref:
          type: string
        external_ref:
          type: string
        status:
          type: string
        payment_method:
          type: string
        device_type:
          type: string
        sport_status:
          type: string
        region:
          type: string
        amount_minor:
          type: integer
          format: int64
        fee_minor:
          type: integer
          format: int64
        currency:
          type: string
        attributes:
          type: object
          additionalProperties:
            type: string
        pii:
          oneOf:
            - $ref: "#/components/schemas/DataDepositPII"
            - type: "null"
        player_flags:
          $ref: "#/components/schemas/PlayerFlags"
        player_isp:
          $ref: "#/components/schemas/PlayerIsp"
    DataDepositTransactionsPage:
      type: object
      properties:
        rows:
          type: array
          items:
            $ref: "#/components/schemas/DataDepositTransactionRow"
        total:
          type: integer
          format: int64
        page:
          type: integer
        page_size:
          type: integer
    # ── Catálogo de estados (depósitos, retiros, deportes) ───────────────────
    DataStatusOption:
      type: object
      properties:
        code:
          type: string
        label:
          type: string
    DataStatusCatalog:
      type: object
      properties:
        statuses:
          type: array
          items:
            $ref: "#/components/schemas/DataStatusOption"
    # ── Ficha de contexto de transacción ─────────────────────────────────────
    DataTransactionDetail:
      type: object
      properties:
        kind:
          type: string
          description: deposit | withdrawal
        ref:
          type: string
        external_ref:
          type: string
        external_player_ref:
          type: string
        status:
          type: string
        payment_method:
          type: string
        method_type:
          type: string
        device_type:
          type: string
        region:
          type: string
        currency:
          type: string
        amount_minor:
          type: integer
          format: int64
        fee_minor:
          type: integer
          format: int64
        tax_minor:
          type: integer
          format: int64
        amount_after_tax_minor:
          type: integer
          format: int64
        created_at:
          type: string
          format: date-time
        last_update_at:
          type: [string, "null"]
          format: date-time
        batch_id:
          type: string
        attributes:
          type: object
          additionalProperties:
            type: string
        pii:
          oneOf:
            - $ref: "#/components/schemas/DataDepositPII"
            - type: "null"
    DataStatusAggregate:
      type: object
      properties:
        status:
          type: string
        count:
          type: integer
          format: int64
        amount_minor:
          type: integer
          format: int64
    DataKeyCount:
      type: object
      properties:
        key:
          type: string
        count:
          type: integer
          format: int64
    DataMovementRow:
      type: object
      properties:
        kind:
          type: string
        ref:
          type: string
        status:
          type: string
        method:
          type: string
        amount_minor:
          type: integer
          format: int64
        created_at:
          type: string
          format: date-time
    DataPlayerTxFlags:
      type: object
      description: >-
        Banderas del jugador de la ficha de contexto de transacción. found/self_excluded/
        blocked/inactive/attention_marked/account_status/partner son el bloque histórico;
        vip/vip_level/vip_experience/ex_vip/partner_tag/staff extienden el contrato con las
        etiquetas de Centrivo (PRD-50).
      properties:
        found:
          type: boolean
        self_excluded:
          type: boolean
        blocked:
          type: boolean
        inactive:
          type: boolean
        attention_marked:
          type: boolean
        account_status:
          type: string
        partner:
          type: string
        vip:
          type: boolean
        vip_level:
          type: [string, "null"]
          enum:
            - diamante
            - zafiro
            - experience
            - null
        vip_experience:
          type: boolean
        ex_vip:
          type: boolean
        partner_tag:
          type: boolean
        staff:
          type: boolean
    DataPlayerPII:
      type: object
      description: PII del jugador; null salvo rol admin con clave del CRM.
      properties:
        username:
          type: string
        first_name:
          type: string
        last_name:
          type: string
        email:
          type: string
        mobile:
          type: string
        document:
          type: string
        dob:
          type: string
    DataPlayerTxContext:
      type: object
      properties:
        external_player_ref:
          type: string
        deposits_by_status:
          type: array
          items:
            $ref: "#/components/schemas/DataStatusAggregate"
        withdrawals_by_status:
          type: array
          items:
            $ref: "#/components/schemas/DataStatusAggregate"
        methods_used:
          type: array
          items:
            $ref: "#/components/schemas/DataKeyCount"
        devices_used:
          type: array
          items:
            $ref: "#/components/schemas/DataKeyCount"
        first_paid_deposit_at:
          type: [string, "null"]
          format: date-time
        last_paid_deposit_at:
          type: [string, "null"]
          format: date-time
        recent_movements:
          type: array
          items:
            $ref: "#/components/schemas/DataMovementRow"
        net_cash_minor:
          type: integer
          format: int64
        flags:
          $ref: "#/components/schemas/DataPlayerTxFlags"
        pii:
          oneOf:
            - $ref: "#/components/schemas/DataPlayerPII"
            - type: "null"
    DataTransactionContext:
      type: object
      properties:
        transaction:
          $ref: "#/components/schemas/DataTransactionDetail"
        player:
          $ref: "#/components/schemas/DataPlayerTxContext"
    # ── Retiros ──────────────────────────────────────────────────────────────
    DataWithdrawalSummaryKPIs:
      type: object
      properties:
        total_count:
          type: integer
          format: int64
        paid_count:
          type: integer
          format: int64
        paid_amount_minor:
          type: integer
          format: int64
        paid_rate:
          type: number
        failed_count:
          type: integer
          format: int64
        cancelled_count:
          type: integer
          format: int64
        avg_ticket_minor:
          type: integer
          format: int64
        unique_players:
          type: integer
          format: int64
        avg_payout_seconds:
          type: [integer, "null"]
          format: int64
    DataWithdrawalSeriesPoint:
      type: object
      properties:
        day:
          type: string
        paid_count:
          type: integer
          format: int64
        paid_amount_minor:
          type: integer
          format: int64
        failed_count:
          type: integer
          format: int64
        cancelled_count:
          type: integer
          format: int64
    DataWithdrawalFreshness:
      type: object
      properties:
        last_applied_at:
          type: [string, "null"]
          format: date-time
        last_period_to:
          type: [string, "null"]
        batches_applied:
          type: integer
          format: int64
    DataWithdrawalSummary:
      type: object
      description: by_method/by_device usan DataAmountBreakdown y by_status usa DataStatusBreakdown (alias de los de depósitos).
      properties:
        currency:
          type: string
        kpis:
          $ref: "#/components/schemas/DataWithdrawalSummaryKPIs"
        series:
          type: array
          items:
            $ref: "#/components/schemas/DataWithdrawalSeriesPoint"
        by_method:
          type: array
          items:
            $ref: "#/components/schemas/DataAmountBreakdown"
        by_device:
          type: array
          items:
            $ref: "#/components/schemas/DataAmountBreakdown"
        by_status:
          type: array
          items:
            $ref: "#/components/schemas/DataStatusBreakdown"
        freshness:
          $ref: "#/components/schemas/DataWithdrawalFreshness"
    DataWithdrawalTransactionRow:
      type: object
      properties:
        id:
          type: string
        created_at:
          type: string
          format: date-time
        external_player_ref:
          type: string
        withdrawal_ref:
          type: string
        external_ref:
          type: string
        status:
          type: string
        payment_method:
          type: string
        device_type:
          type: string
        sport_status:
          type: string
        region:
          type: string
        amount_minor:
          type: integer
          format: int64
        fee_minor:
          type: integer
          format: int64
        currency:
          type: string
        attributes:
          type: object
          additionalProperties:
            type: string
        pii:
          oneOf:
            - $ref: "#/components/schemas/DataDepositPII"
            - type: "null"
        player_flags:
          $ref: "#/components/schemas/PlayerFlags"
        player_isp:
          $ref: "#/components/schemas/PlayerIsp"
    DataWithdrawalTransactionsPage:
      type: object
      properties:
        rows:
          type: array
          items:
            $ref: "#/components/schemas/DataWithdrawalTransactionRow"
        total:
          type: integer
          format: int64
        page:
          type: integer
        page_size:
          type: integer
    # ── Apuestas de casino ───────────────────────────────────────────────────
    DataCasinoRange:
      type: object
      properties:
        from:
          type: string
        to:
          type: string
    DataCasinoDetailWindow:
      type: object
      properties:
        from_day:
          type: string
        to_day:
          type: string
    DataCasinoTotals:
      type: object
      properties:
        bets_count:
          type: integer
          format: int64
        bet_amount_minor:
          type: integer
          format: int64
        win_amount_minor:
          type: integer
          format: int64
        ggr_minor:
          type: integer
          format: int64
        hold_pct:
          type: number
        players_count:
          type: integer
          format: int64
        avg_bet_minor:
          type: integer
          format: int64
        ggr_per_player_minor:
          type: integer
          format: int64
        rounds_count:
          type: integer
          format: int64
    DataCasinoBonusTotals:
      type: object
      properties:
        bets_count:
          type: integer
          format: int64
        bet_amount_minor:
          type: integer
          format: int64
        win_amount_minor:
          type: integer
          format: int64
        ggr_minor:
          type: integer
          format: int64
    DataCasinoSeriesPoint:
      type: object
      properties:
        day:
          type: string
        bets_count:
          type: integer
          format: int64
        bet_amount_minor:
          type: integer
          format: int64
        win_amount_minor:
          type: integer
          format: int64
        ggr_minor:
          type: integer
          format: int64
        players_count:
          type: integer
          format: int64
    DataCasinoBreakdown:
      type: object
      description: Los campos provider/category/device_type/bonus_bet aparecen según el desglose (omitempty).
      properties:
        provider:
          type: string
        category:
          type: string
        device_type:
          type: string
        bonus_bet:
          type: [boolean, "null"]
        bets_count:
          type: integer
          format: int64
        players_count:
          type: integer
          format: int64
        bet_amount_minor:
          type: integer
          format: int64
        win_amount_minor:
          type: integer
          format: int64
        ggr_minor:
          type: integer
          format: int64
        hold_pct:
          type: number
    DataCasinoSummary:
      type: object
      properties:
        range:
          $ref: "#/components/schemas/DataCasinoRange"
        totals:
          $ref: "#/components/schemas/DataCasinoTotals"
        bonus:
          $ref: "#/components/schemas/DataCasinoBonusTotals"
        series:
          type: array
          items:
            $ref: "#/components/schemas/DataCasinoSeriesPoint"
        by_provider:
          type: array
          items:
            $ref: "#/components/schemas/DataCasinoBreakdown"
        by_category:
          type: array
          items:
            $ref: "#/components/schemas/DataCasinoBreakdown"
        by_device:
          type: array
          items:
            $ref: "#/components/schemas/DataCasinoBreakdown"
        by_bonus:
          type: array
          items:
            $ref: "#/components/schemas/DataCasinoBreakdown"
        detail_window:
          oneOf:
            - $ref: "#/components/schemas/DataCasinoDetailWindow"
            - type: "null"
    DataCasinoGameRow:
      type: object
      properties:
        game_id:
          type: string
        game:
          type: string
        provider:
          type: string
        category:
          type: string
        bets_count:
          type: integer
          format: int64
        players_count:
          type: integer
          format: int64
        bet_amount_minor:
          type: integer
          format: int64
        win_amount_minor:
          type: integer
          format: int64
        ggr_minor:
          type: integer
          format: int64
        hold_pct:
          type: number
        avg_bet_minor:
          type: integer
          format: int64
        bonus_bet_amount_minor:
          type: integer
          format: int64
    DataCasinoGamesPage:
      type: object
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/DataCasinoGameRow"
        total:
          type: integer
          format: int64
    DataCasinoPlayerRow:
      type: object
      properties:
        external_player_ref:
          type: string
        game_id:
          type: string
        game:
          type: string
        provider:
          type: string
        bets_count:
          type: integer
          format: int64
        bet_amount_minor:
          type: integer
          format: int64
        win_amount_minor:
          type: integer
          format: int64
        ggr_minor:
          type: integer
          format: int64
        bonus_bet_amount_minor:
          type: integer
          format: int64
        days_active:
          type: integer
          format: int64
        last_bet_at:
          type: [string, "null"]
          format: date-time
        player_flags:
          $ref: "#/components/schemas/PlayerFlags"
        player_isp:
          $ref: "#/components/schemas/PlayerIsp"
    DataCasinoPlayersPage:
      type: object
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/DataCasinoPlayerRow"
        total:
          type: integer
          format: int64
    DataCasinoTransactionRow:
      type: object
      properties:
        id:
          type: string
        round_id:
          type: string
        created_at:
          type: string
          format: date-time
        external_player_ref:
          type: string
        provider:
          type: string
        category:
          type: string
        game:
          type: string
        game_id:
          type: string
        state:
          type: string
        round_finish:
          type: boolean
        bonus_bet:
          type: boolean
        device_type:
          type: string
        bet_amount_minor:
          type: integer
          format: int64
        win_amount_minor:
          type: integer
          format: int64
        ggr_minor:
          type: integer
          format: int64
        balance_after_minor:
          type: [integer, "null"]
          format: int64
        currency:
          type: string
        attributes:
          type: object
          additionalProperties:
            type: string
        pii:
          oneOf:
            - $ref: "#/components/schemas/DataDepositPII"
            - type: "null"
        player_flags:
          $ref: "#/components/schemas/PlayerFlags"
        player_isp:
          $ref: "#/components/schemas/PlayerIsp"
    DataCasinoTransactionsPage:
      type: object
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/DataCasinoTransactionRow"
        total:
          type: integer
          format: int64
        window:
          oneOf:
            - $ref: "#/components/schemas/DataCasinoDetailWindow"
            - type: "null"
        outside_window:
          type: boolean
    # ── Apuestas deportivas ──────────────────────────────────────────────────
    DataSportTotals:
      type: object
      properties:
        bets_count:
          type: integer
          format: int64
        bet_amount_minor:
          type: integer
          format: int64
        win_amount_minor:
          type: integer
          format: int64
        ggr_minor:
          type: integer
          format: int64
        hold_pct:
          type: number
        players_count:
          type: integer
          format: int64
        avg_bet_minor:
          type: integer
          format: int64
        avg_odd:
          type: number
        live_pct:
          type: number
        cash_out_pct:
          type: number
        possible_win_minor:
          type: integer
          format: int64
    DataSportBonusTotals:
      type: object
      properties:
        bets_count:
          type: integer
          format: int64
        bet_amount_minor:
          type: integer
          format: int64
        win_amount_minor:
          type: integer
          format: int64
        ggr_minor:
          type: integer
          format: int64
    DataSportSeriesPoint:
      type: object
      properties:
        day:
          type: string
        bets_count:
          type: integer
          format: int64
        bet_amount_minor:
          type: integer
          format: int64
        win_amount_minor:
          type: integer
          format: int64
        ggr_minor:
          type: integer
          format: int64
        players_count:
          type: integer
          format: int64
    DataSportBreakdown:
      type: object
      properties:
        key:
          type: string
        bets_count:
          type: integer
          format: int64
        bet_amount_minor:
          type: integer
          format: int64
        win_amount_minor:
          type: integer
          format: int64
    DataSportSummary:
      type: object
      properties:
        range:
          $ref: "#/components/schemas/DataCasinoRange"
        totals:
          $ref: "#/components/schemas/DataSportTotals"
        bonus:
          $ref: "#/components/schemas/DataSportBonusTotals"
        series:
          type: array
          items:
            $ref: "#/components/schemas/DataSportSeriesPoint"
        by_state:
          type: array
          items:
            $ref: "#/components/schemas/DataSportBreakdown"
        by_bet_type:
          type: array
          items:
            $ref: "#/components/schemas/DataSportBreakdown"
        by_sport_bet_type:
          type: array
          items:
            $ref: "#/components/schemas/DataSportBreakdown"
        by_device:
          type: array
          items:
            $ref: "#/components/schemas/DataSportBreakdown"
        by_bonus:
          type: array
          items:
            $ref: "#/components/schemas/DataSportBreakdown"
    DataSportTransactionRow:
      type: object
      properties:
        id:
          type: string
        coupon_number:
          type: string
        created_at:
          type: string
          format: date-time
        external_player_ref:
          type: string
        bet_type:
          type: string
        sport_bet_type:
          type: string
        processing_type:
          type: string
        bet_builder:
          type: boolean
        cash_out:
          type: boolean
        odd:
          type: [number, "null"]
        bet_amount_minor:
          type: integer
          format: int64
        possible_win_minor:
          type: [integer, "null"]
          format: int64
        win_amount_minor:
          type: integer
          format: int64
        ggr_minor:
          type: integer
          format: int64
        state:
          type: string
        balance_after_minor:
          type: [integer, "null"]
          format: int64
        currency:
          type: string
        country:
          type: string
        region:
          type: string
        bonus_bet:
          type: boolean
        device_type:
          type: string
        system_type:
          type: string
        attributes:
          type: object
          additionalProperties:
            type: string
        pii:
          oneOf:
            - $ref: "#/components/schemas/DataDepositPII"
            - type: "null"
        player_flags:
          $ref: "#/components/schemas/PlayerFlags"
        player_isp:
          $ref: "#/components/schemas/PlayerIsp"
    DataSportTransactionsPage:
      type: object
      properties:
        rows:
          type: array
          items:
            $ref: "#/components/schemas/DataSportTransactionRow"
        total:
          type: integer
          format: int64
        page:
          type: integer
        page_size:
          type: integer
    PlayerTotalsPeriodsResponse:
      type: object
      required:
        - items
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/PlayerTotalsPeriodItem"
    PlayerTotalsPeriodItem:
      type: object
      properties:
        period_from:
          type: string
          format: date
        period_to:
          type: string
          format: date
        players:
          type: integer
          format: int64
        deposits_amount_minor:
          type: integer
          format: int64
        withdrawals_amount_minor:
          type: integer
          format: int64
        casino_bets_amount_minor:
          type: integer
          format: int64
        casino_ggr_minor:
          type: integer
          format: int64
        sport_bets_amount_minor:
          type: integer
          format: int64
        sport_ggr_minor:
          type: integer
          format: int64
        ngr_minor:
          type: integer
          format: int64
        loaded_at:
          type: string
          format: date-time
    PlayerTotalsPeriodRange:
      type: object
      properties:
        from:
          type: string
          format: date
        to:
          type: string
          format: date
    PlayerTotalsVertical:
      type: object
      properties:
        bets_amount_minor:
          type: integer
          format: int64
        bets_count:
          type: integer
          format: int64
        wins_amount_minor:
          type: integer
          format: int64
        ggr_minor:
          type: integer
          format: int64
    PlayerTotalsSummaryTotals:
      type: object
      properties:
        players:
          type: integer
          format: int64
        deposits_amount_minor:
          type: integer
          format: int64
        deposits_count:
          type: integer
          format: int64
        withdrawals_amount_minor:
          type: integer
          format: int64
        withdrawals_count:
          type: integer
          format: int64
        net_cash_minor:
          type: integer
          format: int64
        casino_bets_amount_minor:
          type: integer
          format: int64
        casino_bets_count:
          type: integer
          format: int64
        casino_wins_amount_minor:
          type: integer
          format: int64
        casino_ggr_minor:
          type: integer
          format: int64
        sport_bets_amount_minor:
          type: integer
          format: int64
        sport_bets_count:
          type: integer
          format: int64
        sport_wins_amount_minor:
          type: integer
          format: int64
        sport_ggr_minor:
          type: integer
          format: int64
        ggr_minor:
          type: integer
          format: int64
        ngr_minor:
          type: integer
          format: int64
        taxes_minor:
          type: integer
          format: int64
        hold_pct:
          type:
            - number
            - "null"
          description: Fracción 0..1 (net_cash/depósitos); null si depósitos == 0.
        ggr_margin_pct:
          type:
            - number
            - "null"
          description: Fracción 0..1 (GGR/depósitos); null si depósitos == 0.
    PlayerTotalsSummary:
      type: object
      properties:
        period:
          $ref: "#/components/schemas/PlayerTotalsPeriodRange"
        totals:
          $ref: "#/components/schemas/PlayerTotalsSummaryTotals"
        by_vertical:
          type: object
          properties:
            casino:
              $ref: "#/components/schemas/PlayerTotalsVertical"
            sport:
              $ref: "#/components/schemas/PlayerTotalsVertical"
    PlayerTotalsPII:
      type: object
      properties:
        username:
          type: string
    PlayerTotalsListRow:
      type: object
      properties:
        external_player_ref:
          type: string
        account_status:
          type: string
        deposits_amount_minor:
          type: integer
          format: int64
        deposits_count:
          type: integer
          format: int64
        withdrawals_amount_minor:
          type: integer
          format: int64
        net_cash_minor:
          type: integer
          format: int64
        casino_bets_amount_minor:
          type: integer
          format: int64
        casino_ggr_minor:
          type: integer
          format: int64
        sport_bets_amount_minor:
          type: integer
          format: int64
        sport_ggr_minor:
          type: integer
          format: int64
        ggr_minor:
          type: integer
          format: int64
        ngr_minor:
          type: integer
          format: int64
        pii:
          oneOf:
            - $ref: "#/components/schemas/PlayerTotalsPII"
            - type: "null"
          description: PII del jugador (solo rol admin); null para el resto.
        player_flags:
          $ref: "#/components/schemas/PlayerFlags"
        player_isp:
          $ref: "#/components/schemas/PlayerIsp"
        net_cash_total_minor:
          type: integer
          format: int64
          description: Net cash historico del jugador (depositos pagados menos retiros pagados de toda su vida), CLP.
        deposits_loaded:
          type: boolean
          description: true si el jugador tiene algun deposito cargado en Convray (PRD-53); distingue el net cash $0 honesto del $0 por falta de historial cargado.
    PlayerTotalsListPage:
      type: object
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/PlayerTotalsListRow"
        total:
          type: integer
          format: int64
    PlayerTotalsRangeVertical:
      type: object
      description: Sub-total de una vertical (casino o deportes) en el rango. GGR con y sin bono.
      properties:
        bets_amount_minor:
          type: integer
          format: int64
        bets_count:
          type: integer
          format: int64
        wins_amount_minor:
          type: integer
          format: int64
        ggr_minor:
          type: integer
          format: int64
          description: GGR con bono (apostado menos ganado).
        ggr_no_bonus_minor:
          type: integer
          format: int64
          description: GGR sin bono (descuenta las apuestas y premios de bono).
        bonus_bets_amount_minor:
          type: integer
          format: int64
        bonus_ggr_minor:
          type: integer
          format: int64
    PlayerTotalsRangeTotals:
      type: object
      description: >-
        KPIs del rango calculados desde data_player_activity_daily (exactos para
        cualquier rango). ngr_minor y taxes_minor son null cuando el rango no
        coincide exactamente con periodos cargados (ngr_available=false); nunca
        se estima el NGR.
      properties:
        players:
          type: integer
          format: int64
        deposits_amount_minor:
          type: integer
          format: int64
        deposits_count:
          type: integer
          format: int64
        withdrawals_amount_minor:
          type: integer
          format: int64
        withdrawals_count:
          type: integer
          format: int64
        net_cash_minor:
          type: integer
          format: int64
        casino_bets_amount_minor:
          type: integer
          format: int64
        casino_bets_count:
          type: integer
          format: int64
        casino_wins_amount_minor:
          type: integer
          format: int64
        casino_ggr_minor:
          type: integer
          format: int64
        casino_ggr_no_bonus_minor:
          type: integer
          format: int64
        sport_bets_amount_minor:
          type: integer
          format: int64
        sport_bets_count:
          type: integer
          format: int64
        sport_wins_amount_minor:
          type: integer
          format: int64
        sport_ggr_minor:
          type: integer
          format: int64
        sport_ggr_no_bonus_minor:
          type: integer
          format: int64
        ggr_minor:
          type: integer
          format: int64
          description: GGR total con bono.
        ggr_no_bonus_minor:
          type: integer
          format: int64
          description: GGR total sin bono.
        bonus_bets_amount_minor:
          type: integer
          format: int64
        bonus_ggr_minor:
          type: integer
          format: int64
        ngr_minor:
          type:
            - integer
            - "null"
          format: int64
          description: NGR exacto (suma de los periodos cargados que teselan el rango); null si el rango no coincide con periodos cargados.
        taxes_minor:
          type:
            - integer
            - "null"
          format: int64
          description: Impuestos exactos (de los periodos cargados); null si el rango no coincide con periodos cargados.
        ngr_available:
          type: boolean
          description: true si el rango se puede cubrir exactamente con periodos cargados; entonces ngr_minor y taxes_minor no son null.
        hold_pct:
          type:
            - number
            - "null"
          description: Fraccion (net_cash/depositos); null si depositos == 0.
        ggr_margin_pct:
          type:
            - number
            - "null"
          description: Fraccion (GGR con bono/depositos); null si depositos == 0.
    PlayerTotalsRangeSummary:
      type: object
      properties:
        range:
          $ref: "#/components/schemas/PlayerTotalsPeriodRange"
        totals:
          $ref: "#/components/schemas/PlayerTotalsRangeTotals"
        by_vertical:
          type: object
          properties:
            casino:
              $ref: "#/components/schemas/PlayerTotalsRangeVertical"
            sport:
              $ref: "#/components/schemas/PlayerTotalsRangeVertical"
        periods_used:
          type: array
          description: Periodos cargados que teselan el rango (vacio si el NGR no esta disponible).
          items:
            $ref: "#/components/schemas/PlayerTotalsPeriodRange"
    PlayerTotalsRangeRow:
      type: object
      properties:
        external_player_ref:
          type: string
        account_status:
          type: string
        deposits_amount_minor:
          type: integer
          format: int64
        deposits_count:
          type: integer
          format: int64
        withdrawals_amount_minor:
          type: integer
          format: int64
        net_cash_minor:
          type: integer
          format: int64
        casino_bets_amount_minor:
          type: integer
          format: int64
        casino_ggr_minor:
          type: integer
          format: int64
        sport_bets_amount_minor:
          type: integer
          format: int64
        sport_ggr_minor:
          type: integer
          format: int64
        ggr_minor:
          type: integer
          format: int64
        ggr_no_bonus_minor:
          type: integer
          format: int64
        ngr_minor:
          type:
            - integer
            - "null"
          format: int64
          description: NGR del jugador en el rango; null si el rango no coincide con periodos cargados.
        pii:
          oneOf:
            - $ref: "#/components/schemas/PlayerTotalsPII"
            - type: "null"
          description: PII del jugador (solo rol admin); null para el resto.
        player_flags:
          $ref: "#/components/schemas/PlayerFlags"
        player_isp:
          $ref: "#/components/schemas/PlayerIsp"
        net_cash_total_minor:
          type: integer
          format: int64
        deposits_loaded:
          type: boolean
    PlayerTotalsRangeListPage:
      type: object
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/PlayerTotalsRangeRow"
        total:
          type: integer
          format: int64
        ngr_available:
          type: boolean
          description: true si el rango coincide con periodos cargados; entonces ngr_minor por fila no es null.
    PlayerTotalsLTVRow:
      type: object
      properties:
        external_player_ref:
          type: string
        registered_at:
          type:
            - string
            - "null"
          format: date-time
        deposits_amount_minor:
          type: integer
          format: int64
        net_cash_minor:
          type: integer
          format: int64
        ggr_minor:
          type: integer
          format: int64
        ngr_minor:
          type: integer
          format: int64
        periods:
          type: integer
          format: int64
        player_flags:
          $ref: "#/components/schemas/PlayerFlags"
        player_isp:
          $ref: "#/components/schemas/PlayerIsp"
    PlayerTotalsLTVPage:
      type: object
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/PlayerTotalsLTVRow"
        total:
          type: integer
          format: int64
        periods_used:
          type: array
          items:
            $ref: "#/components/schemas/PlayerTotalsPeriodRange"
    PlayerRange:
      type: object
      properties:
        from:
          type: string
          format: date
        to:
          type: string
          format: date
    PlayerTotals:
      type: object
      properties:
        players_total:
          type: integer
          format: int64
        registered:
          type: integer
          format: int64
        depositors_registered:
          type: integer
          format: int64
        conversion_pct:
          type: number
          description: Fracción 0..1 con 4 decimales.
        self_excluded_total:
          type: integer
          format: int64
        blocked_total:
          type: integer
          format: int64
        inactive_total:
          type: integer
          format: int64
        partner_tagged_total:
          type: integer
          format: int64
        partners_distinct:
          type: integer
          format: int64
        attention_marked_total:
          type: integer
          format: int64
        logged_in_30d:
          type: integer
          format: int64
        vip_total:
          type: integer
          format: int64
          description: >-
            Jugadores con etiqueta VIP vigente de Centrivo (Diamante+Zafiro) sobre toda la base
            (PRD-50). Entero no nulo; 0 mientras no hubo ninguna sincronización de etiquetas.
        ex_vip_total:
          type: integer
          format: int64
          description: >-
            Jugadores Ex-VIP (tuvieron la etiqueta VIP y hoy no la tienen), misma regla que
            player_flags.ex_vip. Entero no nulo; 0 sin sincronización previa.
        partner_tag_total:
          type: integer
          format: int64
          description: >-
            Jugadores con etiqueta Partner vigente de Centrivo (streamers con saldo cargado). Entero
            no nulo; 0 sin sincronización previa.
        staff_total:
          type: integer
          format: int64
          description: >-
            Jugadores con etiqueta Staff vigente de Centrivo. Entero no nulo; 0 sin sincronización
            previa.
    PlayerSeriesPoint:
      type: object
      properties:
        day:
          type: string
          format: date
        registered:
          type: integer
          format: int64
        depositors_registered:
          type: integer
          format: int64
    PlayerDeviceBreakdown:
      type: object
      properties:
        key:
          type: string
        registered:
          type: integer
          format: int64
    PlayerStatusBreakdown:
      type: object
      properties:
        key:
          type: string
        players:
          type: integer
          format: int64
    PlayerPartnerBreakdown:
      type: object
      properties:
        key:
          type: string
        players:
          type: integer
          format: int64
        depositors:
          type: integer
          format: int64
    PlayerSnapshot:
      type: object
      properties:
        day:
          type: string
          format: date
        players_total:
          type: integer
          format: int64
        self_excluded:
          type: integer
          format: int64
        blocked:
          type: integer
          format: int64
        inactive:
          type: integer
          format: int64
        depositors:
          type: integer
          format: int64
        partner_tagged:
          type: integer
          format: int64
        attention_marked:
          type: integer
          format: int64
    PlayerSummary:
      type: object
      properties:
        range:
          $ref: "#/components/schemas/PlayerRange"
        totals:
          $ref: "#/components/schemas/PlayerTotals"
        series:
          type: array
          items:
            $ref: "#/components/schemas/PlayerSeriesPoint"
        by_device:
          type: array
          items:
            $ref: "#/components/schemas/PlayerDeviceBreakdown"
        by_account_status:
          type: array
          items:
            $ref: "#/components/schemas/PlayerStatusBreakdown"
        by_partner:
          type: array
          items:
            $ref: "#/components/schemas/PlayerPartnerBreakdown"
        snapshot:
          oneOf:
            - $ref: "#/components/schemas/PlayerSnapshot"
            - type: "null"
          description: Último conteo diario del snapshot; null si no hay.
        status_as_of:
          oneOf:
            - type: string
              format: date-time
            - type: "null"
          description: >-
            Instante de la última foto COMPLETA de jugadores aplicada (juego
            responsable). null si nunca se aplicó una foto completa.
        new_self_excluded_since:
          type: integer
          format: int64
          description: >-
            Jugadores existentes que pasaron a autoexcluido en las últimas 24 h
            (cambios registrados en la carga de la foto/base).
        vip_breakdown:
          $ref: "#/components/schemas/PlayerVIPBreakdown"
    PlayerVIPBreakdown:
      type: object
      description: >-
        Desglose por nivel VIP de la base FILTRADA del summary (OLA 2, lienzo Ledger). Respeta los
        mismos filtros que players_total (incluir/excluir, contactable, estado, isp, rango) y es una
        PARTICIÓN de ese total: diamante + zafiro + experience + sin_vip == players_total. La
        precedencia diamante > zafiro > experience evita el doble conteo.
      required:
        - diamante
        - zafiro
        - experience
        - sin_vip
      properties:
        diamante:
          type: integer
          format: int64
          description: Jugadores con etiqueta VIP nivel Diamante vigente.
        zafiro:
          type: integer
          format: int64
          description: Jugadores con etiqueta VIP nivel Zafiro vigente (y no Diamante).
        experience:
          type: integer
          format: int64
          description: Jugadores con etiqueta Experiencia VIP vigente (y no Diamante/Zafiro).
        sin_vip:
          type: integer
          format: int64
          description: Jugadores de la base sin ninguna etiqueta VIP vigente.
    SavedView:
      type: object
      description: Vista guardada de filtros del usuario (patrón de filtros globales "Ledger", OLA 2).
      required:
        - id
        - module
        - name
        - filters
        - created_at
        - updated_at
      properties:
        id:
          type: string
          format: uuid
        module:
          type: string
          description: Módulo al que pertenece la vista (p. ej. data-players, data-deposits).
        name:
          type: string
          description: Nombre de la vista; único por (tenant, usuario, módulo).
        filters:
          type: object
          additionalProperties: true
          description: Conjunto de filtros guardado (objeto JSON con los parámetros del contrato).
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    SavedViewList:
      type: object
      required:
        - views
      properties:
        views:
          type: array
          items:
            $ref: "#/components/schemas/SavedView"
    SavedViewCreateInput:
      type: object
      required:
        - module
        - name
        - filters
      properties:
        module:
          type: string
          maxLength: 64
        name:
          type: string
          maxLength: 200
        filters:
          type: object
          additionalProperties: true
          description: Objeto JSON con los filtros a guardar.
    SavedViewPatchInput:
      type: object
      description: Campos opcionales; los ausentes no cambian. Al menos uno para tener efecto.
      properties:
        name:
          oneOf:
            - type: string
              maxLength: 200
            - type: "null"
        filters:
          oneOf:
            - type: object
              additionalProperties: true
            - type: "null"
    PlayerVerified:
      type: object
      properties:
        identity:
          type: boolean
        email:
          type: boolean
        mobile:
          type: boolean
        address:
          type: boolean
    PlayerPII:
      type: object
      description: PII de un jugador descifrada; presente solo para el rol admin.
      properties:
        username:
          type: string
        first_name:
          type: string
        last_name:
          type: string
        email:
          type: string
        mobile:
          type: string
        document:
          type: string
        dob:
          type: string
    PlayerListRow:
      type: object
      properties:
        external_player_ref:
          type: string
        casino_player_ref:
          type: string
        registered_at:
          type: string
          format: date-time
        registration_device:
          type: string
        country:
          type: string
        region:
          type: string
        city:
          type: string
        currency:
          type: string
        account_status:
          type: string
        sport_status:
          type: string
        last_login_at:
          type:
            - string
            - "null"
          format: date-time
        has_deposited:
          type: boolean
        account_inactive:
          type: boolean
        account_locked:
          type: boolean
        attention_marked:
          type: boolean
        exclusion_state:
          type: boolean
        btag:
          type: string
        verified:
          $ref: "#/components/schemas/PlayerVerified"
        attributes:
          type: object
          additionalProperties:
            type: string
        pii:
          oneOf:
            - $ref: "#/components/schemas/PlayerPII"
            - type: "null"
          description: PII del jugador (solo rol admin); null para el resto.
        player_flags:
          $ref: "#/components/schemas/PlayerFlags"
        player_isp:
          $ref: "#/components/schemas/PlayerIsp"
        net_cash_total_minor:
          type: integer
          format: int64
          description: Net cash historico del jugador (depositos pagados menos retiros pagados de toda su vida), CLP.
        deposits_paid_total_minor:
          type: integer
          format: int64
          description: Depositos PAGADOS historicos del jugador (CLP). Columna "Depositos" de la tabla de Jugadores.
        withdrawals_paid_total_minor:
          type: integer
          format: int64
          description: Retiros PAGADOS historicos del jugador (CLP). Columna "Retiros" de la tabla de Jugadores.
        ltv_minor:
          type: integer
          format: int64
          description: LTV del jugador = NGR historico (suma sin solape de los periodos maximales de data_player_period_totals, misma regla que /player-totals/ltv), CLP. Sin periodos cargados es 0.
        deposits_loaded:
          type: boolean
          description: true si el jugador tiene algun deposito cargado en Convray (PRD-53); distingue el net cash $0 honesto del $0 por falta de historial cargado.
        deposited_window_minor:
          type: integer
          format: int64
          description: Suma de depositos PAGADOS del jugador dentro de la ventana dep_from/dep_to (CLP). 0 sin ventana de depositos activa.
        deposits_window_count:
          type: integer
          format: int64
          description: Cantidad de depositos PAGADOS del jugador dentro de la ventana dep_from/dep_to. 0 sin ventana activa.
        deposit_window_last_at:
          type: string
          format: date-time
          nullable: true
          description: Ultimo deposito PAGADO del jugador dentro de la ventana dep_from/dep_to; null si no hay o sin ventana activa.
    PlayerListPage:
      type: object
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/PlayerListRow"
        total:
          type: integer
          format: int64
    PlayerStatusOption:
      type: object
      properties:
        code:
          type: string
        label:
          type: string
    PlayerProfilePartner:
      type: object
      properties:
        slug:
          type: string
        name:
          type: string
    PlayerProfileFlags:
      type: object
      description: >-
        Banderas de atención al jugador (data_players); vip/vip_level/vip_experience/ex_vip/
        partner_tag/staff extienden el contrato con las etiquetas de Centrivo (PRD-50) sin
        romper los campos vigentes.
      properties:
        self_excluded:
          type: boolean
        blocked:
          type: boolean
        inactive:
          type: boolean
        attention_marked:
          type: boolean
        blacklisted:
          type: boolean
        vip:
          type: boolean
        vip_level:
          type: [string, "null"]
          enum:
            - diamante
            - zafiro
            - experience
            - null
        vip_experience:
          type: boolean
        ex_vip:
          type: boolean
        partner_tag:
          type: boolean
        staff:
          type: boolean
        self_excluded_at:
          type: [string, "null"]
          format: date-time
          description: >-
            Fecha de la autoexclusión más reciente (UTC), PRD-50. Sólo viene si el jugador está
            autoexcluido y ya se sincronizó; null mientras no haya.
        self_exclusion_ends_at:
          type: [string, "null"]
          format: date-time
          description: Fin de la limitación (UTC); null si es indefinida.
        self_exclusion_indefinite:
          type: boolean
          description: La autoexclusión no tiene fin (indefinida).
        self_exclusion_applied_by:
          type: [string, "null"]
          enum:
            - player
            - operator
            - unknown
            - null
          description: Quién aplicó la autoexclusión, normalizado. Nunca el nombre del operador (PII).
        self_exclusion_date_status:
          type: [string, "null"]
          enum:
            - synced
            - no_history
            - pending
            - null
          description: >-
            Estado de la fecha cuando el jugador está autoexcluido hoy (PRD-50, arreglo 2026-09-24):
            "synced" (fecha sincronizada), "no_history" (no tiene historial de juego responsable en
            Centrivo: la ficha muestra "Sin fecha en Centrivo") o "pending" (aún no consultado /
            pendiente de sincronizar). null si el jugador no está autoexcluido.
    PlayerProfileIdentity:
      type: object
      properties:
        external_player_ref:
          type: string
        casino_player_ref:
          type: string
        account_status:
          type: string
        registered_at:
          type: string
          format: date-time
        country:
          type: string
        region:
          type: string
        city:
          type: string
        currency:
          type: string
        registration_device:
          type: string
        sport_status:
          type: string
        last_login_at:
          type:
            - string
            - "null"
          format: date-time
        has_deposited:
          type: boolean
        btag:
          type: string
        btag_prefix:
          type: string
        partner:
          oneOf:
            - $ref: "#/components/schemas/PlayerProfilePartner"
            - type: "null"
          description: Socio resuelto del jugador; null si no mapea a ninguno.
        flags:
          $ref: "#/components/schemas/PlayerProfileFlags"
    PlayerDepositMark:
      type: object
      properties:
        at:
          type: string
          format: date-time
        amount_minor:
          type: integer
          format: int64
    PlayerProfileKPIs:
      type: object
      properties:
        deposits_paid_amount_minor:
          type: integer
          format: int64
        withdrawals_paid_amount_minor:
          type: integer
          format: int64
        net_cash_minor:
          type: integer
          format: int64
        casino_bet_amount_minor:
          type: integer
          format: int64
        sport_bet_amount_minor:
          type: integer
          format: int64
        ggr_no_bonus_minor:
          type: integer
          format: int64
        ngr_minor:
          type: integer
          format: int64
        ltv_ngr_minor:
          type: integer
          format: int64
        deposits_loaded:
          type: boolean
          description: true si el jugador tiene algun deposito cargado en Convray (PRD-53); distingue el net cash $0 honesto del $0 por falta de historial cargado.
    PlayerProfileCounts:
      type: object
      properties:
        deposits_paid:
          type: integer
          format: int64
        deposits_failed:
          type: integer
          format: int64
        withdrawals:
          type: integer
          format: int64
        sport_bets:
          type: integer
          format: int64
        casino_days:
          type: integer
          format: int64
    PlayerProfile:
      type: object
      properties:
        identity:
          $ref: "#/components/schemas/PlayerProfileIdentity"
        verified:
          $ref: "#/components/schemas/PlayerVerified"
        first_paid_deposit:
          oneOf:
            - $ref: "#/components/schemas/PlayerDepositMark"
            - type: "null"
        last_paid_deposit:
          oneOf:
            - $ref: "#/components/schemas/PlayerDepositMark"
            - type: "null"
        kpis:
          $ref: "#/components/schemas/PlayerProfileKPIs"
        counts:
          $ref: "#/components/schemas/PlayerProfileCounts"
        pii:
          oneOf:
            - $ref: "#/components/schemas/PlayerPII"
            - type: "null"
          description: PII del jugador (solo con permiso); null para el resto.
        player_isp:
          $ref: "#/components/schemas/PlayerIsp"
    PlayerActivityRow:
      type: object
      properties:
        kind:
          type: string
          description: registration | deposit | withdrawal | sport_bet | casino.
        at:
          type: string
          format: date-time
        ref:
          type: string
        label:
          type: string
        status:
          type: string
        amount_minor:
          type: integer
          format: int64
    PlayerActivityPage:
      type: object
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/PlayerActivityRow"
        total:
          type: integer
          format: int64
    PlayerProfileDepositRow:
      type: object
      properties:
        ref:
          type: string
        created_at:
          type: string
          format: date-time
        status:
          type: string
        payment_method:
          type: string
        device_type:
          type: string
        amount_minor:
          type: integer
          format: int64
    PlayerProfileDepositsPage:
      type: object
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/PlayerProfileDepositRow"
        total:
          type: integer
          format: int64
    PlayerProfileBetRow:
      type: object
      properties:
        ref:
          type: string
        created_at:
          type: string
          format: date-time
        bet_type:
          type: string
        sport_bet_type:
          type: string
        state:
          type: string
        odd:
          type:
            - number
            - "null"
        cash_out:
          type: boolean
        bonus_bet:
          type: boolean
        bet_amount_minor:
          type: integer
          format: int64
        win_amount_minor:
          type: integer
          format: int64
        ggr_minor:
          type: integer
          format: int64
    PlayerProfileBetsPage:
      type: object
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/PlayerProfileBetRow"
        total:
          type: integer
          format: int64
    PlayerProfileMonthlyRow:
      type: object
      properties:
        month:
          type: string
          description: Mes YYYY-MM en America/Santiago.
        deposits_minor:
          type: integer
          format: int64
        deposits_count:
          type: integer
          format: int64
        withdrawals_minor:
          type: integer
          format: int64
        withdrawals_count:
          type: integer
          format: int64
        net_cash_minor:
          type: integer
          format: int64
          description: Depósitos pagados menos retiros pagados del mes.
        ggr_minor:
          type: integer
          format: int64
          description: GGR del mes SIN apuestas con bono (casino + deportes, ledger diario).
    PlayerProfileMonthlyPage:
      type: object
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/PlayerProfileMonthlyRow"
    PlayerProfileGameRow:
      type: object
      properties:
        game_id:
          type: string
        game:
          type: string
        provider:
          type: string
        bet_amount_minor:
          type: integer
          format: int64
        pct:
          type: integer
    PlayerProfileGamesPage:
      type: object
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/PlayerProfileGameRow"
        total_bet_minor:
          type: integer
          format: int64
    PlayerProfileWithdrawalRow:
      type: object
      properties:
        ref:
          type: string
        created_at:
          type: string
          format: date-time
        status:
          type: string
        payment_method:
          type: string
        device_type:
          type: string
        amount_minor:
          type: integer
          format: int64
    PlayerProfileWithdrawalsPage:
      type: object
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/PlayerProfileWithdrawalRow"
        total:
          type: integer
          format: int64
    PlayerProfileCasinoRow:
      type: object
      properties:
        day:
          type: string
        game_id:
          type: string
        game:
          type: string
        provider:
          type: string
        bets_count:
          type: integer
          format: int64
        bet_amount_minor:
          type: integer
          format: int64
        win_amount_minor:
          type: integer
          format: int64
        ggr_minor:
          type: integer
          format: int64
    PlayerProfileCasinoPage:
      type: object
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/PlayerProfileCasinoRow"
        total:
          type: integer
          format: int64
    CasinoGameDailyRow:
      type: object
      properties:
        day:
          type: string
        ggr_minor:
          type: integer
          format: int64
        bet_amount_minor:
          type: integer
          format: int64
    CasinoGameDailyPage:
      type: object
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/CasinoGameDailyRow"
    AnalyticsRange:
      type: object
      properties:
        from:
          type: string
          format: date
        to:
          type: string
          format: date
    PlayerRankingItem:
      type: object
      properties:
        player:
          type: string
          description: external_player_ref; enmascarado si el rol no tiene PII.
        current_minor:
          type: integer
          format: int64
        previous_minor:
          type: integer
          format: int64
        delta_minor:
          type: integer
          format: int64
        delta_pct:
          type:
            - number
            - "null"
          description: Variación con una decimal; null si el período anterior fue 0.
        deposits_count:
          type: integer
          format: int64
        last_deposit_day:
          type:
            - string
            - "null"
        segment:
          type: string
          description: partner | organic.
        btag:
          type: string
        pii:
          oneOf:
            - $ref: "#/components/schemas/PlayerPII"
            - type: "null"
        player_flags:
          $ref: "#/components/schemas/PlayerFlags"
        player_isp:
          $ref: "#/components/schemas/PlayerIsp"
        net_cash_total_minor:
          type: integer
          format: int64
          description: Net cash historico del jugador (depositos pagados menos retiros pagados de toda su vida), CLP.
        deposits_loaded:
          type: boolean
          description: true si el jugador tiene algun deposito cargado en Convray (PRD-53); distingue el net cash $0 honesto del $0 por falta de historial cargado.
    PlayerRankingsResponse:
      type: object
      properties:
        range:
          $ref: "#/components/schemas/AnalyticsRange"
        previous_range:
          $ref: "#/components/schemas/AnalyticsRange"
        metric:
          type: string
        direction:
          type: string
        currency:
          type: string
        items:
          type: array
          items:
            $ref: "#/components/schemas/PlayerRankingItem"
    RestrictFlag:
      type: object
      properties:
        self_excluded:
          type: boolean
        blocked:
          type: boolean
        inactive:
          type: boolean
        attention:
          type: boolean
    RiskActivityDrop:
      type: object
      properties:
        player:
          type: string
        previous_deposits_minor:
          type: integer
          format: int64
        current_deposits_minor:
          type: integer
          format: int64
        drop_pct:
          type: number
        previous_deposits_count:
          type: integer
          format: int64
        segment:
          type: string
        btag:
          type: string
        pii:
          oneOf:
            - $ref: "#/components/schemas/PlayerPII"
            - type: "null"
        player_flags:
          $ref: "#/components/schemas/PlayerFlags"
        player_isp:
          $ref: "#/components/schemas/PlayerIsp"
        net_cash_total_minor:
          type: integer
          format: int64
          description: Net cash historico del jugador (depositos pagados menos retiros pagados de toda su vida), CLP.
        deposits_loaded:
          type: boolean
          description: true si el jugador tiene algun deposito cargado en Convray (PRD-53); distingue el net cash $0 honesto del $0 por falta de historial cargado.
    RiskActivityDropList:
      type: object
      properties:
        count:
          type: integer
        total_previous_minor:
          type: integer
          format: int64
        total_current_minor:
          type: integer
          format: int64
        items:
          type: array
          items:
            $ref: "#/components/schemas/RiskActivityDrop"
    RiskWithdrawalItem:
      type: object
      properties:
        player:
          type: string
        withdrawals_minor:
          type: integer
          format: int64
        withdrawals_count:
          type: integer
          format: int64
        deposits_minor:
          type: integer
          format: int64
        net_minor:
          type: integer
          format: int64
        avg_history_minor:
          type: integer
          format: int64
        factor:
          type:
            - number
            - "null"
        reason:
          type: string
          description: spike | negative_net.
        segment:
          type: string
        btag:
          type: string
        pii:
          oneOf:
            - $ref: "#/components/schemas/PlayerPII"
            - type: "null"
        player_flags:
          $ref: "#/components/schemas/PlayerFlags"
        player_isp:
          $ref: "#/components/schemas/PlayerIsp"
        net_cash_total_minor:
          type: integer
          format: int64
          description: Net cash historico del jugador (depositos pagados menos retiros pagados de toda su vida), CLP.
        deposits_loaded:
          type: boolean
          description: true si el jugador tiene algun deposito cargado en Convray (PRD-53); distingue el net cash $0 honesto del $0 por falta de historial cargado.
    RiskWithdrawalList:
      type: object
      properties:
        count:
          type: integer
        total_minor:
          type: integer
          format: int64
        items:
          type: array
          items:
            $ref: "#/components/schemas/RiskWithdrawalItem"
    RiskRestrictedItem:
      type: object
      properties:
        player:
          type: string
        deposits_minor:
          type: integer
          format: int64
        deposits_count:
          type: integer
          format: int64
        account_status:
          type: string
        flags:
          $ref: "#/components/schemas/RestrictFlag"
        segment:
          type: string
        btag:
          type: string
        pii:
          oneOf:
            - $ref: "#/components/schemas/PlayerPII"
            - type: "null"
        player_flags:
          $ref: "#/components/schemas/PlayerFlags"
        player_isp:
          $ref: "#/components/schemas/PlayerIsp"
        net_cash_total_minor:
          type: integer
          format: int64
          description: Net cash historico del jugador (depositos pagados menos retiros pagados de toda su vida), CLP.
        deposits_loaded:
          type: boolean
          description: true si el jugador tiene algun deposito cargado en Convray (PRD-53); distingue el net cash $0 honesto del $0 por falta de historial cargado.
    RiskRestrictedList:
      type: object
      properties:
        count:
          type: integer
        total_deposits_minor:
          type: integer
          format: int64
        self_excluded:
          type: integer
        blocked:
          type: integer
        inactive:
          type: integer
        attention:
          type: integer
        items:
          type: array
          items:
            $ref: "#/components/schemas/RiskRestrictedItem"
    PlayerRiskResponse:
      type: object
      properties:
        range:
          $ref: "#/components/schemas/AnalyticsRange"
        previous_range:
          $ref: "#/components/schemas/AnalyticsRange"
        currency:
          type: string
        activity_drop:
          $ref: "#/components/schemas/RiskActivityDropList"
        abnormal_withdrawals:
          $ref: "#/components/schemas/RiskWithdrawalList"
        restricted_depositors:
          $ref: "#/components/schemas/RiskRestrictedList"
    CohortRetention:
      type: object
      properties:
        m:
          type: integer
          description: Offset del mes (0..n).
        active:
          type: integer
          format: int64
        pct:
          type: number
    CohortRow:
      type: object
      properties:
        cohort:
          type: string
          description: Mes de registro (YYYY-MM).
        size:
          type: integer
          format: int64
        first_deposit_count:
          type: integer
          format: int64
        conversion_pct:
          type: number
        retention:
          type: array
          items:
            $ref: "#/components/schemas/CohortRetention"
    PlayerCohortsResponse:
      type: object
      properties:
        basis:
          type: string
          description: deposits | bets.
        months:
          type: integer
        generated_at:
          type: string
          format: date-time
        cohorts:
          type: array
          items:
            $ref: "#/components/schemas/CohortRow"
    PlayerActiveByLoginResponse:
      type: object
      properties:
        range:
          $ref: "#/components/schemas/AnalyticsRange"
        active_by_login:
          type: integer
          format: int64
        logged_in_ever:
          type: integer
          format: int64
        reliable_since:
          type: string
          format: date
        is_snapshot:
          type: boolean
        note:
          type: string
    VIPRecoveryResponse:
      type: object
      description: >-
        Recuperación VIP del mes (PRD-50 Fase 3). Universo = jugadores con etiqueta vip vigente
        (Diamante + Zafiro). Los autoexcluidos y bloqueados no aparecen en las listas; van solo en
        el resumen. Montos en unidad menor (CLP: minor == mayor).
      required:
        - month
        - previous_month
        - days_elapsed
        - days_in_previous_month
        - summary
        - apagados
        - bajando_ritmo
        - net_cash_negativo
      properties:
        month:
          type: string
          description: Mes evaluado YYYY-MM.
        previous_month:
          type: string
          description: Mes anterior YYYY-MM.
        days_elapsed:
          type: integer
          description: Días civiles transcurridos del mes evaluado (incluido hoy si es el mes actual).
        days_in_previous_month:
          type: integer
        days_in_month:
          type: integer
          description: Días del mes evaluado (para proyectar un mes en curso).
        deposits_as_of:
          type: [string, "null"]
          format: date-time
        status_as_of:
          type: [string, "null"]
          format: date-time
        summary:
          $ref: "#/components/schemas/VIPRecoverySummary"
        apagados:
          type: array
          items:
            $ref: "#/components/schemas/VIPRecoveryRow"
        bajando_ritmo:
          type: array
          items:
            $ref: "#/components/schemas/VIPRecoveryRow"
        net_cash_negativo:
          type: array
          items:
            $ref: "#/components/schemas/VIPRecoveryRow"
        group:
          $ref: "#/components/schemas/VIPGroupTotals"
    VIPGroupTotals:
      type: object
      description: >-
        Depósitos pagados del grupo VIP COMPLETO (incluidos autoexcluidos y bloqueados) en el mes
        evaluado y los dos anteriores, en total y por nivel (diamante, zafiro, experience, sin_nivel).
      required:
        - total
        - by_level
      properties:
        total:
          $ref: "#/components/schemas/VIPGroupLevel"
        by_level:
          type: object
          additionalProperties:
            $ref: "#/components/schemas/VIPGroupLevel"
    VIPGroupLevel:
      type: object
      required:
        - players
        - month
        - previous_month
        - prev2_month
      properties:
        players:
          type: integer
        month:
          $ref: "#/components/schemas/VIPGroupMonth"
        previous_month:
          $ref: "#/components/schemas/VIPGroupMonth"
        prev2_month:
          $ref: "#/components/schemas/VIPGroupMonth"
    VIPGroupMonth:
      type: object
      required:
        - deposits_minor
        - depositors
      properties:
        deposits_minor:
          type: integer
          format: int64
        depositors:
          type: integer
          description: VIP con al menos un depósito pagado en el mes.
        withdrawals_minor:
          type: integer
          format: int64
          description: Retiros pagados; solo se informa en el mes evaluado.
    VIPRecoverySummary:
      type: object
      required:
        - vip_total
        - apagados
        - bajando_ritmo
        - net_cash_negativo
        - excluded_self_excluded
        - excluded_blocked
        - apagados_prev_month_deposits_minor
      properties:
        vip_total:
          type: integer
          description: Total del universo VIP (incluye a los excluidos).
        apagados:
          type: integer
        bajando_ritmo:
          type: integer
        net_cash_negativo:
          type: integer
        excluded_self_excluded:
          type: integer
        excluded_blocked:
          type: integer
        apagados_prev_month_deposits_minor:
          type: integer
          format: int64
    VIPRecoveryRow:
      type: object
      description: >-
        Fila de una lista de recuperación. external_player_ref == casino_player_ref (Player ID de
        Centrivo). Sin PII (nombres/contacto no). pace_pct solo aplica a "bajando ritmo".
      required:
        - external_player_ref
        - casino_player_ref
        - player_flags
        - prev2_month_deposits_minor
        - prev_month_deposits_minor
        - month_deposits_minor
        - month_withdrawals_minor
        - month_net_cash_minor
        - btag
      properties:
        external_player_ref:
          type: string
        casino_player_ref:
          type: string
        vip_level:
          type: [string, "null"]
          enum:
            - diamante
            - zafiro
            - experience
            - null
        player_flags:
          $ref: "#/components/schemas/PlayerFlags"
        player_isp:
          $ref: "#/components/schemas/PlayerIsp"
        net_cash_total_minor:
          type: integer
          format: int64
          description: Net cash historico del jugador (depositos pagados menos retiros pagados de toda su vida), CLP.
        deposits_loaded:
          type: boolean
          description: true si el jugador tiene algun deposito cargado en Convray (PRD-53); distingue el net cash $0 honesto del $0 por falta de historial cargado.
        prev2_month_deposits_minor:
          type: integer
          format: int64
        prev_month_deposits_minor:
          type: integer
          format: int64
        month_deposits_minor:
          type: integer
          format: int64
        month_withdrawals_minor:
          type: integer
          format: int64
        month_net_cash_minor:
          type: integer
          format: int64
        pace_pct:
          type: [number, "null"]
        last_deposit_at:
          type: [string, "null"]
          format: date
        days_since_last_deposit:
          type: [integer, "null"]
          format: int64
        btag:
          type: string
    VIPProgramCandidatesResponse:
      type: object
      description: >-
        Candidatos del programa de detección temprana de jugadores VIP (VIP-07) para un día
        puntuado. Los escribe el worker diario en data_vip_candidates; el runtime solo lee bajo la
        RLS de producto (rol mínimo viewer). Sin PII: external_player_ref es el Player ID de Centrivo.
      required:
        - score_date
        - items
        - summary
      properties:
        score_date:
          type: string
          description: Fecha puntuada resuelta YYYY-MM-DD; vacía si el programa aún no puntuó ningún día.
        items:
          type: array
          items:
            $ref: "#/components/schemas/VIPProgramCandidateRow"
        summary:
          $ref: "#/components/schemas/VIPProgramSummary"
    VIPProgramCandidateRow:
      type: object
      description: >-
        Candidato del día. external_player_ref es el Player ID de Centrivo (clave de contacto, no
        PII). reasons son motivos legibles con cifras; rg_signals lista las señales de juego
        responsable que activaron el filtro.
      required:
        - external_player_ref
        - lane
        - tier
        - days_since_anchor
        - score
        - reasons
        - rg_flag
        - rg_signals
        - assigned_group
        - rule_version
      properties:
        external_player_ref:
          type: string
        lane:
          type: string
          enum:
            - new
            - escalation
        tier:
          type: string
          enum:
            - day1_alert
            - priority
            - list
            - escalation
        days_since_anchor:
          type: integer
        score:
          type: number
        reasons:
          type: array
          items:
            type: string
        rg_flag:
          type: boolean
        rg_signals:
          type: array
          items:
            type: string
        assigned_group:
          type: string
          enum:
            - treatment
            - control
        rule_version:
          type: string
    VIPProgramSummary:
      type: object
      description: >-
        Conteos del día ya filtrado. total = filas devueltas; rg_blocked = cuántas traen la marca de
        juego responsable; treatment/control = reparto del experimento.
      required:
        - total
        - by_lane
        - by_tier
        - rg_blocked
        - treatment
        - control
      properties:
        total:
          type: integer
        by_lane:
          $ref: "#/components/schemas/VIPProgramByLane"
        by_tier:
          $ref: "#/components/schemas/VIPProgramByTier"
        rg_blocked:
          type: integer
        treatment:
          type: integer
        control:
          type: integer
    VIPProgramByLane:
      type: object
      required:
        - new
        - escalation
      properties:
        new:
          type: integer
        escalation:
          type: integer
    VIPProgramByTier:
      type: object
      required:
        - day1_alert
        - priority
        - list
        - escalation
      properties:
        day1_alert:
          type: integer
        priority:
          type: integer
        list:
          type: integer
        escalation:
          type: integer
    DataFilterOptionsResponse:
      type: object
      required:
        - dataset
        - dimensions
      properties:
        dataset:
          type: string
        dimensions:
          type: array
          items:
            $ref: "#/components/schemas/DataFilterDimensionValue"
    DataFilterDimensionValue:
      type: object
      required:
        - dimension
        - values
      properties:
        dimension:
          type: string
        values:
          type: array
          items:
            $ref: "#/components/schemas/DataFilterOptionValue"
    DataFilterOptionValue:
      type: object
      required:
        - value
        - label
        - count
      properties:
        value:
          type: string
        label:
          type: string
        count:
          type: integer
          format: int64
    DataMetricAlertCatalogResponse:
      type: object
      required:
        - metrics
      properties:
        metrics:
          type: array
          items:
            $ref: "#/components/schemas/DataMetricAlertMetric"
    DataMetricAlertMetric:
      type: object
      required:
        - code
        - label
        - unit
      properties:
        code:
          type: string
        label:
          type: string
        unit:
          type: string
          description: Unidad de formato (money, count, percent).
          enum:
            - money
            - count
            - percent
    DataMetricAlertRuleList:
      type: object
      required:
        - rules
      properties:
        rules:
          type: array
          items:
            $ref: "#/components/schemas/DataMetricAlertRule"
    DataMetricAlertRule:
      type: object
      required:
        - id
        - metric
        - comparison
        - threshold
        - baseline_days
        - severity
        - active
        - created_by
        - created_at
        - updated_at
      properties:
        id:
          type: string
          format: uuid
        metric:
          type: string
        comparison:
          type: string
          enum:
            - below
            - above
            - pct_drop
            - pct_rise
        threshold:
          type: number
        baseline_days:
          type:
            - integer
            - "null"
        severity:
          type: string
          enum:
            - info
            - warning
            - critical
        active:
          type: boolean
        created_by:
          type: string
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    DataMetricAlertRuleCreateBody:
      type: object
      required:
        - metric
        - comparison
        - threshold
      properties:
        metric:
          type: string
          description: Código del catálogo de métricas vigilables.
        comparison:
          type: string
          enum:
            - below
            - above
            - pct_drop
            - pct_rise
        threshold:
          type: number
        baseline_days:
          type:
            - integer
            - "null"
          description: Sólo para pct_drop/pct_rise; debe ser 7 o 28.
        severity:
          type: string
          enum:
            - info
            - warning
            - critical
        active:
          type:
            - boolean
            - "null"
      additionalProperties: false
    DataMetricAlertRulePatchBody:
      type: object
      properties:
        threshold:
          type:
            - number
            - "null"
        baseline_days:
          type:
            - integer
            - "null"
        severity:
          type:
            - string
            - "null"
        active:
          type:
            - boolean
            - "null"
      additionalProperties: false
    DataMetricAlertEventList:
      type: object
      required:
        - events
      properties:
        events:
          type: array
          items:
            $ref: "#/components/schemas/DataMetricAlertEvent"
    DataMetricAlertEvent:
      type: object
      required:
        - id
        - rule_id
        - civil_day
        - metric
        - comparison
        - severity
        - value
        - reference
        - status
        - opened_at
        - acknowledged_by
        - acknowledged_at
        - resolved_by
        - resolved_at
      properties:
        id:
          type: string
          format: uuid
        rule_id:
          type: string
          format: uuid
        civil_day:
          type: string
          format: date
        metric:
          type: string
        comparison:
          type: string
        severity:
          type: string
        value:
          type: number
        reference:
          type:
            - number
            - "null"
        status:
          type: string
        opened_at:
          type: string
          format: date-time
        acknowledged_by:
          type: string
        acknowledged_at:
          type:
            - string
            - "null"
          format: date-time
        resolved_by:
          type: string
        resolved_at:
          type:
            - string
            - "null"
          format: date-time
    DataSegmentList:
      type: object
      required:
        - segments
      properties:
        segments:
          type: array
          items:
            $ref: "#/components/schemas/DataSegment"
    DataSegment:
      type: object
      required:
        - id
        - name
        - description
        - dataset
        - kind
        - filters
        - shared
        - created_by
        - created_at
        - updated_at
        - player_count
        - contactable_count
        - non_contactable_count
        - last_evaluated_at
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        description:
          type: string
        dataset:
          type: string
        kind:
          type: string
          description: dynamic (filtros re-evaluados) o static (foto de Player IDs).
          enum:
            - dynamic
            - static
        filters:
          type: object
          additionalProperties: true
          description: Objeto de filtros del contrato (vacío para estáticos).
        shared:
          type: boolean
        created_by:
          type: string
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        player_count:
          type:
            - integer
            - "null"
          format: int64
        contactable_count:
          type:
            - integer
            - "null"
          format: int64
        non_contactable_count:
          type:
            - integer
            - "null"
          format: int64
        last_evaluated_at:
          type:
            - string
            - "null"
          format: date-time
    DataSegmentCreateBody:
      type: object
      required:
        - name
      properties:
        name:
          type: string
        description:
          type: string
        shared:
          type: boolean
        filters:
          type: object
          additionalProperties: true
          description: Objeto de filtros del contrato (dataset players).
      additionalProperties: false
    DataSegmentPatchBody:
      type: object
      properties:
        name:
          type:
            - string
            - "null"
        description:
          type:
            - string
            - "null"
        shared:
          type:
            - boolean
            - "null"
        filters:
          type: object
          additionalProperties: true
          description: Objeto de filtros del contrato; se ignora si es null.
      additionalProperties: false
    DataSegmentPreview:
      type: object
      required:
        - segment_id
        - from
        - to
        - player_count
        - self_excluded
        - blocked
        - contactable
        - deposits_minor
        - net_cash_minor
        - ggr_no_bonus_minor
      properties:
        segment_id:
          type: string
        from:
          type: string
          format: date
        to:
          type: string
          format: date
        player_count:
          type: integer
          format: int64
        self_excluded:
          type: integer
          format: int64
        blocked:
          type: integer
          format: int64
        contactable:
          type: integer
          format: int64
        deposits_minor:
          type: integer
          format: int64
        net_cash_minor:
          type: integer
          format: int64
        ggr_no_bonus_minor:
          type: integer
          format: int64
    DataSegmentUploadList:
      type: object
      required:
        - uploads
      properties:
        uploads:
          type: array
          items:
            $ref: "#/components/schemas/DataSegmentUpload"
    DataSegmentUpload:
      type: object
      required:
        - id
        - file_name
        - file_sha256
        - uploaded_by
        - uploaded_at
        - rows_read
        - rows_valid
        - rows_unknown
        - rows_duplicate
        - rows_excluded
      properties:
        id:
          type: string
          format: uuid
        file_name:
          type: string
        file_sha256:
          type: string
          description: Hash hex del archivo; cadena vacía si no se guardó.
        uploaded_by:
          type: string
          format: uuid
        uploaded_at:
          type: string
          format: date-time
        rows_read:
          type: integer
          format: int64
        rows_valid:
          type: integer
          format: int64
        rows_unknown:
          type: integer
          format: int64
        rows_duplicate:
          type: integer
          format: int64
        rows_excluded:
          type: integer
          format: int64
    DataStaticImportBody:
      type: object
      required:
        - name
        - player_ids
      properties:
        name:
          type: string
        description:
          type: string
        shared:
          type: boolean
        player_ids:
          type: array
          items:
            type: string
      additionalProperties: false
    DataStaticImportMultipart:
      type: object
      required:
        - file
        - name
      properties:
        file:
          type: string
          format: binary
          description: CSV de una columna de Player ID (encabezado opcional).
        name:
          type: string
        description:
          type: string
        shared:
          type: string
          description: Valor booleano de formulario (true/1/yes/sí/si).
    DataStaticImportResult:
      type: object
      required:
        - segment
        - import
        - file_name
        - captured_at
      properties:
        segment:
          $ref: "#/components/schemas/DataSegment"
        import:
          $ref: "#/components/schemas/DataStaticImportCounts"
        file_name:
          type: string
        captured_at:
          type: string
          format: date-time
    DataStaticImportCounts:
      type: object
      required:
        - rows_read
        - valid
        - duplicate
        - unknown
        - excluded
      properties:
        rows_read:
          type: integer
        valid:
          type: integer
        duplicate:
          type: integer
        unknown:
          type: integer
        excluded:
          type: integer
    DataReportRequest:
      type: object
      required:
        - kind
      properties:
        kind:
          type: string
          enum:
            - monthly_summary
            - deposits
            - withdrawals
            - casino
            - sports
            - players
            - segment
        from:
          type: string
          format: date
        to:
          type: string
          format: date
        filters:
          type: object
          additionalProperties: true
          description: Filtros del contrato (según el kind); el rango del informe manda.
        segment:
          type: string
          description: Id del segmento (obligatorio cuando kind es segment).
      additionalProperties: false
    DataReport:
      type: object
      required:
        - kind
        - title
        - period
        - generated_at
        - sections
        - responsible_gaming
      properties:
        kind:
          type: string
        title:
          type: string
        period:
          $ref: "#/components/schemas/DataReportPeriod"
        generated_at:
          type: string
          format: date-time
        segment_name:
          type: string
        sections:
          type: array
          items:
            $ref: "#/components/schemas/DataReportSection"
        responsible_gaming:
          oneOf:
            - $ref: "#/components/schemas/DataReportResponsibleGaming"
            - type: "null"
    DataReportPeriod:
      type: object
      required:
        - from
        - to
        - label
      properties:
        from:
          type: string
          format: date
        to:
          type: string
          format: date
        label:
          type: string
    DataReportSection:
      type: object
      required:
        - heading
        - metrics
        - tables
        - findings
      properties:
        heading:
          type: string
        metrics:
          type: array
          items:
            $ref: "#/components/schemas/DataReportMetric"
        tables:
          type: array
          items:
            $ref: "#/components/schemas/DataReportTable"
        findings:
          type: array
          items:
            $ref: "#/components/schemas/DataReportFinding"
    DataReportMetric:
      type: object
      required:
        - label
        - value
        - unit
        - delta
      properties:
        label:
          type: string
        value:
          type: integer
          format: int64
        unit:
          type: string
          enum:
            - money
            - count
            - percent
        delta:
          oneOf:
            - $ref: "#/components/schemas/DataReportDelta"
            - type: "null"
    DataReportDelta:
      type: object
      required:
        - previous
        - change
        - change_pct
      properties:
        previous:
          type: integer
          format: int64
        change:
          type: integer
          format: int64
        change_pct:
          type:
            - number
            - "null"
    DataReportTable:
      type: object
      required:
        - title
        - columns
        - column_units
        - rows
      properties:
        title:
          type: string
        columns:
          type: array
          items:
            type: string
        column_units:
          type: array
          description: Unidad de cada columna (text, count, money).
          items:
            type: string
        rows:
          type: array
          items:
            type: array
            items:
              type: string
    DataReportFinding:
      type: object
      required:
        - text
        - source
      properties:
        text:
          type: string
        source:
          type: string
    DataReportResponsibleGaming:
      type: object
      required:
        - player_count
        - non_contactable
      properties:
        player_count:
          type: integer
          format: int64
        non_contactable:
          type: integer
          format: int64
    CentrivoConnectorView:
      type: object
      required:
        - settings
        - reports
        - payments
        - server_time
      properties:
        settings:
          $ref: "#/components/schemas/CentrivoSettingsView"
        reports:
          type: array
          items:
            $ref: "#/components/schemas/CentrivoReportView"
        payments:
          type: array
          description: >-
            Estado de depósitos y retiros (pagos que cambian de estado después de creados). Siempre
            dos filas, en orden deposits, withdrawals.
          items:
            $ref: "#/components/schemas/CentrivoPaymentView"
        server_time:
          type: string
          format: date-time
    CentrivoPaymentView:
      type: object
      required:
        - report
        - window_hours
        - paid
        - pending
        - in_process
        - rejected
        - open_older
        - resolved_24h
        - last_check
        - last_sweep
      properties:
        report:
          type: string
          enum: [deposits, withdrawals]
        window_hours:
          type: integer
          description: Ventana reciente de los conteos por estado (48).
        paid:
          type: integer
          format: int64
          description: Pagados creados dentro de la ventana.
        pending:
          type: integer
          format: int64
          description: Inicializados, pendientes y por pagar creados dentro de la ventana.
        in_process:
          type: integer
          format: int64
          description: En proceso creados dentro de la ventana.
        rejected:
          type: integer
          format: int64
          description: Fallidos, rechazados y cancelados creados dentro de la ventana.
        open_older:
          type: integer
          format: int64
          description: Abiertos creados antes de la ventana (los cierra el barrido o la recarga semanal).
        resolved_24h:
          type: integer
          format: int64
          description: Abiertos que cambiaron de estado con las corridas de las últimas 24 h.
        last_check:
          anyOf:
            - $ref: "#/components/schemas/CentrivoPaymentCheckView"
            - type: "null"
        last_sweep:
          anyOf:
            - $ref: "#/components/schemas/CentrivoPaymentCheckView"
            - type: "null"
    CentrivoPaymentCheckView:
      type: object
      required:
        - at
        - trigger
        - period_from
        - period_to
        - open_before
        - resolved
      properties:
        at:
          type: string
          format: date-time
        trigger:
          type: string
        period_from:
          type: [string, "null"]
          format: date
        period_to:
          type: [string, "null"]
          format: date
        open_before:
          type: integer
          description: Pagos abiertos en la ventana de la corrida antes de importar.
        resolved:
          type: integer
          description: De esos, cuántos cambiaron de estado con la carga.
    CentrivoSettingsView:
      type: object
      required:
        - enabled
        - interval_minutes
        - paused_reason
        - next_run_at
        - requested_reports
      properties:
        enabled:
          type: boolean
        interval_minutes:
          type: integer
        paused_reason:
          type: [string, "null"]
        next_run_at:
          type: [string, "null"]
          format: date-time
        requested_reports:
          type: array
          items:
            type: string
    CentrivoReportView:
      type: object
      required:
        - report
        - dataset_code
        - cadence
        - last_run
        - next_run_at
        - covered_until
        - gap_since
      properties:
        report:
          type: string
          description: >-
            Código del reporte, en el orden fijo del conector: deposits, withdrawals, players,
            player_period_totals, casino_bets, sport_bets y bonuses. bonuses (regalías, PRD-52) va
            al final y no baja archivo ni crea lote de importación: lee de Centrivo las campañas
            cross-platform y normales (con su disparador), los bonos entregados por jugador con el
            depósito que activó cada uno y el turnover exigido, los packs de promo codes con sus
            usos por jugador y la lista de jugadores de prueba. Un bono entregado se guarda una
            sola vez, aunque Centrivo lo muestre en varias vistas. Un código desconocido se muestra
            tal cual.
        dataset_code:
          type: string
          description: Dataset de la corrida; en bonuses es bonuses.
        cadence:
          type: string
        last_run:
          anyOf:
            - $ref: "#/components/schemas/CentrivoLastRunView"
            - type: "null"
        next_run_at:
          type: [string, "null"]
          format: date-time
        covered_until:
          type: [string, "null"]
          format: date-time
          description: window_to de la última corrida exitosa (hasta dónde hay datos), o null.
        gap_since:
          type: [string, "null"]
          format: date-time
          description: Marca un hueco de cobertura cuando el conector se atrasó más de 4 h, o null.
    CentrivoLastRunView:
      type: object
      required:
        - id
        - status
        - trigger
        - started_at
        - finished_at
        - period_from
        - period_to
        - window_from
        - window_to
        - file_name
        - file_size
        - rows_read
        - import_batch_id
        - error_code
        - error_message
      properties:
        id:
          type: string
        status:
          type: string
        trigger:
          type: string
        started_at:
          type: string
          format: date-time
        finished_at:
          type: [string, "null"]
          format: date-time
        period_from:
          type: [string, "null"]
          format: date-time
        period_to:
          type: [string, "null"]
          format: date-time
        window_from:
          type: [string, "null"]
          format: date-time
          description: Ventana UTC exacta pedida a Centrivo; null en corridas históricas.
        window_to:
          type: [string, "null"]
          format: date-time
          description: Ventana UTC exacta pedida a Centrivo; null en corridas históricas.
        file_name:
          type: [string, "null"]
        file_size:
          type: [integer, "null"]
          format: int64
        rows_read:
          type: [integer, "null"]
        import_batch_id:
          type: [string, "null"]
        error_code:
          type: [string, "null"]
        error_message:
          type: [string, "null"]
    CentrivoPutSettingsRequest:
      type: object
      required:
        - enabled
        - interval_minutes
      properties:
        enabled:
          type: boolean
        interval_minutes:
          type: integer
          minimum: 30
          maximum: 1440
          description: Intervalo de corrida en minutos (entre 30 y 1440).
      additionalProperties: false
    CentrivoRunRequest:
      type: object
      required:
        - report
      properties:
        report:
          type: string
          description: >-
            Código del reporte a encolar: deposits, withdrawals, players, player_period_totals,
            casino_bets, sport_bets o bonuses (regalías, campañas normales y promo codes desde
            Centrivo, solo lectura).
      additionalProperties: false
    CentrivoRunQueuedResponse:
      type: object
      required:
        - queued
      properties:
        queued:
          type: boolean
    CentrivoPausedError:
      type: object
      required:
        - error
      properties:
        error:
          type: string
          description: Siempre "paused" cuando el conector está pausado.
    SupportCreateTokenRequest:
      type: object
      required:
        - integration
        - name
      properties:
        integration:
          type: string
          maxLength: 60
          description: Nombre de la integración (obligatorio).
        name:
          type: string
          maxLength: 80
          description: Nombre del token (obligatorio).
        ip_allowlist:
          type: array
          items:
            type: string
          description: IPs o rangos CIDR permitidos. Vacío = sin restricción.
        expires_in_days:
          type: integer
          minimum: 1
          maximum: 365
          description: Días hasta el vencimiento (0 = default de 180 días).
      additionalProperties: false
    SupportTokenView:
      type: object
      required:
        - id
        - integration
        - name
        - prefix
        - scopes
        - ip_allowlist
        - expires_at
        - last_used_at
        - revoked_at
        - created_at
      properties:
        id:
          type: string
          format: uuid
        integration:
          type: string
        name:
          type: string
        prefix:
          type: string
        scopes:
          type: array
          items:
            type: string
        ip_allowlist:
          type: array
          items:
            type: string
        expires_at:
          type: string
          format: date-time
        last_used_at:
          type: [string, "null"]
          format: date-time
        revoked_at:
          type: [string, "null"]
          format: date-time
        created_at:
          type: string
          format: date-time
        created_by:
          description: Quien emitió el token.
          oneOf:
            - $ref: "#/components/schemas/ApiIntegrationActor"
            - type: "null"
        revoked_by:
          description: >-
            Quien revocó o rotó el token. null en un token vivo y en las revocaciones anteriores a la
            versión 0.64.0 (ese dato no se guardaba).
          oneOf:
            - $ref: "#/components/schemas/ApiIntegrationActor"
            - type: "null"
    ApiIntegrationActor:
      type: object
      description: Quien hizo un cambio en una integración (emitir, revocar, pausar o reanudar).
      required:
        - id
        - name
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
          description: Nombre para mostrar de la cuenta; si no tiene, su correo.
    ApiIntegrationState:
      type: object
      description: >-
        Estado de encendido de una API con tokens. Apagada es una pausa: la API responde 503
        integration_paused a las llamadas de esa integración y los tokens se conservan.
      required:
        - integration
        - enabled
        - updated_at
        - updated_by
      properties:
        integration:
          type: string
          description: support para la API de soporte; juego-octubre, juego-codigos o qa-retencion en retención.
          enum:
            - support
            - juego-octubre
            - juego-codigos
            - qa-retencion
        enabled:
          type: boolean
        updated_at:
          type: [string, "null"]
          format: date-time
          description: Última pausa o reanudación; null si nadie la cambió todavía.
        updated_by:
          description: Quien la pausó o la reanudó por última vez; null si nadie la cambió todavía.
          oneOf:
            - $ref: "#/components/schemas/ApiIntegrationActor"
            - type: "null"
    ApiIntegrationStateInput:
      type: object
      required:
        - enabled
      properties:
        enabled:
          type: boolean
          description: true enciende la integración; false la pausa (los tokens se conservan).
    SupportCreateTokenResponse:
      type: object
      required:
        - token
        - support_token
      properties:
        token:
          type: string
          description: Secreto en claro del token (se muestra una sola vez).
        support_token:
          $ref: "#/components/schemas/SupportTokenView"
    SupportTokenListResponse:
      type: object
      required:
        - support_tokens
      properties:
        support_tokens:
          type: array
          items:
            $ref: "#/components/schemas/SupportTokenView"
    SupportScheduleView:
      type: object
      required:
        - dataset
        - local_times
        - grace_minutes
        - timezone
        - updated_at
      properties:
        dataset:
          type: string
        local_times:
          type: array
          items:
            type: string
          description: Horas locales de carga en formato HH:MM.
        grace_minutes:
          type: integer
        timezone:
          type: string
        updated_at:
          type: string
          format: date-time
    SupportScheduleListResponse:
      type: object
      required:
        - schedules
      properties:
        schedules:
          type: array
          items:
            $ref: "#/components/schemas/SupportScheduleView"
    SupportPutScheduleRequest:
      type: object
      required:
        - local_times
        - timezone
      properties:
        local_times:
          type: array
          items:
            type: string
          description: Horas locales de carga en formato HH:MM.
        grace_minutes:
          type: integer
          minimum: 0
          maximum: 1440
          description: Minutos de gracia (0 aplica el default del servidor).
        timezone:
          type: string
      additionalProperties: false
    SupportAuditView:
      type: object
      required:
        - id
        - integration
        - player_ref_internal
        - endpoint
        - fields_returned
        - conversation_id
        - ip
        - status
        - latency_ms
        - created_at
      properties:
        id:
          type: string
        integration:
          type: string
        player_ref_internal:
          type: [string, "null"]
        endpoint:
          type: string
        fields_returned:
          type: array
          items:
            type: string
        conversation_id:
          type: string
        ip:
          type: [string, "null"]
        status:
          type: integer
        latency_ms:
          type: integer
        created_at:
          type: string
          format: date-time
    SupportAuditListResponse:
      type: object
      required:
        - events
      properties:
        events:
          type: array
          items:
            $ref: "#/components/schemas/SupportAuditView"
    RetentionPromoRedemption:
      type: object
      additionalProperties: false
      required:
        - redemption_id
        - code
        - pack_id
        - pack_name
        - pack_type
        - player_ref
        - casino_player_ref
        - redeemed_at
        - observed_at
        - bonus_found
        - bonus_statuses
        - staff
        - test_player
      properties:
        redemption_id:
          type: string
          description: ID del canje (uso) en Centrivo; clave de idempotencia del consumidor.
        code:
          type:
            - string
            - "null"
          description: >-
            Texto del código ya canjeado, solo en packs multiple y external. Null en single y unknown (su
            código sigue vivo para otros jugadores) o si Centrivo no lo trajo.
        pack_id:
          type: string
        pack_name:
          type: string
          description: Nombre actual del pack en Centrivo.
        pack_type:
          type: string
          enum:
            - single
            - multiple
            - external
            - unknown
        player_ref:
          type: string
          description: external_player_ref de Centrivo (el ID que el sitio entrega al juego). Nunca null.
        casino_player_ref:
          type:
            - string
            - "null"
          description: External Player ID de Centrivo; null si el jugador todavía no está en data_players.
        redeemed_at:
          type:
            - string
            - "null"
          format: date-time
          description: Fecha del canje en Centrivo (el menor used_at de sus bonos); null si Centrivo no la trajo.
        observed_at:
          type: string
          format: date-time
          description: Primera vez que Convray vio el canje (6 decimales). Nunca cambia; ordena el feed.
        bonus_found:
          type: boolean
          description: true si al menos un bono del canje ya está en Data.
        bonus_statuses:
          type: array
          description: Estados distintos de los bonos entregados por el canje; lista vacía si no hay bono.
          items:
            type: string
            enum:
              - new
              - active
              - finished
              - canceled
              - expired
              - pending
              - paused
              - unknown
        staff:
          type: boolean
        test_player:
          type: boolean
    RetentionPromoRedemptionsResponse:
      type: object
      additionalProperties: false
      required:
        - items
        - next_cursor
        - has_more
        - promo_data_through
        - promo_backlog_packs
        - settle_seconds
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/RetentionPromoRedemption"
        next_cursor:
          type: string
          description: Cursor opaco; siempre viene. Se guarda después de procesar la página.
        has_more:
          type: boolean
        promo_data_through:
          type:
            - string
            - "null"
          format: date-time
          description: >-
            Todo canje de los packs vigentes del patrón hecho antes de este instante ya salió (o sale en esta
            página) del feed. Sin packs vigentes del patrón, la última lectura válida de la lista; null si
            nunca hubo una.
        promo_backlog_packs:
          type: integer
          minimum: 0
          description: Packs vigentes del patrón con usos pendientes de releer.
        settle_seconds:
          type: integer
    RetentionHealthResponse:
      type: object
      additionalProperties: false
      required:
        - api_version
        - server_time
        - data_through
        - promo_data_through
        - connector_paused
        - connector_paused_reason
        - settle_seconds
        - promo_backlog_packs
      properties:
        api_version:
          type: string
        server_time:
          type: string
          format: date-time
        data_through:
          type:
            - string
            - "null"
          format: date-time
          description: Todo depósito creado en Centrivo antes de este instante ya está en Convray (sin recargas ni barridos).
        promo_data_through:
          type:
            - string
            - "null"
          format: date-time
          description: Todo canje de un pack vigente hecho antes de este instante ya está en Convray.
        connector_paused:
          type: boolean
        connector_paused_reason:
          type:
            - string
            - "null"
          enum:
            - auth
            - forbidden
            - manual
            - other
            - null
        settle_seconds:
          type: integer
        promo_backlog_packs:
          type: integer
          minimum: 0
    RetentionPaidDeposit:
      type: object
      additionalProperties: false
      required:
        - deposit_ref
        - player_ref
        - casino_player_ref
        - amount_clp
        - currency
        - created_at
        - paid_observed_at
        - eligible
        - staff
        - test_player
      properties:
        deposit_ref:
          type: string
          description: ID del depósito en Centrivo; clave de idempotencia del consumidor.
        player_ref:
          type: string
          description: external_player_ref de Centrivo (el ID que el sitio entrega al juego).
        casino_player_ref:
          type:
            - string
            - "null"
          description: External Player ID de Centrivo; null si el jugador todavía no está en data_players.
        amount_clp:
          type:
            - integer
            - "null"
          description: Pesos chilenos enteros; null si currency no es CLP.
        currency:
          type: string
        created_at:
          type: string
          format: date-time
        paid_observed_at:
          type: string
          format: date-time
          description: Primera vez que Convray vio el paso a pagado (6 decimales). Nunca cambia.
        eligible:
          type:
            - boolean
            - "null"
          description: Sin autoexclusión ni bloqueo, al momento de la solicitud; null si el jugador no está en data_players.
        staff:
          type: boolean
        test_player:
          type: boolean
    RetentionPaidDepositsResponse:
      type: object
      additionalProperties: false
      required:
        - items
        - next_cursor
        - has_more
        - data_through
        - settle_seconds
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/RetentionPaidDeposit"
        next_cursor:
          type: string
          description: Cursor opaco; siempre viene. Se guarda después de procesar la página.
        has_more:
          type: boolean
        data_through:
          type:
            - string
            - "null"
          format: date-time
        settle_seconds:
          type: integer
    RetentionReversedDeposit:
      type: object
      additionalProperties: false
      required:
        - deposit_ref
        - player_ref
        - casino_player_ref
        - amount_clp
        - currency
        - created_at
        - paid_observed_at
        - eligible
        - staff
        - test_player
        - status
        - paid_reversed_at
      properties:
        deposit_ref:
          type: string
        player_ref:
          type: string
        casino_player_ref:
          type:
            - string
            - "null"
        amount_clp:
          type:
            - integer
            - "null"
        currency:
          type: string
        created_at:
          type: string
          format: date-time
        paid_observed_at:
          type:
            - string
            - "null"
          format: date-time
        eligible:
          type:
            - boolean
            - "null"
        staff:
          type: boolean
        test_player:
          type: boolean
        status:
          type: string
          description: Estado actual en Convray, sin normalizar (paid si el depósito volvió a pagado).
          enum:
            - initialized
            - pending
            - in_process
            - cancelled_by_operator
            - failed
            - declined
            - paid
        paid_reversed_at:
          type: string
          format: date-time
          description: La última vez que Convray vio que el depósito salió de pagado (6 decimales).
    RetentionReversedDepositsResponse:
      type: object
      additionalProperties: false
      required:
        - items
        - next_cursor
        - has_more
        - data_through
        - settle_seconds
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/RetentionReversedDeposit"
        next_cursor:
          type: string
          description: Cursor opaco de este feed; siempre viene. Se guarda después de procesar la página.
        has_more:
          type: boolean
        data_through:
          type:
            - string
            - "null"
          format: date-time
        settle_seconds:
          type: integer
    RetentionPlayerFlagsItem:
      type: object
      additionalProperties: false
      required:
        - player_ref
        - casino_player_ref
        - eligible
        - staff
        - test_player
      properties:
        player_ref:
          type: string
        casino_player_ref:
          type:
            - string
            - "null"
        eligible:
          type:
            - boolean
            - "null"
          description: null si el jugador todavía no está en la foto de jugadores ("todavía no se sabe").
        staff:
          type: boolean
        test_player:
          type: boolean
    RetentionPlayerFlagsResponse:
      type: object
      additionalProperties: false
      required:
        - items
        - unknown
      properties:
        items:
          type: array
          maxItems: 100
          items:
            $ref: "#/components/schemas/RetentionPlayerFlagsItem"
        unknown:
          type: integer
          minimum: 0
          description: Cantidad de refs distintos sin respuesta (sin depósitos de la campaña, inexistentes o de otro tenant).
    RetentionPlayerDeposit:
      type: object
      additionalProperties: false
      required:
        - deposit_ref
        - amount_clp
        - currency
        - status
        - created_at
        - paid_observed_at
        - paid_reversed_at
      properties:
        deposit_ref:
          type: string
        amount_clp:
          type:
            - integer
            - "null"
        currency:
          type: string
        status:
          type: string
          description: Estado actual en Convray, sin normalizar; distinto de paid solo si el depósito se revirtió.
          enum:
            - initialized
            - pending
            - in_process
            - cancelled_by_operator
            - failed
            - declined
            - paid
        created_at:
          type: string
          format: date-time
        paid_observed_at:
          type: string
          format: date-time
        paid_reversed_at:
          type:
            - string
            - "null"
          format: date-time
    RetentionPlayerDepositsResponse:
      type: object
      additionalProperties: false
      required:
        - player_ref
        - casino_player_ref
        - eligible
        - staff
        - test_player
        - from
        - to
        - data_through
        - truncated
        - deposits
      properties:
        player_ref:
          type: string
        casino_player_ref:
          type:
            - string
            - "null"
        eligible:
          type:
            - boolean
            - "null"
        staff:
          type: boolean
        test_player:
          type: boolean
        from:
          type: string
          format: date-time
          description: El from efectivo, max(from pedido, deposits_from del token).
        to:
          type: string
          format: date-time
        data_through:
          type:
            - string
            - "null"
          format: date-time
        truncated:
          type: boolean
        deposits:
          type: array
          maxItems: 500
          items:
            $ref: "#/components/schemas/RetentionPlayerDeposit"
    RetentionEmailCheckRequest:
      type: object
      additionalProperties: false
      required:
        - email_sha256
      properties:
        email_sha256:
          type: string
          pattern: "^[0-9a-f]{64}$"
          minLength: 64
          maxLength: 64
          description: hex(sha256(utf8(lower(trim(correo))))) en minúsculas, calculado por el consumidor.
    RetentionEmailCheckResponse:
      type: object
      additionalProperties: false
      required:
        - match
      properties:
        match:
          type: boolean
          description: true solo si el jugador existe en el tenant del token, tiene correo y el hash coincide.
    RetentionPromoPack:
      type: object
      additionalProperties: false
      required:
        - pack_id
        - pack_name
        - pack_type
        - codes_count
        - codes_synced
        - complete
        - used_count
        - first_seen_at
        - codes_synced_at
      properties:
        pack_id:
          type: string
        pack_name:
          type: string
          description: Nombre actual del pack en Centrivo.
        pack_type:
          type: string
          enum:
            - multiple
            - external
        codes_count:
          type: integer
          description: Códigos que declara Centrivo para el pack (codesCount).
        codes_synced:
          type: integer
          description: Códigos del pack que Convray tiene guardados.
        complete:
          type: boolean
          description: >-
            true si la última lectura de la lista del pack se hizo con el codes_count actual y trajo exactamente
            codes_count códigos distintos. Un pack incompleto se relee en la próxima corrida del conector.
        used_count:
          type: integer
          description: Canjes que informa Centrivo para el pack (usedCount).
        first_seen_at:
          type: string
          format: date-time
          description: Primera vez que Convray vio el pack.
        codes_synced_at:
          type:
            - string
            - "null"
          format: date-time
          description: Fin de la última lectura completa de sus códigos; null si nunca se leyó completa.
    RetentionPromoPacksResponse:
      type: object
      additionalProperties: false
      required:
        - items
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/RetentionPromoPack"
    RetentionPromoCode:
      type: object
      additionalProperties: false
      required:
        - code
        - pack_id
        - pack_name
        - first_seen_at
        - used
        - used_by_player_ref
        - used_at
      properties:
        code:
          type: string
          description: Texto del código tal como lo trae Centrivo (sin espacios al borde). Es un premio canjeable.
        pack_id:
          type: string
        pack_name:
          type: string
        first_seen_at:
          type: string
          format: date-time
          description: >-
            Primera vez que Convray vio el código (6 decimales); Centrivo no da fecha de creación por código.
            Ordena el feed.
        used:
          type: boolean
          description: true si Centrivo ya informó un canje de este código en su pack.
        used_by_player_ref:
          type:
            - string
            - "null"
          description: external_player_ref de Centrivo de quien lo canjeó (el primer canje); null si no se canjeó.
        used_at:
          type:
            - string
            - "null"
          format: date-time
          description: Fecha del canje en Centrivo; null si no se canjeó o si Centrivo no trajo fecha.
    RetentionPromoCodesResponse:
      type: object
      additionalProperties: false
      required:
        - items
        - next_cursor
        - has_more
        - settle_seconds
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/RetentionPromoCode"
        next_cursor:
          type: string
          description: Cursor opaco; siempre viene. Se guarda después de procesar la página.
        has_more:
          type: boolean
        settle_seconds:
          type: integer
    RetentionTokenCreateInput:
      type: object
      required:
        - integration
        - name
        - promo_pack_patterns
      properties:
        integration:
          type: string
          description: >-
            juego-octubre (depósitos, jugadores y canjes; scope retention.read), qa-retencion (pruebas de
            aceptación; scope retention.read) o juego-codigos (códigos de premio; scope retention.codes, que
            solo entra a promo/packs, promo/codes y health).
          enum:
            - juego-octubre
            - qa-retencion
            - juego-codigos
        name:
          type: string
          maxLength: 80
        ip_allowlist:
          type: array
          items:
            type: string
          description: >-
            CIDR o IP suelta; vacía no restringe. Cada rango debe ser /24 o más angosto en IPv4 y /48 o más
            angosto en IPv6 (0.0.0.0/0, ::/0 o un /16 responden 400 invalid-parameter), para toda
            integración: una lista ancha no cuenta como lista de IP. Sin lista, un token juego-octubre o
            juego-codigos vence a los 45 días como máximo.
        expires_in_days:
          type: integer
          minimum: 1
          maximum: 90
        promo_pack_patterns:
          type: array
          maxItems: 3
          items:
            type: string
          description: >-
            Obligatorio (puede ser [], salvo en juego-codigos, que exige al menos uno). Hasta 3 patrones de 3 a
            40 letras, dígitos o espacios; se guardan en minúsculas.
        deposits_from:
          type:
            - string
            - "null"
          format: date-time
          description: >-
            Obligatorio en juego-octubre y qa-retencion (null responde 400). Piso de fecha de los depósitos, en RFC 3339 con zona
            horaria (sin zona responde 400): los feeds de depósitos nunca entregan un depósito creado antes de
            este instante, aunque se pida un created_from menor o un since anterior. No cambia después de
            emitir; para otro piso se emite otro token. Para el juego, 2026-10-01T03:00:00Z (00:00 de Chile).
            En juego-codigos no aplica: se acepta ausente, null o con cualquier valor, se ignora y el token
            guarda el instante de emisión.
          example: "2026-10-01T03:00:00Z"
    RetentionToken:
      type: object
      required:
        - id
        - integration
        - name
        - prefix
        - scopes
        - promo_pack_patterns
        - ip_allowlist
        - deposits_from
        - expires_at
        - created_at
      properties:
        id:
          type: string
          format: uuid
        integration:
          type: string
        name:
          type: string
        prefix:
          type: string
        scopes:
          type: array
          items:
            type: string
        promo_pack_patterns:
          type: array
          items:
            type: string
        ip_allowlist:
          type: array
          items:
            type: string
        deposits_from:
          type: string
          format: date-time
          description: >-
            Piso de fecha de los depósitos del token, en UTC; fijo desde la emisión. En un token juego-codigos
            es el instante de emisión (no aplica: su scope no entra a depósitos).
        expires_at:
          type: string
          format: date-time
        last_used_at:
          type: string
          format: date-time
        revoked_at:
          type: string
          format: date-time
        created_at:
          type: string
          format: date-time
        created_by:
          $ref: "#/components/schemas/ApiIntegrationActor"
          description: Quien emitió el token.
        revoked_by:
          $ref: "#/components/schemas/ApiIntegrationActor"
          description: >-
            Quien revocó el token. Ausente en un token vivo y en las revocaciones anteriores a la versión
            0.64.0 (ese dato no se guardaba).
    RetentionIntegrationListResponse:
      type: object
      required:
        - integrations
      properties:
        integrations:
          type: array
          description: Siempre tres filas, en orden juego-octubre, juego-codigos y qa-retencion.
          items:
            $ref: "#/components/schemas/ApiIntegrationState"
    RetentionTokenCreated:
      type: object
      required:
        - token
        - retention_token
      properties:
        token:
          type: string
          description: Secreto en claro (cvj_...); se muestra una sola vez.
        retention_token:
          $ref: "#/components/schemas/RetentionToken"
    RetentionTokenListResponse:
      type: object
      required:
        - retention_tokens
      properties:
        retention_tokens:
          type: array
          items:
            $ref: "#/components/schemas/RetentionToken"
    RetentionAuditEvent:
      type: object
      additionalProperties: false
      required:
        - id
        - token_id
        - integration
        - endpoint
        - status
        - code
        - rows_returned
        - latency_ms
        - ip
        - position_from
        - position_to
        - player_ref_internal
        - created_at
      properties:
        id:
          type: string
          format: uuid
        token_id:
          type:
            - string
            - "null"
        integration:
          type: string
        endpoint:
          type: string
        status:
          type: integer
        code:
          type:
            - string
            - "null"
        rows_returned:
          type: integer
        latency_ms:
          type: integer
        ip:
          type:
            - string
            - "null"
        position_from:
          type:
            - string
            - "null"
          format: date-time
        position_to:
          type:
            - string
            - "null"
          format: date-time
        player_ref_internal:
          type:
            - string
            - "null"
        created_at:
          type: string
          format: date-time
    RetentionAuditListResponse:
      type: object
      required:
        - events
        - totals
      properties:
        events:
          type: array
          items:
            $ref: "#/components/schemas/RetentionAuditEvent"
        totals:
          type: object
          description: >-
            Conteos con los mismos filtros que la lista, sin el de resultado y sin el tope de filas
            (pestañas Todas / Correctas / Con error).
          required:
            - all
            - ok
            - error
          properties:
            all:
              type: integer
              minimum: 0
            ok:
              type: integer
              minimum: 0
            error:
              type: integer
              minimum: 0
    RetentionSummary:
      type: object
      required:
        - generated_at
        - time_zone
        - day
        - live_tokens
        - live_tokens_total
        - calls_today
        - calls_yesterday_same_time
        - errors_today
        - server_errors_today
        - latency_p95_ms_today
        - data_through
        - hourly_from
        - tokens
      properties:
        generated_at:
          type: string
          format: date-time
        time_zone:
          type: string
          const: America/Santiago
        day:
          type: string
          format: date
          description: Día civil de hoy en Chile.
        live_tokens:
          type: array
          description: Tokens vivos (no revocados ni vencidos) por integración.
          items:
            type: object
            required:
              - integration
              - count
            properties:
              integration:
                type: string
              count:
                type: integer
                minimum: 1
        live_tokens_total:
          type: integer
          minimum: 0
        calls_today:
          type: integer
          minimum: 0
          description: Llamadas auditadas desde hoy 00:00 (Chile) hasta ahora.
        calls_yesterday_same_time:
          type: integer
          minimum: 0
          description: Llamadas de ayer entre las 00:00 y la misma hora de pared que ahora (Chile).
        errors_today:
          type: integer
          minimum: 0
          description: Llamadas de hoy con status 400 o mayor.
        server_errors_today:
          type: integer
          minimum: 0
          description: Llamadas de hoy con status 500 o mayor.
        latency_p95_ms_today:
          type:
            - integer
            - "null"
          description: Percentil 95 de latency_ms de hoy; null sin llamadas.
        data_through:
          type:
            - string
            - "null"
          format: date-time
          description: Hasta cuándo están al día los depósitos (misma regla que /retention/health).
        hourly_from:
          type: string
          format: date-time
          description: Inicio de la primera de las 24 cubetas horarias (la última es la hora en curso).
        tokens:
          type: array
          items:
            type: object
            required:
              - token_id
              - last_used_at
              - calls_24h
              - hourly_calls
            properties:
              token_id:
                type: string
                format: uuid
              last_used_at:
                type:
                  - string
                  - "null"
                format: date-time
              calls_24h:
                type: integer
                minimum: 0
              hourly_calls:
                type: array
                minItems: 24
                maxItems: 24
                items:
                  type: integer
                  minimum: 0
    SupportAccountResponse:
      type: object
      required:
        - player_id
        - account_status
        - reason_code
        - freshness
      properties:
        player_id:
          type: string
        account_status:
          type: string
          enum:
            - active
            - restricted
            - restricted_withdrawals
            - self_excluded
            - inactive
        reason_code:
          type: string
        handoff_required:
          type: boolean
          description: Presente cuando exige derivar a un humano (autoexcluido o bloqueado).
        freshness:
          $ref: "#/components/schemas/SupportFreshness"
    SupportVerificationResponse:
      type: object
      required:
        - player_id
        - kyc_status
        - checks
        - freshness
      properties:
        player_id:
          type: string
        kyc_status:
          type: string
          enum:
            - verified
            - partial
            - pending
        checks:
          $ref: "#/components/schemas/SupportVerificationSet"
        freshness:
          $ref: "#/components/schemas/SupportFreshness"
    SupportVerificationSet:
      type: object
      required:
        - identity
        - email
        - mobile
        - address
        - payment
      properties:
        identity:
          type: boolean
        email:
          type: boolean
        mobile:
          type: boolean
        address:
          type: boolean
        payment:
          type: boolean
    SupportWithdrawalItem:
      type: object
      required:
        - transaction_id
        - amount_minor
        - currency
        - requested_at
        - status
        - typical_payout_minutes
      properties:
        transaction_id:
          type: string
        amount_minor:
          type: integer
          format: int64
        currency:
          type: string
        requested_at:
          type: string
          format: date-time
        status:
          type: string
          enum:
            - pending
            - in_review
            - approved
            - paid
            - rejected
        reason_code:
          type: string
          description: Motivo genérico, presente solo en retiros rechazados.
        typical_payout_minutes:
          type: [integer, "null"]
          format: int64
          description: Promedio real del tenant redondeado; null si no es confiable.
    SupportWithdrawalsResponse:
      type: object
      required:
        - player_id
        - withdrawals
        - freshness
      properties:
        player_id:
          type: string
        withdrawals:
          type: array
          items:
            $ref: "#/components/schemas/SupportWithdrawalItem"
        handoff_recommended:
          type: boolean
          description: Presente cuando el dataset está desactualizado y conviene derivar a un humano.
        freshness:
          $ref: "#/components/schemas/SupportFreshness"
    SupportDepositItem:
      type: object
      required:
        - transaction_id
        - amount_minor
        - currency
        - created_at
        - status
        - method_type
      properties:
        transaction_id:
          type: string
        amount_minor:
          type: integer
          format: int64
        currency:
          type: string
        created_at:
          type: string
          format: date-time
        status:
          type: string
          enum:
            - pending
            - completed
            - failed
        failure_code:
          type: string
          description: Motivo genérico, presente solo en depósitos fallidos.
        method_type:
          type: string
    SupportDepositsResponse:
      type: object
      required:
        - player_id
        - deposits
        - freshness
      properties:
        player_id:
          type: string
        deposits:
          type: array
          items:
            $ref: "#/components/schemas/SupportDepositItem"
        handoff_recommended:
          type: boolean
          description: Presente cuando el dataset está desactualizado y conviene derivar a un humano.
        freshness:
          $ref: "#/components/schemas/SupportFreshness"
    SupportFreshness:
      type: object
      required:
        - dataset
        - data_as_of
        - coverage
        - next_expected_load_at
        - stale
        - assistant_hint
      properties:
        dataset:
          type: string
        data_as_of:
          type: [string, "null"]
          format: date-time
        coverage:
          type: string
          enum:
            - complete
            - partial
        next_expected_load_at:
          type: [string, "null"]
          format: date-time
        stale:
          type: boolean
        assistant_hint:
          type: string
    CrmMailSettingsResponse:
      type: object
      required:
        - settings
      properties:
        settings:
          $ref: "#/components/schemas/CrmMailSettings"
    CrmMailSettings:
      type: object
      required:
        - configured
        - smtp_host
        - smtp_port
        - smtp_username
        - from_name
        - from_email
        - reply_to
        - physical_address
        - use_tls
        - has_password
        - updated_at
      properties:
        configured:
          type: boolean
        smtp_host:
          type: string
        smtp_port:
          type: integer
        smtp_username:
          type: string
        from_name:
          type: string
        from_email:
          type: string
        reply_to:
          type: string
        physical_address:
          type: string
        use_tls:
          type: boolean
        has_password:
          type: boolean
        updated_at:
          type: [string, "null"]
          format: date-time
    CrmMailSettingsRequest:
      type: object
      properties:
        smtp_host:
          type: string
        smtp_port:
          type: integer
        smtp_username:
          type: string
        smtp_password:
          type: string
        from_name:
          type: string
        from_email:
          type: string
        reply_to:
          type: string
        physical_address:
          type: string
        use_tls:
          type: boolean
      additionalProperties: false
    CrmTestMailRequest:
      type: object
      required:
        - to
      properties:
        to:
          type: string
          format: email
      additionalProperties: false
    CrmTestMailResponse:
      type: object
      required:
        - sent
        - message_id
        - to
        - error
      properties:
        sent:
          type: boolean
        message_id:
          type: string
        to:
          type: string
          description: Dirección de destino enmascarada.
        error:
          type: [string, "null"]
    CrmImportResponse:
      type: object
      required:
        - batch
      properties:
        batch:
          $ref: "#/components/schemas/CrmImportBatch"
    CrmImportListResponse:
      type: object
      required:
        - batches
      properties:
        batches:
          type: array
          items:
            $ref: "#/components/schemas/CrmImportBatch"
    CrmImportBatch:
      type: object
      required:
        - id
        - dataset
        - file_name
        - file_sha256
        - status
        - rows_read
        - rows_accepted
        - rows_rejected
        - rows_upserted
        - error
        - uploaded_by_user_id
        - uploaded_by_email
        - created_at
        - finished_at
        - rejects
      properties:
        id:
          type: string
        dataset:
          type: string
        file_name:
          type: string
        file_sha256:
          type: [string, "null"]
          description: SHA-256 del archivo en hexadecimal.
        status:
          type: string
        rows_read:
          type: integer
        rows_accepted:
          type: integer
        rows_rejected:
          type: integer
        rows_upserted:
          type: integer
        error:
          type: [string, "null"]
        uploaded_by_user_id:
          type: [string, "null"]
        uploaded_by_email:
          type: [string, "null"]
        created_at:
          type: string
          format: date-time
        finished_at:
          type: [string, "null"]
          format: date-time
        rejects:
          type: array
          items:
            $ref: "#/components/schemas/CrmRejectEntry"
    CrmRejectEntry:
      type: object
      required:
        - line
        - reason
      properties:
        line:
          type: integer
        reason:
          type: string
    CrmContactListResponse:
      type: object
      required:
        - contacts
        - total
        - page
        - page_size
      properties:
        contacts:
          type: array
          items:
            $ref: "#/components/schemas/CrmContact"
        total:
          type: integer
          format: int64
        page:
          type: integer
        page_size:
          type: integer
    CrmContactResponse:
      type: object
      required:
        - contact
      properties:
        contact:
          $ref: "#/components/schemas/CrmContact"
    CrmContact:
      type: object
      required:
        - id
        - external_player_ref
        - email
        - first_name
        - last_name
        - country
        - registered_at
        - sport_status
        - consent
        - consent_source
        - subscribed
        - self_excluded
        - activity_state
        - first_deposit_at
        - last_deposit_at
        - deposits_count
        - deposits_total_minor
        - deposits_90d_minor
        - created_at
      properties:
        id:
          type: string
        external_player_ref:
          type: string
        email:
          type: string
          description: Enmascarado salvo para administradores.
        first_name:
          type: string
        last_name:
          type: string
        country:
          type: [string, "null"]
        registered_at:
          type: [string, "null"]
          format: date-time
        sport_status:
          type: [string, "null"]
        consent:
          type: boolean
        consent_source:
          type: [string, "null"]
        subscribed:
          type: boolean
        self_excluded:
          type: boolean
        activity_state:
          type: string
        first_deposit_at:
          type: [string, "null"]
          format: date-time
        last_deposit_at:
          type: [string, "null"]
          format: date-time
        deposits_count:
          type: integer
          format: int64
        deposits_total_minor:
          type: integer
          format: int64
        deposits_90d_minor:
          type: integer
          format: int64
        created_at:
          type: string
          format: date-time
    CrmRecomputeResponse:
      type: object
      required:
        - recomputed
      properties:
        recomputed:
          type: boolean
    CrmSummary:
      type: object
      required:
        - contacts
        - with_consent
        - subscribed
        - suppressed
        - by_state
        - last_recompute_at
      properties:
        contacts:
          type: integer
          format: int64
        with_consent:
          type: integer
          format: int64
        subscribed:
          type: integer
          format: int64
        suppressed:
          type: integer
          format: int64
        by_state:
          type: object
          additionalProperties:
            type: integer
            format: int64
        last_recompute_at:
          type: [string, "null"]
          format: date-time
    CrmNexorSettingsResponse:
      type: object
      required:
        - settings
      properties:
        settings:
          $ref: "#/components/schemas/CrmNexorSettings"
    CrmNexorSettings:
      type: object
      required:
        - enabled
        - workflow_id
        - webhook_id
        - conversion_type_id
        - returned_since
        - contacted_status_keys
        - last_error
        - last_error_at
        - last_exclusion_sweep_at
        - last_webhook_at
        - exclusion_sweep_stale
        - updated_at
      properties:
        enabled:
          type: boolean
        workflow_id:
          type: [string, "null"]
        webhook_id:
          type: [string, "null"]
        conversion_type_id:
          type: [string, "null"]
        returned_since:
          type: [string, "null"]
          format: date
        contacted_status_keys:
          type: array
          items:
            type: string
        last_error:
          type: [string, "null"]
        last_error_at:
          type: [string, "null"]
          format: date-time
        last_exclusion_sweep_at:
          type: [string, "null"]
          format: date-time
        last_webhook_at:
          type: [string, "null"]
          format: date-time
        exclusion_sweep_stale:
          type: boolean
        updated_at:
          type: [string, "null"]
          format: date-time
    CrmNexorSettingsRequest:
      type: object
      properties:
        enabled:
          type: boolean
        workflow_id:
          type: string
        webhook_id:
          type: string
        conversion_type_id:
          type: string
        returned_since:
          type: string
          format: date
        contacted_status_keys:
          type: array
          items:
            type: string
      additionalProperties: false
    CrmSegmentRule:
      type: object
      required:
        - field
        - op
        - value
      properties:
        field:
          type: string
          description: >-
            Field de la lista blanca (activity_state, last_deposit_days,
            deposits_total_minor, deposits_count, sport_status, country, consent,
            subscribed, registered_days).
        op:
          type: string
          enum:
            - eq
            - neq
            - gte
            - lte
            - in
        value:
          description: >-
            Valor interpretado según el field (número, texto, booleano; o lista para 'in').
    CrmSegment:
      type: object
      required:
        - id
        - name
        - rules
        - created_at
        - updated_at
      properties:
        id:
          type: string
        name:
          type: string
        rules:
          type: array
          items:
            $ref: "#/components/schemas/CrmSegmentRule"
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    CrmSegmentListResponse:
      type: object
      required:
        - segments
      properties:
        segments:
          type: array
          items:
            $ref: "#/components/schemas/CrmSegment"
    CrmSegmentResponse:
      type: object
      required:
        - segment
      properties:
        segment:
          $ref: "#/components/schemas/CrmSegment"
    CrmSegmentRequest:
      type: object
      required:
        - name
        - rules
      properties:
        name:
          type: string
        rules:
          type: array
          items:
            $ref: "#/components/schemas/CrmSegmentRule"
      additionalProperties: false
    CrmSegmentPatchRequest:
      type: object
      properties:
        name:
          type: string
        rules:
          type: array
          items:
            $ref: "#/components/schemas/CrmSegmentRule"
      additionalProperties: false
    CrmPolicyExclusions:
      type: object
      required:
        - no_consent
        - unsubscribed
        - suppressed
        - self_excluded
      properties:
        no_consent:
          type: integer
          format: int64
        unsubscribed:
          type: integer
          format: int64
        suppressed:
          type: integer
          format: int64
        self_excluded:
          type: integer
          format: int64
    CrmSegmentPreview:
      type: object
      required:
        - count
        - excluded_by_policy
        - sample
      properties:
        count:
          type: integer
          format: int64
        excluded_by_policy:
          $ref: "#/components/schemas/CrmPolicyExclusions"
        sample:
          type: array
          items:
            type: string
    CrmTemplate:
      type: object
      required:
        - id
        - name
        - subject
        - html
        - text
        - variables
        - created_at
        - updated_at
      properties:
        id:
          type: string
        name:
          type: string
        subject:
          type: string
        html:
          type: string
        text:
          type: string
        variables:
          type: array
          items:
            type: string
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    CrmTemplateListResponse:
      type: object
      required:
        - templates
      properties:
        templates:
          type: array
          items:
            $ref: "#/components/schemas/CrmTemplate"
    CrmTemplateResponse:
      type: object
      required:
        - template
      properties:
        template:
          $ref: "#/components/schemas/CrmTemplate"
    CrmTemplateRequest:
      type: object
      required:
        - name
        - subject
        - html
      properties:
        name:
          type: string
        subject:
          type: string
        html:
          type: string
        text:
          type: string
        variables:
          type: array
          items:
            type: string
      additionalProperties: false
    CrmTemplatePatchRequest:
      type: object
      properties:
        name:
          type: string
        subject:
          type: string
        html:
          type: string
        text:
          type: string
        variables:
          type: array
          items:
            type: string
      additionalProperties: false
    CrmCompareRow:
      type: object
      required:
        - campaign_id
        - name
        - metrics
      properties:
        campaign_id:
          type: string
        name:
          type: string
        metrics:
          type: object
          description: >-
            Métricas extendidas de la campaña (mismo objeto que el reporte extendido de
            /crm/campaigns/{id}/report; ver el módulo de campañas del CRM).
          additionalProperties: true
    CrmImpact:
      type: object
      description: Impacto agregado del CRM en un período (GET /crm/impact). Tasas en fracción 0..1; lift en puntos porcentuales.
      required:
        - period
        - channel
        - campaigns
        - truncated
        - funnel
        - rates
        - send
        - control
        - depositors_exclusive
        - incremental
        - by_campaign
        - opens_are_approximate
        - min_population
        - last_computed_at
        - notes
      properties:
        period:
          type: object
          required:
            - from
            - to
          properties:
            from:
              type: string
              format: date
            to:
              type: string
              format: date
        channel:
          type: string
          enum:
            - all
            - email
            - voice_nexor
        campaigns:
          type: integer
          format: int64
        truncated:
          type: boolean
          description: true si el período tenía más de 200 campañas (se suman las 200 más recientes).
        funnel:
          $ref: "#/components/schemas/CrmImpactFunnel"
        rates:
          $ref: "#/components/schemas/CrmImpactRates"
        send:
          $ref: "#/components/schemas/CrmGroupResult"
        control:
          $ref: "#/components/schemas/CrmGroupResult"
        depositors_exclusive:
          type: integer
          format: int64
          description: Depositantes por último contacto entre campañas solapadas (sin doble conteo).
        incremental:
          $ref: "#/components/schemas/CrmImpactIncremental"
        by_campaign:
          type: array
          items:
            $ref: "#/components/schemas/CrmImpactCampaignRow"
        opens_are_approximate:
          type: boolean
        min_population:
          type: integer
        last_computed_at:
          type:
            - string
            - "null"
          format: date-time
        notes:
          type: array
          items:
            type: string
    CrmImpactFunnel:
      type: object
      required:
        - audience
        - excluded
        - contacted
        - control
        - sent
        - delivered
        - bounced
        - opened
        - clicked
        - unsubscribed
      properties:
        audience:
          type: integer
          format: int64
        excluded:
          type: integer
          format: int64
        contacted:
          type: integer
          format: int64
        control:
          type: integer
          format: int64
        sent:
          type: integer
          format: int64
        delivered:
          type: integer
          format: int64
        bounced:
          type: integer
          format: int64
        opened:
          type: integer
          format: int64
        clicked:
          type: integer
          format: int64
        unsubscribed:
          type: integer
          format: int64
    CrmImpactRates:
      type: object
      required:
        - open_rate
        - click_rate
        - click_to_open
      properties:
        open_rate:
          type:
            - number
            - "null"
        click_rate:
          type:
            - number
            - "null"
        click_to_open:
          type:
            - number
            - "null"
    CrmImpactIncremental:
      type: object
      description: Incremental contra el grupo de control, solo con campañas que lo tienen.
      required:
        - comparable_campaigns
        - send_total
        - control_total
        - send_deposit_rate
        - control_deposit_rate
        - deposit_pp
        - deposit_relative_pct
        - depositors
        - deposits_amount_minor
        - ggr_minor
      properties:
        comparable_campaigns:
          type: integer
          format: int64
        send_total:
          type: integer
          format: int64
        control_total:
          type: integer
          format: int64
        send_deposit_rate:
          type: number
        control_deposit_rate:
          type: number
        deposit_pp:
          type:
            - number
            - "null"
        deposit_relative_pct:
          type:
            - number
            - "null"
        depositors:
          type:
            - integer
            - "null"
          format: int64
        deposits_amount_minor:
          type:
            - integer
            - "null"
          format: int64
        ggr_minor:
          type:
            - integer
            - "null"
          format: int64
    CrmImpactCampaignRow:
      type: object
      required:
        - campaign_id
        - name
        - channel
        - status
        - started_at
        - contacted
        - opened
        - clicked
        - depositors
        - deposit_rate
        - deposit_pp
        - incremental_depositors
        - deposits_amount_minor
      properties:
        campaign_id:
          type: string
        name:
          type: string
        channel:
          type: string
        status:
          type: string
        started_at:
          type: string
        contacted:
          type: integer
          format: int64
        opened:
          type: integer
          format: int64
        clicked:
          type: integer
          format: int64
        depositors:
          type: integer
          format: int64
        deposit_rate:
          type: number
        deposit_pp:
          type:
            - number
            - "null"
        incremental_depositors:
          type:
            - integer
            - "null"
          format: int64
        deposits_amount_minor:
          type:
            - integer
            - "null"
          format: int64
    CrmCompareResult:
      type: object
      required:
        - subjects
        - opens_are_approximate
        - notes
      properties:
        subjects:
          type: array
          items:
            $ref: "#/components/schemas/CrmCompareRow"
        opens_are_approximate:
          type: boolean
        notes:
          type: array
          items:
            type: string
    CrmCampaign:
      type: object
      description: Campaña de CRM tal como la devuelve la API.
      required:
        - id
        - name
        - status
        - throttle_per_minute
        - holdout_pct
        - attribution_window_days
        - channel
        - created_at
        - updated_at
      properties:
        id:
          type: string
        name:
          type: string
        segment_id:
          type: [string, "null"]
        template_id:
          type: [string, "null"]
        status:
          type: string
          description: Estado de la campaña (p. ej. draft, scheduled, running, paused, canceled, done).
        scheduled_at:
          type: [string, "null"]
          format: date-time
        throttle_per_minute:
          type: integer
        holdout_pct:
          type: integer
        attribution_window_days:
          type: integer
        segment_snapshot_count:
          type: [integer, "null"]
        control_count:
          type: [integer, "null"]
        started_at:
          type: [string, "null"]
          format: date-time
        finished_at:
          type: [string, "null"]
          format: date-time
        audience_kind:
          type: [string, "null"]
          description: crm_segment | data_segment. Vacío usa el segmento legado (segment_id).
        audience_ref:
          type: [string, "null"]
        bonus_cost_minor:
          type: [integer, "null"]
          format: int64
        audience_no_contact_count:
          type: [integer, "null"]
        audience_discards:
          type: object
          additionalProperties:
            type: integer
          description: >-
            Desglose por motivo de descartes del congelado voice_nexor
            (sin_snapshot, descifrado_fallido, conflicto). Vacío si no hubo
            descartes o la campaña no se congeló.
        channel:
          type: string
          description: email (histórico) | voice_nexor (espejo Nexor).
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    CrmCampaignEnvelope:
      type: object
      required:
        - campaign
      properties:
        campaign:
          $ref: "#/components/schemas/CrmCampaign"
    CrmCampaignListResponse:
      type: object
      required:
        - campaigns
      properties:
        campaigns:
          type: array
          items:
            $ref: "#/components/schemas/CrmCampaign"
    CrmCampaignCreateRequest:
      type: object
      description: Cuerpo de creación de campaña. scheduled_at se acepta pero no programa.
      required:
        - name
      properties:
        name:
          type: string
        segment_id:
          type: [string, "null"]
        template_id:
          type: [string, "null"]
        throttle_per_minute:
          type: integer
        holdout_pct:
          type: integer
        attribution_window_days:
          type: integer
        audience_kind:
          type: [string, "null"]
        audience_ref:
          type: [string, "null"]
        bonus_cost_minor:
          type: [integer, "null"]
          format: int64
        channel:
          type: string
          description: Vacío = email. voice_nexor exige audiencia data_segment.
        scheduled_at:
          type: string
          description: Se acepta pero se ignora al crear (la campaña nace en draft).
      additionalProperties: false
    CrmCampaignPatchRequest:
      type: object
      description: PATCH parcial de campaña en borrador; solo los campos presentes se aplican.
      properties:
        name:
          type: string
        segment_id:
          type: [string, "null"]
        template_id:
          type: [string, "null"]
        throttle_per_minute:
          type: integer
        holdout_pct:
          type: integer
        attribution_window_days:
          type: integer
        audience_kind:
          type: [string, "null"]
        audience_ref:
          type: [string, "null"]
        bonus_cost_minor:
          type: [integer, "null"]
          format: int64
        channel:
          type: string
      additionalProperties: false
    CrmCampaignScheduleRequest:
      type: object
      description: Programación de una campaña. scheduled_at ISO 8601; vacío envía cuanto antes.
      properties:
        scheduled_at:
          type: string
          format: date-time
      additionalProperties: false
    CrmAudienceCheck:
      type: object
      description: Dry-run de contactabilidad de la audiencia (data_segment) de una campaña.
      required:
        - campaign_id
        - total
        - contactable
        - autoexcluido
        - bloqueado
        - sin_consentimiento
        - sin_contacto
        - require_consent
      properties:
        campaign_id:
          type: string
        total:
          type: integer
          format: int64
        contactable:
          type: integer
          format: int64
        autoexcluido:
          type: integer
          format: int64
        bloqueado:
          type: integer
          format: int64
        sin_consentimiento:
          type: integer
          format: int64
        sin_contacto:
          type: integer
          format: int64
        require_consent:
          type: boolean
    CrmRecipient:
      type: object
      required:
        - external_player_ref
        - email
        - group
        - status
        - excluded_reason
      properties:
        external_player_ref:
          type: string
        email:
          type: string
          description: Enmascarado salvo para administradores.
        group:
          type: string
          description: send | control.
        status:
          type: string
        excluded_reason:
          type: string
    CrmCampaignRecipientsResponse:
      type: object
      required:
        - recipients
      properties:
        recipients:
          type: array
          items:
            $ref: "#/components/schemas/CrmRecipient"
    CrmFunnel:
      type: object
      required:
        - recipients
        - sent
        - delivered
        - bounced
        - opened
        - clicked
        - unsubscribed
        - suppressed
        - excluded
      properties:
        recipients:
          type: integer
          format: int64
        sent:
          type: integer
          format: int64
        delivered:
          type: integer
          format: int64
        bounced:
          type: integer
          format: int64
        opened:
          type: integer
          format: int64
        clicked:
          type: integer
          format: int64
        unsubscribed:
          type: integer
          format: int64
        suppressed:
          type: integer
          format: int64
        excluded:
          type: integer
          format: int64
    CrmGroupConversion:
      type: object
      required:
        - converted
        - total
        - rate
        - deposits_amount_minor
        - first_deposits
      properties:
        converted:
          type: integer
          format: int64
        total:
          type: integer
          format: int64
        rate:
          type: number
        deposits_amount_minor:
          type: integer
          format: int64
        first_deposits:
          type: integer
          format: int64
    CrmConversions:
      type: object
      required:
        - send
        - control
      properties:
        send:
          $ref: "#/components/schemas/CrmGroupConversion"
        control:
          $ref: "#/components/schemas/CrmGroupConversion"
    CrmTouchCount:
      type: object
      required:
        - touch
        - converted
      properties:
        touch:
          type: string
        converted:
          type: integer
          format: int64
    CrmGroupResult:
      type: object
      description: Resultados ampliados de un grupo (send o control) en la ventana configurada. Montos nil bajo el umbral de población.
      required:
        - total
        - depositors
        - deposit_rate
        - bettors
        - bet_rate
        - first_deposits
      properties:
        total:
          type: integer
          format: int64
        depositors:
          type: integer
          format: int64
        deposit_rate:
          type: number
        bettors:
          type: integer
          format: int64
        bet_rate:
          type: number
        first_deposits:
          type: integer
          format: int64
        deposits_amount_minor:
          type: [integer, "null"]
          format: int64
        bets_casino_amount_minor:
          type: [integer, "null"]
          format: int64
        bets_sport_amount_minor:
          type: [integer, "null"]
          format: int64
        ggr_minor:
          type: [integer, "null"]
          format: int64
    CrmWindowGroup:
      type: object
      required:
        - depositors
        - deposit_rate
        - bettors
        - bet_rate
      properties:
        depositors:
          type: integer
          format: int64
        deposit_rate:
          type: number
        bettors:
          type: integer
          format: int64
        bet_rate:
          type: number
        deposits_amount_minor:
          type: [integer, "null"]
          format: int64
        ggr_minor:
          type: [integer, "null"]
          format: int64
    CrmWindowRead:
      type: object
      required:
        - window_days
        - send
        - control
      properties:
        window_days:
          type: integer
        send:
          $ref: "#/components/schemas/CrmWindowGroup"
        control:
          $ref: "#/components/schemas/CrmWindowGroup"
    CrmLiftResult:
      type: object
      required:
        - deposit_pp
        - bet_pp
      properties:
        deposit_pp:
          type: number
        deposit_relative_pct:
          type: [number, "null"]
        bet_pp:
          type: number
        bet_relative_pct:
          type: [number, "null"]
    CrmExtendedReport:
      type: object
      description: KPIs ampliados de la campaña (apuestas/GGR, ventanas, lift, costo de bonos y retorno).
      required:
        - audience
        - excluded_by_policy
        - attribution_window_days
        - attribution_anchor
        - send
        - control
        - lift
        - windows
        - depositors_exclusive_send
        - return_reason
        - low_population
        - opens_are_approximate
        - min_population
        - notes
      properties:
        audience:
          type: integer
          format: int64
        excluded_by_policy:
          type: object
          additionalProperties:
            type: integer
            format: int64
        attribution_window_days:
          type: integer
        attribution_anchor:
          type: string
        send:
          $ref: "#/components/schemas/CrmGroupResult"
        control:
          $ref: "#/components/schemas/CrmGroupResult"
        lift:
          $ref: "#/components/schemas/CrmLiftResult"
        windows:
          type: array
          items:
            $ref: "#/components/schemas/CrmWindowRead"
        depositors_exclusive_send:
          type: integer
          format: int64
        bonus_cost_minor:
          type: [integer, "null"]
          format: int64
        return_minor:
          type: [integer, "null"]
          format: int64
        return_reason:
          type: string
        low_population:
          type: boolean
        opens_are_approximate:
          type: boolean
        min_population:
          type: integer
        notes:
          type: array
          items:
            type: string
    CrmVoiceFunnel:
      type: object
      description: Embudo del espejo Nexor de una campaña voice_nexor (acumulado).
      required:
        - recipients
        - pending_push
        - pushed
        - attempted
        - contacted
        - reactivated
        - lost
        - push_failed
        - stopped
      properties:
        recipients:
          type: integer
          format: int64
        pending_push:
          type: integer
          format: int64
        pushed:
          type: integer
          format: int64
        attempted:
          type: integer
          format: int64
        contacted:
          type: integer
          format: int64
        reactivated:
          type: integer
          format: int64
        lost:
          type: integer
          format: int64
        push_failed:
          type: integer
          format: int64
        stopped:
          type: integer
          format: int64
    CrmCampaignReport:
      type: object
      required:
        - campaign
        - funnel
        - conversions
        - uplift_pct
        - by_touch
      properties:
        campaign:
          $ref: "#/components/schemas/CrmCampaign"
        funnel:
          $ref: "#/components/schemas/CrmFunnel"
        conversions:
          $ref: "#/components/schemas/CrmConversions"
        uplift_pct:
          type: number
        by_touch:
          type: array
          items:
            $ref: "#/components/schemas/CrmTouchCount"
        computed_at:
          type: [string, "null"]
          format: date-time
        extended:
          $ref: "#/components/schemas/CrmExtendedReport"
        voice_funnel:
          $ref: "#/components/schemas/CrmVoiceFunnel"
    CrmAudienceFilters:
      type: object
      description: Objeto de filtros de players (mismo contrato que /data/segments). Estructura libre validada por el servicio.
      additionalProperties: true
    CrmAudience:
      type: object
      description: data_segment visto como audiencia del CRM, con sus conteos cacheados.
      required:
        - id
        - name
        - description
        - dataset
        - kind
        - filters
        - shared
        - created_by
        - created_at
        - updated_at
      properties:
        id:
          type: string
        name:
          type: string
        description:
          type: string
        dataset:
          type: string
        kind:
          type: string
        filters:
          $ref: "#/components/schemas/CrmAudienceFilters"
        shared:
          type: boolean
        created_by:
          type: string
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        player_count:
          type: [integer, "null"]
          format: int64
        contactable_count:
          type: [integer, "null"]
          format: int64
        non_contactable_count:
          type: [integer, "null"]
          format: int64
        last_evaluated_at:
          type: [string, "null"]
          format: date-time
    CrmAudienceListResponse:
      type: object
      required:
        - audiences
      properties:
        audiences:
          type: array
          items:
            $ref: "#/components/schemas/CrmAudience"
    CrmAudienceCreateRequest:
      type: object
      required:
        - name
      properties:
        name:
          type: string
        description:
          type: string
        shared:
          type: boolean
        filters:
          $ref: "#/components/schemas/CrmAudienceFilters"
      additionalProperties: false
    CrmAudienceImportRequest:
      type: object
      description: Cuerpo JSON del import estático (lista de Player IDs).
      required:
        - name
        - player_ids
      properties:
        name:
          type: string
        description:
          type: string
        shared:
          type: boolean
        id_kind:
          type: string
          description: external (ID Centrivo, default) | casino (ID de Juégalo).
          enum:
            - external
            - casino
        limit:
          type: integer
          description: Recorta a las primeras N filas antes de deduplicar (0/omitido = todas).
        player_ids:
          type: array
          items:
            type: string
      additionalProperties: false
    CrmAudienceImportMultipart:
      type: object
      description: Cuerpo multipart del import estático con CSV de Player IDs.
      required:
        - file
      properties:
        file:
          type: string
          format: binary
          description: CSV con los Player IDs.
        name:
          type: string
        description:
          type: string
        shared:
          type: string
          description: Bandera booleana en texto (true/false, 1/0).
        id_kind:
          type: string
          enum:
            - external
            - casino
        limit:
          type: integer
    CrmAudienceImportCounts:
      type: object
      required:
        - rows_read
        - valid
        - duplicate
        - unknown
        - excluded
      properties:
        rows_read:
          type: integer
        valid:
          type: integer
        duplicate:
          type: integer
        unknown:
          type: integer
        excluded:
          type: integer
    CrmAudienceImportResult:
      type: object
      required:
        - segment
        - import
        - file_name
        - captured_at
      properties:
        segment:
          $ref: "#/components/schemas/CrmAudience"
        import:
          $ref: "#/components/schemas/CrmAudienceImportCounts"
        file_name:
          type: string
        captured_at:
          type: string
          format: date-time
    CrmAudiencePreview:
      type: object
      description: Preview de una audiencia (conteo y agregados del rango). Nunca la lista de jugadores.
      required:
        - segment_id
        - from
        - to
        - player_count
        - self_excluded
        - blocked
        - contactable
        - deposits_minor
        - net_cash_minor
        - ggr_no_bonus_minor
      properties:
        segment_id:
          type: string
        from:
          type: string
        to:
          type: string
        player_count:
          type: integer
          format: int64
        self_excluded:
          type: integer
          format: int64
        blocked:
          type: integer
          format: int64
        contactable:
          type: integer
          format: int64
        deposits_minor:
          type: integer
          format: int64
        net_cash_minor:
          type: integer
          format: int64
        ggr_no_bonus_minor:
          type: integer
          format: int64
    CrmAudienceUpload:
      type: object
      description: Entrada del historial de cargas de un segmento estático (auditoría).
      required:
        - id
        - file_name
        - file_sha256
        - uploaded_by
        - uploaded_at
        - rows_read
        - rows_valid
        - rows_unknown
        - rows_duplicate
        - rows_excluded
      properties:
        id:
          type: string
        file_name:
          type: string
        file_sha256:
          type: string
        uploaded_by:
          type: string
        uploaded_at:
          type: string
          format: date-time
        rows_read:
          type: integer
          format: int64
        rows_valid:
          type: integer
          format: int64
        rows_unknown:
          type: integer
          format: int64
        rows_duplicate:
          type: integer
          format: int64
        rows_excluded:
          type: integer
          format: int64
    CrmAudienceUploadsResponse:
      type: object
      required:
        - uploads
      properties:
        uploads:
          type: array
          items:
            $ref: "#/components/schemas/CrmAudienceUpload"
    CrmFilterOption:
      type: object
      required:
        - value
        - label
        - count
      properties:
        value:
          type: string
        label:
          type: string
        count:
          type: integer
          format: int64
    CrmFilterOptionsDimension:
      type: object
      required:
        - dimension
        - values
      properties:
        dimension:
          type: string
        values:
          type: array
          items:
            $ref: "#/components/schemas/CrmFilterOption"
    CrmFilterOptionsResponse:
      type: object
      required:
        - dimensions
      properties:
        dimensions:
          type: array
          items:
            $ref: "#/components/schemas/CrmFilterOptionsDimension"
    CrmOpportunityRecommendation:
      type: object
      description: Recomendación operativa de un playbook del detector de oportunidades.
      required:
        - offer_kind
        - channel
        - primary_kpi
        - secondary_kpi
        - control_pct
        - attribution_window_days
        - timing
        - exclusions
        - risk
      properties:
        offer_kind:
          type: string
        channel:
          type: string
        primary_kpi:
          type: string
        secondary_kpi:
          type: string
        control_pct:
          type: integer
        attribution_window_days:
          type: integer
        timing:
          type: string
        exclusions:
          type: string
        risk:
          type: string
    CrmOpportunityTrend:
      type: object
      required:
        - compared_to
        - previous_players
        - delta_players
      properties:
        compared_to:
          type: string
        previous_players:
          type: integer
          format: int64
        delta_players:
          type: integer
          format: int64
        delta_pct:
          type: [number, "null"]
    CrmOpportunityVariant:
      type: object
      required:
        - code
        - label
        - filters
        - players
        - contactable
        - non_contactable
        - deposits_paid_amount_minor
      properties:
        code:
          type: string
        label:
          type: string
        filters:
          $ref: "#/components/schemas/CrmAudienceFilters"
        players:
          type: integer
          format: int64
        contactable:
          type: integer
          format: int64
        non_contactable:
          type: integer
          format: int64
        deposits_paid_amount_minor:
          type: integer
          format: int64
        trend:
          $ref: "#/components/schemas/CrmOpportunityTrend"
    CrmOpportunityView:
      type: object
      required:
        - code
        - name
        - description
        - available
        - segmentable
        - source
        - recommendation
      properties:
        code:
          type: string
        name:
          type: string
        description:
          type: string
        available:
          type: boolean
        unavailable_reason:
          type: string
        segmentable:
          type: boolean
        source:
          type: string
        activity_basis:
          type: string
        recommendation:
          $ref: "#/components/schemas/CrmOpportunityRecommendation"
        variants:
          type: array
          items:
            $ref: "#/components/schemas/CrmOpportunityVariant"
        risk_count:
          type: [integer, "null"]
          format: int64
    CrmOpportunitiesResponse:
      type: object
      required:
        - day
        - playbooks
      properties:
        day:
          type: string
        playbooks:
          type: array
          items:
            $ref: "#/components/schemas/CrmOpportunityView"
    CrmOpportunitySegmentRequest:
      type: object
      description: Cuerpo de creación de segmento desde una oportunidad. variant vacío = default.
      properties:
        variant:
          type: string
        name:
          type: string
        description:
          type: string
        shared:
          type: boolean
      additionalProperties: false
    CrmOpportunitySegmentResult:
      type: object
      required:
        - segment
        - playbook_code
        - variant_code
        - audit_id
      properties:
        segment:
          $ref: "#/components/schemas/CrmAudience"
        playbook_code:
          type: string
        variant_code:
          type: string
        audit_id:
          type: string
    PartnerRef:
      type: object
      description: Identidad de un partner para mostrar (slug y nombre), nunca el tenant id crudo.
      properties:
        tenant_slug:
          type: string
        display_name:
          type: string
    CasinoPartnerMetric:
      type: object
      description: >-
        Fila de métricas agregadas de un partner para una ventana. Los montos van
        en unidad menor según el exponente ISO de source_currency. Los campos
        anulables son NULL cuando el pipeline no tiene ese dato (no 0).
      properties:
        id:
          type: string
        partner:
          $ref: "#/components/schemas/PartnerRef"
        export_kind:
          type: string
        period_month:
          type: string
          description: Mes en YYYY-MM.
        period_from:
          type: string
          format: date-time
        period_to:
          type: string
          format: date-time
        source_currency:
          type: string
        players_registered:
          type: integer
          format: int64
        ftd_count:
          type: integer
          format: int64
        ftd_amount_minor:
          type: integer
          format: int64
        deposit_count:
          type: integer
          format: int64
        deposit_amount_minor:
          type: integer
          format: int64
        ngr_amount_minor:
          type: integer
          format: int64
        cpa_amount_minor:
          type: integer
          format: int64
        total_earnings_minor:
          type: integer
          format: int64
        ggr_amount_minor:
          type: [integer, "null"]
          format: int64
        bet_amount_minor:
          type: [integer, "null"]
          format: int64
        bet_count:
          type: [integer, "null"]
          format: int64
        withdraw_amount_minor:
          type: [integer, "null"]
          format: int64
        withdraw_count:
          type: [integer, "null"]
          format: int64
        source_as_of:
          type: string
          format: date-time
    PartnerIngestionStatus:
      type: object
      description: Estado de ingestión curado de un partner (cobertura, última corrida y huecos).
      properties:
        partner:
          $ref: "#/components/schemas/PartnerRef"
        coverage_from:
          type: [string, "null"]
          format: date-time
        coverage_to:
          type: [string, "null"]
          format: date-time
        last_ingested_at:
          type: [string, "null"]
          format: date-time
        has_gaps:
          type: boolean
        gap_count:
          type: integer
        last_reason_code:
          type: [string, "null"]
    CasinoPartnerMetricsPage:
      type: object
      description: Respuesta de GET /casino/partners/metrics.
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/CasinoPartnerMetric"
        next_cursor:
          type: [string, "null"]
        limit:
          type: integer
        ingestion:
          type: array
          items:
            $ref: "#/components/schemas/PartnerIngestionStatus"
    PlayerMetricRow:
      type: object
      description: >-
        Hecho agregado de un jugador de un partner sobre el rango pedido. Campos
        anulables NULL cuando ninguna ventana sumada traía la columna fuente.
      properties:
        external_player_ref:
          type: string
        external_casino_player_ref:
          type: [string, "null"]
        external_casino_id:
          type: [string, "null"]
        site_username:
          type: [string, "null"]
        btag:
          type: [string, "null"]
        player_country:
          type: [string, "null"]
        registration_at:
          type: [string, "null"]
          format: date-time
        qualified_at:
          type: [string, "null"]
          format: date-time
        source_currency:
          type: string
        deposit_count:
          type: integer
          format: int64
        deposit_amount_minor:
          type: integer
          format: int64
        ftd_amount_minor:
          type: integer
          format: int64
        net_deposit_amount_minor:
          type: integer
          format: int64
        ngr_amount_minor:
          type: integer
          format: int64
        withdraw_count:
          type: [integer, "null"]
          format: int64
        withdraw_amount_minor:
          type: [integer, "null"]
          format: int64
        ggr_amount_minor:
          type: [integer, "null"]
          format: int64
        bet_amount_minor:
          type: [integer, "null"]
          format: int64
        bet_count:
          type: [integer, "null"]
          format: int64
        cpa_amount_minor:
          type: [integer, "null"]
          format: int64
        total_revshare_amount_minor:
          type: [integer, "null"]
          format: int64
        active_days:
          type: integer
        last_active_day:
          type: [string, "null"]
        ngr_last7_minor:
          type: [integer, "null"]
          format: int64
        ngr_prev7_minor:
          type: [integer, "null"]
          format: int64
        windows_included:
          type: integer
        windows_discarded:
          type: integer
    PlayerMetricsPage:
      type: object
      description: Respuesta de GET /casino/partners/players/metrics.
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/PlayerMetricRow"
        total_count:
          type: integer
        from:
          type: string
          description: YYYY-MM-DD, eco del filtro aplicado.
        to:
          type: string
          description: YYYY-MM-DD, eco del filtro aplicado.
    PartnerCommunityDailyPoint:
      type: object
      description: Punto de la serie diaria de comunidad (días sin actividad con ceros explícitos).
      properties:
        date:
          type: string
        clicks:
          type: integer
          format: int64
        new_members:
          type: integer
          format: int64
        departed_members:
          type: integer
          format: int64
    PartnerCommunitySection:
      type: object
      description: Sección de métricas de comunidad de un partner.
      properties:
        linked:
          type: boolean
        clicks:
          type: integer
          format: int64
        active_players:
          type: integer
          format: int64
        active_members:
          type: integer
          format: int64
        new_members:
          type: integer
          format: int64
        departed_members:
          type: integer
          format: int64
        daily:
          type: array
          items:
            $ref: "#/components/schemas/PartnerCommunityDailyPoint"
    PartnerCommunityMetrics:
      type: object
      description: Fila del payload de comunidad (un partner con sus secciones).
      properties:
        partner:
          $ref: "#/components/schemas/PartnerRef"
        community:
          $ref: "#/components/schemas/PartnerCommunitySection"
    PartnerCommunityPage:
      type: object
      description: Respuesta de GET /casino/partners/community-metrics.
      properties:
        window_days:
          type: integer
        partners:
          type: array
          items:
            $ref: "#/components/schemas/PartnerCommunityMetrics"
    PartnerSiteAnalytics:
      type: object
      description: Rollup web del Streamer Site de los partners gestionados del casino.
      properties:
        period:
          $ref: "#/components/schemas/PartnerSiteAnalyticsPeriod"
        web:
          $ref: "#/components/schemas/PartnerSiteWebAnalytics"
        daily:
          type: array
          items:
            $ref: "#/components/schemas/PartnerSiteDailyAnalytics"
        by_partner:
          type: array
          items:
            $ref: "#/components/schemas/PartnerSitePartnerAnalytics"
    PartnerSiteAnalyticsPeriod:
      type: object
      properties:
        key:
          type: string
        start_at:
          type: string
          format: date-time
        end_at:
          type: string
          format: date-time
    PartnerSiteWebAnalytics:
      type: object
      description: Agregado total sobre todos los partners gestionados. CTR/UniqueCTR null si el denominador es 0.
      properties:
        impressions:
          type: integer
          format: int64
        unique_visitors:
          type: integer
          format: int64
        clicks:
          type: integer
          format: int64
        unique_clicks:
          type: integer
          format: int64
        ctr:
          type: [number, "null"]
        unique_ctr:
          type: [number, "null"]
        by_surface:
          type: array
          items:
            $ref: "#/components/schemas/PartnerSiteSurfaceAnalytics"
    PartnerSiteSurfaceAnalytics:
      type: object
      properties:
        surface:
          type: string
        impressions:
          type: integer
          format: int64
        clicks:
          type: integer
          format: int64
    PartnerSiteDailyAnalytics:
      type: object
      description: Serie diaria total con ceros explícitos.
      properties:
        date:
          type: string
        impressions:
          type: integer
          format: int64
        unique_visitors:
          type: integer
          format: int64
        clicks:
          type: integer
          format: int64
    PartnerSitePartnerAnalytics:
      type: object
      description: Drilldown por sitio (una fila por streamer gestionado). CTR/UniqueCTR null si el denominador es 0.
      properties:
        partner_tenant_id:
          type: string
        slug:
          type: string
        display_name:
          type: string
        impressions:
          type: integer
          format: int64
        unique_visitors:
          type: integer
          format: int64
        clicks:
          type: integer
          format: int64
        unique_clicks:
          type: integer
          format: int64
        ctr:
          type: [number, "null"]
        unique_ctr:
          type: [number, "null"]
    PartnerBtag:
      type: object
      description: Mapeo BTAG a partner (vigente o cerrado).
      properties:
        id:
          type: string
        btag:
          type: string
        match_kind:
          type: string
          description: exact o prefix.
        source:
          type: string
        valid_from:
          type: string
          format: date-time
        valid_until:
          type: [string, "null"]
          format: date-time
        created_at:
          type: string
          format: date-time
        players_count:
          type: integer
          format: int64
    PartnerBtagList:
      type: object
      description: Respuesta de GET /casino/partners/{partnerSlug}/btags.
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/PartnerBtag"
    CreatePartnerBtagRequest:
      type: object
      description: Cuerpo de POST /casino/partners/{partnerSlug}/btags.
      required:
        - btag
        - match_kind
      properties:
        btag:
          type: string
        match_kind:
          type: string
          description: exact (texto exacto) o prefix (prefijo de afiliado).
      additionalProperties: false
    UnmappedBtagGroup:
      type: object
      description: Grupo de BTAG sin mapear agrupados por prefijo/código. Solo agregados, sin PII.
      properties:
        prefix:
          type: string
        players_count:
          type: integer
          format: int64
        registered_in_range:
          type: integer
          format: int64
        depositors_count:
          type: integer
          format: int64
        variants_count:
          type: integer
          format: int64
        affiliate_id:
          type: [integer, "null"]
          format: int64
        affiliate_username:
          type: [string, "null"]
        sample_variants:
          type: array
          items:
            type: string
        deposits_paid_amount_minor:
          type: integer
          format: int64
    UnmappedBtagGroupList:
      type: object
      description: Respuesta de GET /casino/partners/btags/unmapped.
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/UnmappedBtagGroup"
    PartnerDataMetricsTotals:
      type: object
      description: Totales agregados del rango desde Data.
      properties:
        registered:
          type: integer
          format: int64
        ftd:
          type: integer
          format: int64
        depositors:
          type: integer
          format: int64
        deposit_count:
          type: integer
          format: int64
        deposit_amount_minor:
          type: integer
          format: int64
        withdraw_count:
          type: integer
          format: int64
        withdraw_amount_minor:
          type: integer
          format: int64
        net_cash_minor:
          type: integer
          format: int64
        casino_bets_count:
          type: integer
          format: int64
        casino_bet_amount_minor:
          type: integer
          format: int64
        casino_ggr_minor:
          type: integer
          format: int64
        sport_bets_count:
          type: integer
          format: int64
        sport_bet_amount_minor:
          type: integer
          format: int64
        sport_ggr_minor:
          type: integer
          format: int64
        ggr_minor:
          type: integer
          format: int64
        bet_amount_minor:
          type: integer
          format: int64
        bonus_bet_amount_minor:
          type: integer
          format: int64
        ngr_minor:
          type: integer
          format: int64
    PartnerDataMetricsSeriesPoint:
      type: object
      description: Un día de la serie diaria desde Data.
      properties:
        day:
          type: string
        registered:
          type: integer
          format: int64
        ftd:
          type: integer
          format: int64
        depositors:
          type: integer
          format: int64
        deposit_amount_minor:
          type: integer
          format: int64
        withdraw_amount_minor:
          type: integer
          format: int64
        ggr_minor:
          type: integer
          format: int64
        bet_amount_minor:
          type: integer
          format: int64
        bonus_bet_amount_minor:
          type: integer
          format: int64
    PartnerDataMetricsByBtag:
      type: object
      description: Fila del desglose por código.
      properties:
        btag:
          type: string
        registered:
          type: integer
          format: int64
        ftd:
          type: integer
          format: int64
        depositors:
          type: integer
          format: int64
        deposit_amount_minor:
          type: integer
          format: int64
        ggr_minor:
          type: integer
          format: int64
        bonus_bet_amount_minor:
          type: integer
          format: int64
    PartnerRankingDelta:
      type: object
      description: Diferencia (rango actual − período anterior de igual largo) de métricas clave.
      properties:
        registered:
          type: integer
          format: int64
        ftd:
          type: integer
          format: int64
        ggr_minor:
          type: integer
          format: int64
        ngr_minor:
          type: integer
          format: int64
    PartnerDataMetricsRanking:
      type: object
      description: Fila del ranking del casino (por partner o filas especiales).
      properties:
        partner_slug:
          type: [string, "null"]
        partner_name:
          type: string
        totals:
          $ref: "#/components/schemas/PartnerDataMetricsTotals"
        conversion_ftd:
          type: [number, "null"]
        active_bettors:
          type: [integer, "null"]
          format: int64
        active_depositors:
          type: [integer, "null"]
          format: int64
        delta:
          $ref: "#/components/schemas/PartnerRankingDelta"
    CasinoPartnerDataMetricsRange:
      type: object
      description: Rango de días civiles del reporte.
      properties:
        from:
          type: string
        to:
          type: string
    CasinoPartnerDataMetricsSources:
      type: object
      description: Declara desde cuándo hay cada dato.
      properties:
        deposits_from:
          type: string
        bets_from:
          type: string
    PartnerDataMetrics:
      type: object
      description: Respuesta de GET /casino/partners/data-metrics.
      properties:
        currency:
          type: string
        range:
          $ref: "#/components/schemas/CasinoPartnerDataMetricsRange"
        sources:
          $ref: "#/components/schemas/CasinoPartnerDataMetricsSources"
        totals:
          $ref: "#/components/schemas/PartnerDataMetricsTotals"
        series:
          type: array
          items:
            $ref: "#/components/schemas/PartnerDataMetricsSeriesPoint"
        by_btag:
          type: array
          items:
            $ref: "#/components/schemas/PartnerDataMetricsByBtag"
        partners:
          type: array
          items:
            $ref: "#/components/schemas/PartnerDataMetricsRanking"
    RecalculatePartnerDataMetricsRequest:
      type: object
      description: Cuerpo opcional de POST /casino/partners/data-metrics/recalculate.
      properties:
        days:
          type: integer
          description: Días a recalcular (1 a 400, default 400).
      additionalProperties: false
    PartnerDataRecalcResult:
      type: object
      description: >-
        Respuesta del recálculo manual. En "queued" solo viaja el status; en "done"
        viajan también days y duration_ms.
      properties:
        status:
          type: string
          enum:
            - done
            - queued
        days:
          type: integer
        duration_ms:
          type: integer
          format: int64
    ReconciliationMetric:
      type: object
      description: Comparación portal vs Data de una métrica (diff = data - portal).
      properties:
        portal:
          type: integer
          format: int64
        data:
          type: integer
          format: int64
        diff:
          type: integer
          format: int64
        diff_pct:
          type: [number, "null"]
    ReconciliationItem:
      type: object
      description: Conciliación de un partner en el mes.
      properties:
        partner_slug:
          type: string
        partner_name:
          type: string
        registered:
          $ref: "#/components/schemas/ReconciliationMetric"
        ftd:
          $ref: "#/components/schemas/ReconciliationMetric"
        deposit_amount_minor:
          $ref: "#/components/schemas/ReconciliationMetric"
        ggr_minor:
          $ref: "#/components/schemas/ReconciliationMetric"
    PartnerReconciliation:
      type: object
      description: Respuesta de GET /casino/partners/reconciliation.
      properties:
        month:
          type: string
        items:
          type: array
          items:
            $ref: "#/components/schemas/ReconciliationItem"
    NGRReconciliationItem:
      type: object
      description: Conciliación de NGR de un partner en un mes.
      properties:
        month:
          type: string
          description: YYYY-MM.
        partner_slug:
          type: string
        partner_name:
          type: string
        ngr_data_minor:
          type: integer
          format: int64
        ngr_portal_minor:
          type: integer
          format: int64
        ngr_diff_minor:
          type: integer
          format: int64
        ngr_diff_pct:
          type: [number, "null"]
        ggr_data_minor:
          type: integer
          format: int64
        ggr_portal_minor:
          type: integer
          format: int64
        data_source:
          type: string
          enum:
            - mensual
            - diario
            - sin_datos
        covered_days:
          type: integer
        days_in_month:
          type: integer
        portal_present:
          type: boolean
        verdict:
          type: string
          enum:
            - calza
            - revisar
            - sin_datos
    NGRReconciliationSummary:
      type: object
      description: Resumen global del rango de la conciliación de NGR.
      properties:
        match:
          type: integer
        review:
          type: integer
        no_data:
          type: integer
        ngr_data_minor:
          type: integer
          format: int64
        ngr_portal_minor:
          type: integer
          format: int64
        ngr_diff_minor:
          type: integer
          format: int64
        ngr_diff_pct:
          type: [number, "null"]
    PartnerNGRReconciliation:
      type: object
      description: Respuesta de GET /casino/partners/ngr-reconciliation.
      properties:
        from:
          type: string
        to:
          type: string
        currency:
          type: string
        items:
          type: array
          items:
            $ref: "#/components/schemas/NGRReconciliationItem"
        summary:
          $ref: "#/components/schemas/NGRReconciliationSummary"
    PartnerPerformanceTotals:
      description: >-
        Totales del rollup por partner extendidos con activos distintos del rango y
        la conversión registro→FTD.
      allOf:
        - $ref: "#/components/schemas/PartnerDataMetricsTotals"
        - type: object
          properties:
            active_bettors:
              type: integer
              format: int64
            active_depositors:
              type: integer
              format: int64
            conversion_ftd:
              type: [number, "null"]
    PartnerPerformance:
      type: object
      description: Respuesta de GET /casino/partners/{partnerSlug}/performance.
      properties:
        currency:
          type: string
        partner_slug:
          type: string
        partner_name:
          type: string
        range:
          $ref: "#/components/schemas/CasinoPartnerDataMetricsRange"
        previous_range:
          $ref: "#/components/schemas/CasinoPartnerDataMetricsRange"
        sources:
          $ref: "#/components/schemas/CasinoPartnerDataMetricsSources"
        totals:
          $ref: "#/components/schemas/PartnerPerformanceTotals"
        previous:
          $ref: "#/components/schemas/PartnerPerformanceTotals"
        series:
          type: array
          items:
            $ref: "#/components/schemas/PartnerDataMetricsSeriesPoint"
        unmapped_hint:
          type: integer
          format: int64
    PlayerResponsibleFlags:
      type: object
      description: Banderas de juego responsable / estado de cuenta del jugador.
      properties:
        self_excluded:
          type: boolean
        blocked:
          type: boolean
        inactive:
          type: boolean
        attention_marked:
          type: boolean
        blacklisted:
          type: boolean
    CasinoPlayerPII:
      type: object
      description: Campos sensibles del jugador. Solo se llenan con permiso de PII del casino.
      properties:
        username:
          type: string
        first_name:
          type: string
        last_name:
          type: string
        email:
          type: string
        mobile:
          type: string
    PartnerPlayerDepositMark:
      type: object
      description: Marca del FTD (día y monto). Nulo si el jugador nunca depositó.
      properties:
        day:
          type: string
        amount_minor:
          type: integer
          format: int64
    PartnerPlayerRow:
      type: object
      description: >-
        Jugador de un partner visto por el casino. También es la respuesta de la
        ficha (GET .../players/{ref}/profile). PII solo con permiso.
      properties:
        player_id:
          type: string
          description: external_player_ref.
        casino_player_ref:
          type: [string, "null"]
        btag:
          type: string
        btag_prefix:
          type: string
        registered_at:
          type: string
        account_status:
          type: string
        flags:
          $ref: "#/components/schemas/PlayerResponsibleFlags"
        ftd:
          oneOf:
            - $ref: "#/components/schemas/PartnerPlayerDepositMark"
            - type: "null"
        range_deposits_count:
          type: integer
          format: int64
        range_deposits_amount_minor:
          type: integer
          format: int64
        range_withdrawals_count:
          type: integer
          format: int64
        range_withdraw_amount_minor:
          type: integer
          format: int64
        range_net_cash_minor:
          type: integer
          format: int64
        range_casino_bet_amount_minor:
          type: integer
          format: int64
        range_sport_bet_amount_minor:
          type: integer
          format: int64
        range_ggr_minor:
          type: integer
          format: int64
        deposits_count:
          type: integer
          format: int64
        deposits_amount_minor:
          type: integer
          format: int64
        withdrawals_count:
          type: integer
          format: int64
        withdraw_amount_minor:
          type: integer
          format: int64
        net_cash_minor:
          type: integer
          format: int64
        casino_bet_amount_minor:
          type: integer
          format: int64
        sport_bet_amount_minor:
          type: integer
          format: int64
        ggr_minor:
          type: integer
          format: int64
        ngr_minor:
          type: integer
          format: int64
        ltv_minor:
          type: integer
          format: int64
        last_deposit_day:
          type: [string, "null"]
        last_bet_day:
          type: [string, "null"]
        last_activity_day:
          type: [string, "null"]
        inactive_days:
          type: [integer, "null"]
          format: int64
        pii:
          oneOf:
            - $ref: "#/components/schemas/CasinoPlayerPII"
            - type: "null"
    PartnerPlayersPage:
      type: object
      description: Respuesta de GET /casino/partners/{partnerSlug}/players.
      properties:
        currency:
          type: string
        partner_slug:
          type: string
        partner_name:
          type: string
        range:
          $ref: "#/components/schemas/CasinoPartnerDataMetricsRange"
        segment:
          type: string
        sort:
          type: string
        direction:
          type: string
        page:
          type: integer
        page_size:
          type: integer
        total:
          type: integer
        pii_visible:
          type: boolean
        items:
          type: array
          items:
            $ref: "#/components/schemas/PartnerPlayerRow"
    PartnerPlayerActivityRow:
      type: object
      description: Movimiento mezclado del jugador. Ref de depósito/retiro enmascarado sin permiso.
      properties:
        kind:
          type: string
          description: registration, deposit, withdrawal, sport_bet o casino.
        at:
          type: string
          format: date-time
        ref:
          type: string
        label:
          type: string
        status:
          type: string
        amount_minor:
          type: integer
          format: int64
    PartnerPlayerActivityPage:
      type: object
      description: Respuesta paginada de .../profile/activity.
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/PartnerPlayerActivityRow"
        total:
          type: integer
          format: int64
    PartnerPlayerDepositRow:
      type: object
      description: Transacción de depósito del jugador.
      properties:
        ref:
          type: string
        created_at:
          type: string
          format: date-time
        status:
          type: string
        payment_method:
          type: string
        device_type:
          type: string
        amount_minor:
          type: integer
          format: int64
    PartnerPlayerDepositsPage:
      type: object
      description: Respuesta paginada de .../profile/deposits.
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/PartnerPlayerDepositRow"
        total:
          type: integer
          format: int64
    PartnerPlayerBetRow:
      type: object
      description: Apuesta deportiva fila por fila del jugador.
      properties:
        ref:
          type: string
        created_at:
          type: string
          format: date-time
        bet_type:
          type: string
        sport_bet_type:
          type: string
        state:
          type: string
        odd:
          type: [number, "null"]
        cash_out:
          type: boolean
        bonus_bet:
          type: boolean
        bet_amount_minor:
          type: integer
          format: int64
        win_amount_minor:
          type: integer
          format: int64
        ggr_minor:
          type: integer
          format: int64
    PartnerPlayerBetsPage:
      type: object
      description: Respuesta paginada de .../profile/bets.
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/PartnerPlayerBetRow"
        total:
          type: integer
          format: int64
    CampaignImage:
      type: object
      description: Imagen referenciada por la campaña de la landing de jugadores.
      required:
        - src
      properties:
        src:
          type: string
          description: Ruta relativa ('/...') o URL https absoluta.
        alt:
          type: string
        w:
          type: integer
        h:
          type: integer
    Campaign:
      type: object
      description: Contrato v1 del contenido editable de la landing de jugadores.
      properties:
        logo:
          $ref: "#/components/schemas/CampaignImage"
        hero:
          $ref: "#/components/schemas/CampaignImage"
        character:
          $ref: "#/components/schemas/CampaignImage"
        sideLeft:
          $ref: "#/components/schemas/CampaignImage"
        sideRight:
          $ref: "#/components/schemas/CampaignImage"
        banner:
          $ref: "#/components/schemas/CampaignImage"
        cards:
          type: array
          items:
            $ref: "#/components/schemas/CampaignImage"
        text:
          type: object
          description: Copy de la campaña como mapa clave→texto.
          additionalProperties:
            type: string
        colors:
          type: object
          description: Paleta de la campaña como mapa clave→#RRGGBB.
          additionalProperties:
            type: string
    PlayerLanding:
      type: object
      description: Borrador de la landing de jugadores de un casino.
      properties:
        id:
          type: string
        slug:
          type: string
        display_name:
          type: string
        campaign:
          $ref: "#/components/schemas/Campaign"
        register_url:
          type: string
        publication_status:
          type: string
        has_unpublished_changes:
          type: boolean
        version:
          type: integer
          format: int64
        published_at:
          type: [string, "null"]
          format: date-time
        updated_at:
          type: string
          format: date-time
    UpdatePlayerLandingRequest:
      type: object
      description: >-
        Cuerpo de PATCH /casino/player-landing/settings. Cada campo nil deja el
        valor intacto; campaign no-nil reemplaza la campaña completa.
      required:
        - expected_version
      properties:
        display_name:
          type: [string, "null"]
        campaign:
          $ref: "#/components/schemas/Campaign"
        register_url:
          type: [string, "null"]
        publication_status:
          type: [string, "null"]
        expected_version:
          type: integer
          format: int64
          description: Candado optimista. 0 crea la landing si no existe.
      additionalProperties: false
    PublicPlayerLanding:
      type: object
      description: Lectura pública del snapshot publicado de la landing de jugadores.
      properties:
        casino_slug:
          type: string
        display_name:
          type: string
        campaign:
          $ref: "#/components/schemas/Campaign"
        register_url:
          type: string
        published_at:
          type: string
          format: date-time
    PlayerLandingEventRequest:
      type: object
      description: Cuerpo del beacon público de la landing de jugadores.
      properties:
        event_id:
          type: string
          format: uuid
        event_type:
          type: string
          description: page_view o click.
        surface:
          type: string
        target_type:
          type: [string, "null"]
        target_key:
          type: [string, "null"]
      additionalProperties: false
    PlayerLandingEventReceipt:
      type: object
      description: >-
        Confirmación del ingreso idempotente del evento. replayed=true cuando el
        mismo event_id ya estaba persistido con idéntico contenido.
      properties:
        accepted:
          type: boolean
        replayed:
          type: boolean
    PlayerLandingAnalytics:
      type: object
      description: Reporte recortado de la landing de jugadores (solo web).
      properties:
        period:
          $ref: "#/components/schemas/PlayerLandingAnalyticsPeriod"
        web:
          $ref: "#/components/schemas/PlayerLandingWebAnalytics"
        daily:
          type: array
          items:
            $ref: "#/components/schemas/PlayerLandingDailyAnalytics"
        top_interactions:
          type: array
          items:
            $ref: "#/components/schemas/PlayerLandingInteractionAnalytics"
    PlayerLandingAnalyticsPeriod:
      type: object
      properties:
        key:
          type: string
        start_at:
          type: string
          format: date-time
        end_at:
          type: string
          format: date-time
    PlayerLandingWebAnalytics:
      type: object
      description: Agregado web de la landing. CTR/UniqueCTR null si el denominador es 0.
      properties:
        impressions:
          type: integer
          format: int64
        unique_visitors:
          type: integer
          format: int64
        clicks:
          type: integer
          format: int64
        unique_clicks:
          type: integer
          format: int64
        ctr:
          type: [number, "null"]
        unique_ctr:
          type: [number, "null"]
        by_surface:
          type: array
          items:
            $ref: "#/components/schemas/PlayerLandingSurfaceAnalytics"
    PlayerLandingSurfaceAnalytics:
      type: object
      properties:
        surface:
          type: string
        impressions:
          type: integer
          format: int64
        clicks:
          type: integer
          format: int64
    PlayerLandingDailyAnalytics:
      type: object
      description: Serie diaria de la landing con ceros explícitos.
      properties:
        date:
          type: string
        impressions:
          type: integer
          format: int64
        unique_visitors:
          type: integer
          format: int64
        clicks:
          type: integer
          format: int64
    PlayerLandingInteractionAnalytics:
      type: object
      properties:
        target_type:
          type: string
        target_key:
          type: string
        clicks:
          type: integer
          format: int64
        unique_clicks:
          type: integer
          format: int64
    PlatformTenant:
      type: object
      description: Casino con contadores para el módulo Personas y roles del super_admin.
      properties:
        slug:
          type: string
        display_name:
          type: string
        kind:
          type: string
        members_active:
          type: integer
          format: int64
        invitations_pending:
          type: integer
          format: int64
        members_without_role:
          type: integer
          format: int64
    PlatformTenantList:
      type: object
      description: Respuesta de GET /platform/tenants.
      properties:
        tenants:
          type: array
          items:
            $ref: "#/components/schemas/PlatformTenant"
    CompanyDomainList:
      type: object
      required:
        - domains
      properties:
        domains:
          type: array
          items:
            type: string
    CasinoReplaceCompanyDomainsRequest:
      type: object
      required:
        - domains
      properties:
        domains:
          type: array
          items:
            type: string
      additionalProperties: false
    CompanyDomainReplacement:
      type: object
      required:
        - domains
        - affected_emails
      properties:
        domains:
          type: array
          items:
            type: string
        affected_emails:
          type: array
          items:
            type: string
    TeamProductGrant:
      type: object
      description: Acceso por producto {product, role, area}. area es null cuando el producto no la exige.
      required:
        - product
        - role
        - area
      properties:
        product:
          type: string
        role:
          type: string
        area:
          type: [string, "null"]
    TeamArea:
      type: object
      required:
        - code
        - label
        - sort_order
      properties:
        code:
          type: string
        label:
          type: string
        sort_order:
          type: integer
    TeamAreaList:
      type: object
      required:
        - areas
      properties:
        areas:
          type: array
          items:
            $ref: "#/components/schemas/TeamArea"
    TeamRole:
      type: object
      required:
        - code
        - label
        - grants
        - is_system
        - sort_order
      properties:
        code:
          type: string
        label:
          type: string
        grants:
          type: array
          items:
            $ref: "#/components/schemas/TeamProductGrant"
        is_system:
          type: boolean
        sort_order:
          type: integer
    TeamRoleList:
      type: object
      required:
        - roles
      properties:
        roles:
          type: array
          items:
            $ref: "#/components/schemas/TeamRole"
    TeamMember:
      type: object
      required:
        - membership_id
        - email
        - role
        - status
        - version
        - created_at
        - joined_at
        - products
        - job_title
        - role_code
        - company_account
      properties:
        membership_id:
          type: string
        email:
          type: string
        role:
          type: string
        status:
          type: string
        version:
          type: integer
          format: int64
        created_at:
          type: string
          format: date-time
        joined_at:
          type: string
          format: date-time
        products:
          type: array
          items:
            $ref: "#/components/schemas/TeamProductGrant"
        job_title:
          type: [string, "null"]
        role_code:
          type: [string, "null"]
        company_account:
          type: boolean
    TeamInvitation:
      type: object
      required:
        - id
        - email
        - role
        - status
        - expires_at
        - version
        - created_at
        - products
        - role_code
        - company_account
      properties:
        id:
          type: string
        email:
          type: string
        role:
          type: string
        status:
          type: string
        expires_at:
          type: string
          format: date-time
        version:
          type: integer
          format: int64
        created_at:
          type: string
          format: date-time
        token:
          type: string
          description: Token de invitación en claro. Solo presente al crear o regenerar el enlace.
        products:
          type: array
          items:
            $ref: "#/components/schemas/TeamProductGrant"
        role_code:
          type: [string, "null"]
        company_account:
          type: boolean
        email_sent:
          type: boolean
          description: Solo al crear o regenerar. true si la invitación se envió por correo.
    CasinoRegenerateInvitationLinkRequest:
      type: object
      description: Body opcional. Si trae expires_at, extiende el vencimiento de la invitación.
      properties:
        expires_at:
          type: [string, "null"]
          format: date-time
      additionalProperties: false
    CasinoReplaceMemberProductsRequest:
      type: object
      required:
        - products
      properties:
        products:
          type: array
          items:
            $ref: "#/components/schemas/TeamProductGrant"
      additionalProperties: false
    CasinoAssignMemberRoleRequest:
      type: object
      properties:
        role_code:
          type: [string, "null"]
          description: Código del rol de empresa a aplicar. null quita el rol y vacía los accesos.
      additionalProperties: false
    CommunityProgram:
      type: object
      required:
        - id
        - name
        - symbol
        - status
        - version
        - updated_at
      properties:
        id:
          type: string
        name:
          type: string
        symbol:
          type: string
        status:
          type: string
        version:
          type: integer
          format: int64
        updated_at:
          type: string
          format: date-time
    CommunityAccessSettings:
      type: object
      required:
        - kick_enabled
        - discord_enabled
        - version
        - updated_at
      properties:
        kick_enabled:
          type: boolean
        discord_enabled:
          type: boolean
        version:
          type: integer
          format: int64
        updated_at:
          type: string
          format: date-time
    CommunityOverlaySettings:
      type: object
      required:
        - enabled_event_types
        - version
        - updated_at
      properties:
        enabled_event_types:
          type: array
          items:
            type: string
        overlay_path:
          type: string
        version:
          type: integer
          format: int64
        updated_at:
          type: string
          format: date-time
    CommunitySummary:
      type: object
      required:
        - active_viewers
        - total_viewers
        - points_in_circulation
        - active_rules
        - available_sources
        - total_sources
      properties:
        active_viewers:
          type: integer
          format: int64
        total_viewers:
          type: integer
          format: int64
        points_in_circulation:
          type: integer
          format: int64
        active_rules:
          type: integer
          format: int64
        available_sources:
          type: integer
          format: int64
        total_sources:
          type: integer
          format: int64
    CommunitySource:
      type: object
      required:
        - id
        - source_type
        - display_name
        - status
        - evidence_required
        - status_reason
        - updated_at
      properties:
        id:
          type: string
        source_type:
          type: string
        display_name:
          type: string
        status:
          type: string
        evidence_required:
          type: boolean
        status_reason:
          type: string
        updated_at:
          type: string
          format: date-time
    CommunityRule:
      type: object
      required:
        - id
        - name
        - source_type
        - source_status
        - status
        - amount
        - frequency_seconds
        - per_viewer_limit
        - global_limit
        - valid_from
        - valid_until
        - version
      properties:
        id:
          type: string
        name:
          type: string
        source_type:
          type: string
        source_status:
          type: string
        status:
          type: string
        amount:
          type: integer
          format: int64
        frequency_seconds:
          type: integer
        per_viewer_limit:
          type: [integer, "null"]
          format: int64
        global_limit:
          type: [integer, "null"]
          format: int64
        valid_from:
          type: [string, "null"]
          format: date-time
        valid_until:
          type: [string, "null"]
          format: date-time
        version:
          type: integer
          format: int64
    CommunityViewer:
      type: object
      required:
        - subscription_id
        - viewer_id
        - alias
        - avatar_url
        - status
        - balance
        - joined_at
        - last_movement_at
      properties:
        subscription_id:
          type: string
        viewer_id:
          type: string
        alias:
          type: string
        avatar_url:
          type: [string, "null"]
        status:
          type: string
        balance:
          type: integer
          format: int64
        joined_at:
          type: string
          format: date-time
        last_movement_at:
          type: [string, "null"]
          format: date-time
    CommunityLedgerEntry:
      type: object
      required:
        - id
        - subscription_id
        - viewer_id
        - viewer_alias
        - amount
        - balance_after
        - reason
        - source_type
        - created_at
      properties:
        id:
          type: string
        subscription_id:
          type: string
        viewer_id:
          type: string
        viewer_alias:
          type: string
        amount:
          type: integer
          format: int64
        balance_after:
          type: integer
          format: int64
        reason:
          type: string
        source_type:
          type: string
        created_at:
          type: string
          format: date-time
    CommunityChatCodeSettings:
      type: object
      required:
        - enabled
        - max_amount
      properties:
        enabled:
          type: boolean
        max_amount:
          type: integer
          format: int64
    CommunityOverview:
      type: object
      required:
        - program
        - access_settings
        - overlay_settings
        - summary
        - sources
        - rules
        - viewers
        - recent_entries
        - chat_codes
      properties:
        program:
          $ref: "#/components/schemas/CommunityProgram"
        access_settings:
          $ref: "#/components/schemas/CommunityAccessSettings"
        overlay_settings:
          $ref: "#/components/schemas/CommunityOverlaySettings"
        summary:
          $ref: "#/components/schemas/CommunitySummary"
        sources:
          type: array
          items:
            $ref: "#/components/schemas/CommunitySource"
        rules:
          type: array
          items:
            $ref: "#/components/schemas/CommunityRule"
        viewers:
          type: array
          items:
            $ref: "#/components/schemas/CommunityViewer"
        recent_entries:
          type: array
          items:
            $ref: "#/components/schemas/CommunityLedgerEntry"
        chat_codes:
          $ref: "#/components/schemas/CommunityChatCodeSettings"
    CommunityUpdateProgramRequest:
      type: object
      required:
        - name
        - symbol
        - expected_version
      properties:
        name:
          type: string
        symbol:
          type: string
        expected_version:
          type: integer
          format: int64
      additionalProperties: false
    CommunityUpdateAccessSettingsRequest:
      type: object
      description: >-
        kick_enabled y discord_enabled son opcionales (null o ausente = sin cambio).
        expected_version es obligatorio para el bloqueo optimista.
      required:
        - expected_version
      properties:
        kick_enabled:
          type: [boolean, "null"]
        discord_enabled:
          type: [boolean, "null"]
        expected_version:
          type: integer
          format: int64
      additionalProperties: false
    CommunityPointAdjustmentRequest:
      type: object
      required:
        - subscription_id
        - amount
        - reason
        - idempotency_key
      properties:
        subscription_id:
          type: string
        amount:
          type: integer
          format: int64
          description: Positivo acredita, negativo debita. No puede dejar el saldo en negativo.
        reason:
          type: string
        idempotency_key:
          type: string
      additionalProperties: false
    CommunityAdjustmentResult:
      type: object
      required:
        - entry
        - replayed
      properties:
        entry:
          $ref: "#/components/schemas/CommunityLedgerEntry"
        replayed:
          type: boolean
    CommunityFollowerDailyPoint:
      type: object
      required:
        - date
        - new_followers
      properties:
        date:
          type: string
          format: date
        new_followers:
          type: integer
          format: int64
    CommunityChannelFollowers:
      type: object
      required:
        - window_days
        - new_followers
        - daily
      properties:
        window_days:
          type: integer
        new_followers:
          type: integer
          format: int64
        daily:
          type: array
          items:
            $ref: "#/components/schemas/CommunityFollowerDailyPoint"
    CommunityMemberDailyPoint:
      type: object
      required:
        - date
        - new_members
      properties:
        date:
          type: string
          format: date
        new_members:
          type: integer
          format: int64
    CommunityMemberDaily:
      type: object
      required:
        - window_days
        - new_members
        - daily
      properties:
        window_days:
          type: integer
        new_members:
          type: integer
          format: int64
        daily:
          type: array
          items:
            $ref: "#/components/schemas/CommunityMemberDailyPoint"
    GiveawayCover:
      type: object
      required:
        - kind
        - url
      properties:
        kind:
          type: string
          description: gallery o custom.
        url:
          type: string
    GiveawayCoverWinner:
      type: object
      required:
        - rank
        - public_alias
        - public_reference
      properties:
        rank:
          type: integer
        public_alias:
          type: string
        public_reference:
          type: string
    GiveawayCoverMetrics:
      type: object
      required:
        - entries_count
        - participants_count
        - winners_selected
        - entry_points_collected
        - prize_points_awarded
      properties:
        entries_count:
          type: integer
        participants_count:
          type: integer
        winners_selected:
          type: integer
        entry_points_collected:
          type: integer
          format: int64
        prize_points_awarded:
          type: integer
          format: int64
    GiveawayCoverResource:
      type: object
      required:
        - id
        - title
        - description
        - prize_type
        - prize_description
        - prize_points_amount
        - terms_text
        - terms_url
        - starts_at
        - ends_at
        - winners_count
        - entry_points_cost
        - max_entries_per_viewer
        - terms_version
        - status
        - cover_kind
        - cover_ref
        - cover
        - version
        - published_at
        - created_at
        - updated_at
        - results_finalized
        - drawn_at
        - winners
        - metrics
      properties:
        id:
          type: string
        title:
          type: string
        description:
          type: string
        prize_type:
          type: string
        prize_description:
          type: string
        prize_points_amount:
          type: integer
          format: int64
        terms_text:
          type: [string, "null"]
        terms_url:
          type: [string, "null"]
        starts_at:
          type: string
          format: date-time
        ends_at:
          type: string
          format: date-time
        winners_count:
          type: integer
        entry_points_cost:
          type: integer
          format: int64
        max_entries_per_viewer:
          type: integer
        terms_version:
          type: string
        status:
          type: string
        cover_kind:
          type: string
        cover_ref:
          type: [string, "null"]
        cover:
          $ref: "#/components/schemas/GiveawayCover"
        version:
          type: integer
          format: int64
        published_at:
          type: [string, "null"]
          format: date-time
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        results_finalized:
          type: boolean
        drawn_at:
          type: [string, "null"]
          format: date-time
        winners:
          type: array
          items:
            $ref: "#/components/schemas/GiveawayCoverWinner"
        metrics:
          $ref: "#/components/schemas/GiveawayCoverMetrics"
    GiveawayUpdateCoverRequest:
      type: object
      required:
        - expected_version
        - cover_kind
      properties:
        expected_version:
          type: integer
          format: int64
        cover_kind:
          type: string
          description: Desde el runtime solo se admite gallery.
        cover_ref:
          type: [string, "null"]
          description: Clave de la galería fija. Vacío/null deriva la portada de prize_description.
      additionalProperties: false
    WorkUserRef:
      type: object
      description: Referencia a un usuario. email vacío y display_name null si ya no es miembro con acceso 'work'.
      required:
        - user_id
        - email
      properties:
        user_id:
          type: string
        email:
          type: string
        display_name:
          type: [string, "null"]
    WorkMember:
      type: object
      required:
        - user_id
        - email
      properties:
        user_id:
          type: string
        email:
          type: string
        display_name:
          type: [string, "null"]
        role_code:
          type: [string, "null"]
        area_code:
          type: [string, "null"]
        product_role:
          type: [string, "null"]
    WorkMembersResponse:
      type: object
      required:
        - members
      properties:
        members:
          type: array
          items:
            $ref: "#/components/schemas/WorkMember"
    WorkColumn:
      type: object
      required:
        - id
        - label
        - canonical_state
        - sort_order
      properties:
        id:
          type: string
        label:
          type: string
        canonical_state:
          type: string
        sort_order:
          type: integer
    WorkBoard:
      type: object
      required:
        - id
        - label
        - is_default
        - sort_order
        - archived
        - open_cards
        - columns
      properties:
        id:
          type: string
        label:
          type: string
        is_default:
          type: boolean
        sort_order:
          type: integer
        archived:
          type: boolean
        open_cards:
          type: integer
        columns:
          type: array
          items:
            $ref: "#/components/schemas/WorkColumn"
    WorkBoardResponse:
      type: object
      required:
        - board
      properties:
        board:
          $ref: "#/components/schemas/WorkBoard"
    WorkCreateBoardRequest:
      type: object
      required:
        - name
      properties:
        name:
          type: string
          description: Nombre del tablero (1..80).
        template:
          type: string
          description: Plantilla de columnas del tablero nuevo. Por defecto "general".
          enum:
            - general
            - short
    WorkPatchBoardRequest:
      type: object
      description: Renombra (name) y/o archiva/desarchiva (archived) un tablero. El tablero General no se archiva.
      properties:
        name:
          type: string
        archived:
          type: boolean
    WorkSpace:
      type: object
      required:
        - id
        - area_code
        - label
        - boards
      properties:
        id:
          type: string
        area_code:
          type: string
        label:
          type: string
        boards:
          type: array
          items:
            $ref: "#/components/schemas/WorkBoard"
    WorkSpacesResponse:
      type: object
      required:
        - spaces
      properties:
        spaces:
          type: array
          items:
            $ref: "#/components/schemas/WorkSpace"
    WorkCard:
      type: object
      required:
        - id
        - board_id
        - column_id
        - canonical_state
        - title
        - priority
        - executing_area
        - requested_by
        - position
        - posts_count
        - created_at
        - updated_at
        - overdue
      properties:
        id:
          type: string
        board_id:
          type: string
        column_id:
          type: string
        canonical_state:
          type: string
        title:
          type: string
        description:
          type: [string, "null"]
        priority:
          type: string
        due_at:
          type: [string, "null"]
          format: date-time
        requesting_area:
          type: [string, "null"]
        executing_area:
          type: string
        requested_by:
          $ref: "#/components/schemas/WorkUserRef"
        assignee:
          oneOf:
            - $ref: "#/components/schemas/WorkUserRef"
            - type: "null"
        assignee_role_code:
          type: [string, "null"]
        parent_card_id:
          type: [string, "null"]
        position:
          type: number
        posts_count:
          type: integer
        last_post_at:
          type: [string, "null"]
          format: date-time
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        overdue:
          type: boolean
    WorkCardsResponse:
      type: object
      required:
        - cards
      properties:
        cards:
          type: array
          items:
            $ref: "#/components/schemas/WorkCard"
    WorkCardResponse:
      type: object
      required:
        - card
      properties:
        card:
          $ref: "#/components/schemas/WorkCard"
    WorkPost:
      type: object
      required:
        - id
        - author
        - body
        - kind
        - mentions
        - created_at
      properties:
        id:
          type: string
        author:
          $ref: "#/components/schemas/WorkUserRef"
        body:
          type: string
        kind:
          type: string
        mentions:
          type: array
          items:
            type: string
        created_at:
          type: string
          format: date-time
    WorkPostResponse:
      type: object
      required:
        - post
      properties:
        post:
          $ref: "#/components/schemas/WorkPost"
    WorkCardDetail:
      type: object
      required:
        - card
        - posts
        - watchers
      properties:
        card:
          $ref: "#/components/schemas/WorkCard"
        posts:
          type: array
          items:
            $ref: "#/components/schemas/WorkPost"
        watchers:
          type: array
          items:
            $ref: "#/components/schemas/WorkUserRef"
    WorkCreateCardRequest:
      type: object
      required:
        - title
        - executing_area
      properties:
        title:
          type: string
        description:
          type: [string, "null"]
        priority:
          type: [string, "null"]
        due_at:
          type: [string, "null"]
          format: date-time
        executing_area:
          type: string
        assignee_user_id:
          type: [string, "null"]
        assignee_role_code:
          type: [string, "null"]
        requesting_area:
          type: [string, "null"]
        parent_card_id:
          type: [string, "null"]
      additionalProperties: false
    WorkPatchCardRequest:
      type: object
      description: Todos los campos son opcionales; se aplican solo los presentes. executing_area no se puede modificar (responde 422).
      properties:
        title:
          type: string
        description:
          type: [string, "null"]
        priority:
          type: string
        due_at:
          type: [string, "null"]
          format: date-time
        assignee_user_id:
          type: [string, "null"]
        assignee_role_code:
          type: [string, "null"]
        column_id:
          type: string
        position:
          type: number
      additionalProperties: false
    WorkCreatePostRequest:
      type: object
      required:
        - body
      properties:
        body:
          type: string
      additionalProperties: false
    TicketUserRef:
      type: object
      required:
        - id
        - email
      properties:
        id:
          type: string
        email:
          type: string
    Ticket:
      type: object
      required:
        - id
        - number
        - title
        - description
        - module
        - kind
        - severity
        - status
        - reporter
        - reporter_user_id
        - comments_count
        - origin
        - attachments
        - created_at
        - updated_at
      properties:
        id:
          type: string
        number:
          type: integer
          format: int64
        title:
          type: string
        description:
          type: string
        module:
          type: string
        kind:
          type: string
        severity:
          type: string
        status:
          type: string
        reporter:
          $ref: "#/components/schemas/TicketUserRef"
        reporter_user_id:
          type: string
        assignee:
          oneOf:
            - $ref: "#/components/schemas/TicketUserRef"
            - type: "null"
        assignee_user_id:
          type: [string, "null"]
        comments_count:
          type: integer
          format: int64
        origin:
          type: object
          additionalProperties: true
        attachments:
          description: Estructura libre de adjuntos tal como se envió.
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        resolved_at:
          type: [string, "null"]
          format: date-time
    TicketDetail:
      description: Detalle del ticket con los campos en la raíz y repetidos en 'ticket' por tolerancia.
      allOf:
        - $ref: "#/components/schemas/Ticket"
        - type: object
          required:
            - ticket
          properties:
            ticket:
              $ref: "#/components/schemas/Ticket"
    TicketListResponse:
      type: object
      required:
        - items
        - total
        - can_admin
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/Ticket"
        total:
          type: integer
        can_admin:
          type: boolean
    TicketResponse:
      type: object
      required:
        - ticket
      properties:
        ticket:
          $ref: "#/components/schemas/Ticket"
    TicketComment:
      type: object
      required:
        - id
        - ticket_id
        - author
        - author_user_id
        - body
        - created_at
      properties:
        id:
          type: string
        ticket_id:
          type: string
        author:
          $ref: "#/components/schemas/TicketUserRef"
        author_user_id:
          type: string
        body:
          type: string
        created_at:
          type: string
          format: date-time
    TicketCommentResponse:
      type: object
      required:
        - comment
      properties:
        comment:
          $ref: "#/components/schemas/TicketComment"
    TicketCommentsResponse:
      type: object
      required:
        - items
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/TicketComment"
    TicketStatsResponse:
      type: object
      required:
        - by_status
        - by_severity
        - open_total
        - total
      properties:
        by_status:
          type: object
          additionalProperties:
            type: integer
        by_severity:
          type: object
          additionalProperties:
            type: integer
        open_total:
          type: integer
        total:
          type: integer
    TicketOrigin:
      type: object
      properties:
        url:
          type: string
        product:
          type: string
        user_agent:
          type: string
        session_id:
          type: string
      additionalProperties: false
    TicketCreateRequest:
      type: object
      required:
        - title
        - description
        - module
        - kind
        - severity
      properties:
        title:
          type: string
        description:
          type: string
        module:
          type: string
        kind:
          type: string
        severity:
          type: string
        origin:
          $ref: "#/components/schemas/TicketOrigin"
        attachments:
          description: Estructura libre de adjuntos.
    TicketPatchRequest:
      type: object
      description: Todos los campos son opcionales; se aplican solo los presentes. assignee_user_id null desasigna.
      properties:
        title:
          type: string
        status:
          type: string
        severity:
          type: string
        assignee_user_id:
          type: [string, "null"]
      additionalProperties: false
    TicketCommentCreateRequest:
      type: object
      required:
        - body
      properties:
        body:
          type: string
      additionalProperties: false
    IntegrationError:
      type: object
      description: Cuerpo de error del módulo Integraciones. NO es problem+json.
      required: [code, message]
      properties:
        code:
          type: string
        message:
          type: string
    IntegrationValidationError:
      type: object
      required: [code, message, fields]
      properties:
        code:
          type: string
          const: validation_failed
        message:
          type: string
        fields:
          type: object
          additionalProperties:
            type: string
    IntegrationUserRef:
      type: object
      required: [id, name]
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
    Integration:
      type: object
      required: [code, enabled, marketing_enabled, config, secret_set, updated_at, status]
      properties:
        code:
          type: string
          enum: [sendgrid, twilio_sms, telegram]
        enabled:
          type: boolean
        marketing_enabled:
          type: boolean
          description: Solo twilio_sms. Segundo interruptor del SMS de marketing.
        config:
          type: object
          description: "Configuración no secreta: sendgrid (from_email, from_name, reply_to, webhook_public_key), twilio_sms (account_sid, messaging_service_sid, price_per_segment_usd), telegram (bot_username, lo fija la prueba)."
          additionalProperties: true
        secret_set:
          type: boolean
        secret_last4:
          type: string
        secret_set_at:
          type: string
          format: date-time
        secret_set_by:
          $ref: "#/components/schemas/IntegrationUserRef"
        last_test_at:
          type: string
          format: date-time
        last_test_ok:
          type: boolean
        last_error:
          type: string
        updated_at:
          type: string
          format: date-time
        updated_by:
          $ref: "#/components/schemas/IntegrationUserRef"
        status:
          type: string
          enum: [connected, error, off, unconfigured]
    IntegrationsResponse:
      type: object
      required: [encryption_available, integrations]
      properties:
        encryption_available:
          type: boolean
        integrations:
          type: array
          items:
            $ref: "#/components/schemas/Integration"
    IntegrationUpdateRequest:
      type: object
      properties:
        config:
          type: object
          additionalProperties: true
        secret:
          type: string
          minLength: 1
          maxLength: 4096
          description: Secreto de solo escritura (API key de SendGrid, Auth Token de Twilio o token del bot). Omitido = no cambia.
        marketing_enabled:
          type: boolean
    IntegrationTestRequest:
      type: object
      properties:
        to:
          type: string
          description: "sendgrid: correo que recibe el correo de prueba (obligatorio). twilio_sms: número E.164 que recibe un SMS de prueba (opcional). telegram: no se usa."
    IntegrationTestResult:
      type: object
      required: [ok, message, at]
      properties:
        ok:
          type: boolean
        message:
          type: string
        at:
          type: string
          format: date-time
    IntegrationAuditEntry:
      type: object
      required: [id, created_at, code, action, detail]
      properties:
        id:
          type: string
          format: uuid
        created_at:
          type: string
          format: date-time
        actor:
          $ref: "#/components/schemas/IntegrationUserRef"
        code:
          type: string
          enum: [sendgrid, twilio_sms, telegram]
        action:
          type: string
        detail:
          type: object
          additionalProperties: true
    IntegrationAuditResponse:
      type: object
      required: [entries]
      properties:
        entries:
          type: array
          items:
            $ref: "#/components/schemas/IntegrationAuditEntry"
    AlertTeam:
      type: object
      required: [id, name, status, created_at]
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        status:
          type: string
          enum: [pending, linked, unlinked]
        chat_title:
          type: string
        linked_at:
          type: string
          format: date-time
        link_expires_at:
          type: string
          format: date-time
        quiet_from:
          type: string
          pattern: "^([01][0-9]|2[0-3]):[0-5][0-9]$"
        quiet_to:
          type: string
          pattern: "^([01][0-9]|2[0-3]):[0-5][0-9]$"
        created_at:
          type: string
          format: date-time
    AlertTeamsResponse:
      type: object
      required: [teams]
      properties:
        teams:
          type: array
          items:
            $ref: "#/components/schemas/AlertTeam"
    AlertTeamRequest:
      type: object
      required: [name]
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 60
        quiet_from:
          type: string
        quiet_to:
          type: string
    AlertTeamLinkCode:
      type: object
      required: [url, code, expires_at]
      properties:
        url:
          type: string
          format: uri
        code:
          type: string
        expires_at:
          type: string
          format: date-time
    AlertKind:
      type: object
      required: [code, label, group, critical]
      properties:
        code:
          type: string
        label:
          type: string
        group:
          type: string
        critical:
          type: boolean
    AlertRoute:
      type: object
      required: [alert_kind, team_id]
      properties:
        alert_kind:
          type: string
          enum:
            - vip_payment.deposit_failed_streak
            - vip_payment.withdrawal_high_pending
            - vip_payment.withdrawal_failed
            - vip_payment.self_excluded_deposit_attempt
            - vip_candidate
            - payment_method.effectiveness_drop
            - data_quality
        team_id:
          type: string
          format: uuid
    AlertRoutesRequest:
      type: object
      required: [routes]
      properties:
        routes:
          type: array
          items:
            $ref: "#/components/schemas/AlertRoute"
    AlertRoutesResponse:
      type: object
      required: [kinds, teams, routes]
      properties:
        kinds:
          type: array
          items:
            $ref: "#/components/schemas/AlertKind"
        teams:
          type: array
          items:
            $ref: "#/components/schemas/AlertTeam"
        routes:
          type: array
          items:
            $ref: "#/components/schemas/AlertRoute"
    AlertDelivery:
      type: object
      required: [id, team_id, team_name, alert_kind, severity, tenant_slug, title, status, attempts, created_at]
      properties:
        id:
          type: string
          format: uuid
        team_id:
          type: string
          format: uuid
        team_name:
          type: string
        alert_kind:
          type: string
        severity:
          type: string
          enum: [info, warning, critical]
        tenant_slug:
          type: string
        title:
          type: string
        status:
          type: string
          enum: [pending, sent, grouped, skipped, failed]
        reason:
          type: string
        attempts:
          type: integer
        created_at:
          type: string
          format: date-time
        sent_at:
          type: string
          format: date-time
    AlertDeliveriesResponse:
      type: object
      required: [deliveries]
      properties:
        deliveries:
          type: array
          items:
            $ref: "#/components/schemas/AlertDelivery"
    AssistantError:
      type: object
      description: Cuerpo de error del asistente. NO es problem+json.
      required:
        - code
        - message
      properties:
        code:
          type: string
        message:
          type: string
    AssistantBudget:
      type: object
      required:
        - tenant_used_usd
        - tenant_cap_usd
        - user_used_usd
        - user_cap_usd
        - platform_used_usd
        - platform_cap_usd
        - department_used_usd
      properties:
        tenant_used_usd:
          type: number
          deprecated: true
          description: Deprecado (PRD-51); espeja platform_used_usd.
        tenant_cap_usd:
          type: number
          deprecated: true
          description: Deprecado (PRD-51); espeja platform_cap_usd.
        user_used_usd:
          type: number
        user_cap_usd:
          type: number
        platform_used_usd:
          type: number
        platform_cap_usd:
          type: number
        department_code:
          type: [string, "null"]
        department_used_usd:
          type: number
        department_cap_usd:
          type: [number, "null"]
    AssistantStatusResponse:
      type: object
      required:
        - configured
        - reason
        - name
        - provider
        - model
        - budget
      properties:
        configured:
          type: boolean
        reason:
          type: [string, "null"]
          description: >-
            null cuando Ray responde; si no, el primer motivo por el que no lo hace, en este orden:
            disabled (desactivado), missing_provider, missing_model, missing_key (sin llave) o
            key_unreadable (hay llave guardada pero no se pudo descifrar).
          enum:
            - disabled
            - missing_provider
            - missing_model
            - missing_key
            - key_unreadable
            - null
        name:
          type: string
        provider:
          type: [string, "null"]
        model:
          type: [string, "null"]
        budget:
          $ref: "#/components/schemas/AssistantBudget"
    AssistantSettingsUserRef:
      type: [object, "null"]
      properties:
        id:
          type: string
        name:
          type: [string, "null"]
    AssistantSettingsApiKey:
      type: object
      required:
        - set
      properties:
        set:
          type: boolean
        last4:
          type: [string, "null"]
        set_at:
          type: [string, "null"]
          format: date-time
        set_by:
          $ref: "#/components/schemas/AssistantSettingsUserRef"
    AssistantSettingsPrices:
      type: object
      required:
        - input_per_mtok_usd
        - output_per_mtok_usd
      properties:
        input_per_mtok_usd:
          type: number
        output_per_mtok_usd:
          type: number
    AssistantSettingsLimits:
      type: object
      required:
        - platform_monthly_cap_usd
        - default_department_monthly_cap_usd
        - default_user_monthly_cap_usd
        - rate_per_hour
      properties:
        platform_monthly_cap_usd:
          type: number
        default_department_monthly_cap_usd:
          type: number
        default_user_monthly_cap_usd:
          type: number
        rate_per_hour:
          type: integer
    AssistantSettingsResponse:
      type: object
      description: Configuración efectiva de Ray. La llave nunca se devuelve (solo api_key.last4).
      required:
        - source
        - enabled
        - api_key
        - encryption_available
        - prices
        - limits
      properties:
        source:
          type: string
          enum: [database, environment, none]
        enabled:
          type: boolean
        provider:
          type: [string, "null"]
        model:
          type: [string, "null"]
        base_url:
          type: [string, "null"]
        api_key:
          $ref: "#/components/schemas/AssistantSettingsApiKey"
        encryption_available:
          type: boolean
        prices:
          $ref: "#/components/schemas/AssistantSettingsPrices"
        limits:
          $ref: "#/components/schemas/AssistantSettingsLimits"
        updated_at:
          type: [string, "null"]
          format: date-time
        updated_by:
          $ref: "#/components/schemas/AssistantSettingsUserRef"
    AssistantSettingsUpdateRequest:
      type: object
      required:
        - enabled
        - provider
        - model
        - prices
        - limits
      properties:
        enabled:
          type: boolean
        provider:
          type: [string, "null"]
          enum: [anthropic, openai, null]
        model:
          type: string
        base_url:
          type: [string, "null"]
        api_key:
          type: [string, "null"]
          description: Omitido conserva la llave; string vacío devuelve 400 (para quitarla usar DELETE).
        prices:
          $ref: "#/components/schemas/AssistantSettingsPrices"
        limits:
          $ref: "#/components/schemas/AssistantSettingsLimits"
    AssistantSettingsInvalidError:
      type: object
      description: Error de validación de configuración (assistant_settings_invalid).
      required:
        - code
        - message
      properties:
        code:
          type: string
        message:
          type: string
        fields:
          type: object
          additionalProperties:
            type: string
    AssistantTestConnectionRequest:
      type: object
      required:
        - provider
        - model
      properties:
        provider:
          type: string
          enum: [anthropic, openai]
        model:
          type: string
        base_url:
          type: [string, "null"]
        api_key:
          type: [string, "null"]
          description: Omitido usa la llave guardada.
    AssistantTestConnectionResponse:
      type: object
      required:
        - ok
      properties:
        ok:
          type: boolean
        latency_ms:
          type: integer
        error_code:
          type: string
          enum: [unauthorized, model_not_found, timeout, provider_error]
        message:
          type: string
    AssistantAuditEntry:
      type: object
      required:
        - id
        - created_at
        - action
        - detail
      properties:
        id:
          type: string
        created_at:
          type: string
          format: date-time
        actor:
          type: [object, "null"]
          properties:
            id:
              type: string
            name:
              type: [string, "null"]
        action:
          type: string
          enum: [settings_updated, api_key_set, api_key_removed, department_cap_updated, user_cap_updated]
        detail:
          type: object
    AssistantAuditResponse:
      type: object
      required:
        - items
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/AssistantAuditEntry"
    AssistantDepartmentBudget:
      type: object
      required:
        - code
        - label
        - cap_usd
        - cap_source
        - used_usd
        - members
      properties:
        code:
          type: string
        label:
          type: string
        cap_usd:
          type: number
        cap_source:
          type: string
          enum: [default, override, shared]
        shares_with:
          type: [string, "null"]
        used_usd:
          type: number
        members:
          type: integer
    AssistantUserBudget:
      type: object
      required:
        - user_id
        - name
        - email
        - cap_usd
        - cap_source
        - used_usd
      properties:
        user_id:
          type: string
        name:
          type: string
        email:
          type: string
        department_code:
          type: [string, "null"]
        cap_usd:
          type: number
        cap_source:
          type: string
          enum: [default, override]
        used_usd:
          type: number
    AssistantBudgetsResponse:
      type: object
      required:
        - month_start
        - platform
        - departments
        - users
      properties:
        month_start:
          type: string
          format: date-time
        platform:
          type: object
          required:
            - used_usd
            - cap_usd
          properties:
            used_usd:
              type: number
            cap_usd:
              type: number
        departments:
          type: array
          items:
            $ref: "#/components/schemas/AssistantDepartmentBudget"
        users:
          type: array
          items:
            $ref: "#/components/schemas/AssistantUserBudget"
    AssistantDepartmentBudgetUpdateRequest:
      type: object
      properties:
        monthly_cap_usd:
          type: [number, "null"]
        shares_with:
          type: [string, "null"]
    AssistantUserBudgetUpdateRequest:
      type: object
      properties:
        monthly_cap_usd:
          type: [number, "null"]
    AssistantConversationSummary:
      type: object
      required:
        - id
        - title
        - updated_at
        - message_count
      properties:
        id:
          type: string
        title:
          type: string
        updated_at:
          type: string
          format: date-time
        message_count:
          type: integer
    AssistantConversationListResponse:
      type: object
      required:
        - items
        - next_cursor
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/AssistantConversationSummary"
        next_cursor:
          type: [string, "null"]
        conversation_limit:
          type: integer
          description: Tope de conversaciones activas propias por persona; al alcanzarlo, crear una nueva responde 409.
    AssistantCreateConversationRequest:
      type: object
      properties:
        title:
          type: string
      additionalProperties: false
    AssistantConversationCreated:
      type: object
      required:
        - id
        - title
        - created_at
      properties:
        id:
          type: string
        title:
          type: string
        created_at:
          type: string
          format: date-time
    AssistantMessage:
      type: object
      required:
        - id
        - role
        - content
        - created_at
      properties:
        id:
          type: string
        role:
          type: string
        content:
          type: string
        created_at:
          type: string
          format: date-time
        tool_name:
          type: string
        tool_calls:
          type: array
          items:
            type: object
            additionalProperties: true
    AssistantConversationDetail:
      type: object
      required:
        - id
        - title
        - created_at
        - messages
      properties:
        id:
          type: string
        title:
          type: string
        created_at:
          type: string
          format: date-time
        messages:
          type: array
          items:
            $ref: "#/components/schemas/AssistantMessage"
    AssistantPatchConversationRequest:
      type: object
      description: Campos opcionales; se aplican solo los presentes.
      properties:
        title:
          type: string
        archived:
          type: boolean
      additionalProperties: false
    AssistantPostMessageRequest:
      type: object
      required:
        - content
      properties:
        content:
          type: string
      additionalProperties: false
    Notification:
      type: object
      required:
        - id
        - created_at
      properties:
        id:
          type: string
        kind:
          type: [string, "null"]
        title:
          type: [string, "null"]
        body:
          type: [string, "null"]
        product:
          type: [string, "null"]
        link_path:
          type: [string, "null"]
        entity_id:
          type: [string, "null"]
        read_at:
          type: [string, "null"]
          format: date-time
        created_at:
          type: string
          format: date-time
    NotificationListResponse:
      type: object
      required:
        - notifications
        - unread_count
      properties:
        notifications:
          type: array
          items:
            $ref: "#/components/schemas/Notification"
        unread_count:
          type: integer
    NotificationMarkAllResponse:
      type: object
      required:
        - marked
      properties:
        marked:
          type: integer
    NotificationDismissAllResponse:
      type: object
      required:
        - dismissed
      properties:
        dismissed:
          type: integer
          minimum: 0
          description: Cantidad de notificaciones eliminadas de la bandeja en esta llamada.
    ViewerSessionView:
      type: object
      required:
        - viewer_id
        - alias
        - avatar_url
        - identities
      properties:
        viewer_id:
          type: string
        alias:
          type: string
        avatar_url:
          type: [string, "null"]
        identities:
          type: array
          items:
            $ref: "#/components/schemas/ViewerExternalIdentity"
        membership:
          $ref: "#/components/schemas/ViewerMembership"
    ViewerExternalIdentity:
      type: object
      required:
        - provider
        - handle
      properties:
        provider:
          type: string
        handle:
          type: string
    ViewerMembership:
      type: object
      required:
        - subscription_id
        - status
        - balance
        - program_name
        - program_symbol
      properties:
        subscription_id:
          type: string
        status:
          type: string
        balance:
          type: integer
          format: int64
        program_name:
          type: string
        program_symbol:
          type: string
    ViewerSubscriptionRequest:
      type: object
      required:
        - streamer_slug
      properties:
        streamer_slug:
          type: string
      additionalProperties: false
    StreamerSiteSocialLinks:
      type: object
      properties:
        kick:
          type: [string, "null"]
        discord:
          type: [string, "null"]
        youtube:
          type: [string, "null"]
        twitch:
          type: [string, "null"]
        instagram:
          type: [string, "null"]
        x:
          type: [string, "null"]
    StreamerSiteSlotCode:
      type: object
      required:
        - slot_key
        - code
      properties:
        slot_key:
          type: string
        code:
          type: string
    StreamerSiteSettings:
      type: object
      required:
        - slug
        - display_name
        - tagline
        - bio
        - creator_color
        - theme
        - publication_status
        - has_unpublished_changes
        - social_links
        - slot_codes
        - version
        - updated_at
        - published_at
      properties:
        slug:
          type: string
        display_name:
          type: string
        tagline:
          type: string
        bio:
          type: string
        creator_color:
          type: string
        theme:
          type: string
        publication_status:
          type: string
          enum:
            - draft
            - published
            - unpublished
        has_unpublished_changes:
          type: boolean
        social_links:
          $ref: "#/components/schemas/StreamerSiteSocialLinks"
        slot_codes:
          type: array
          items:
            $ref: "#/components/schemas/StreamerSiteSlotCode"
        version:
          type: integer
          format: int64
        updated_at:
          type: string
          format: date-time
        published_at:
          type: [string, "null"]
          format: date-time
    UpdateStreamerSiteSettingsRequest:
      type: object
      required:
        - expected_version
      properties:
        display_name:
          type: string
        tagline:
          type: string
        bio:
          type: string
        creator_color:
          type: string
        theme:
          type: string
        publication_status:
          type: string
          enum:
            - draft
            - published
            - unpublished
        social_links:
          type: object
          description: Objeto de enlaces sociales; cada enlace es una URL HTTPS o null.
          additionalProperties:
            type: [string, "null"]
        slot_codes:
          type: array
          items:
            $ref: "#/components/schemas/StreamerSiteSlotCode"
        expected_version:
          type: integer
          format: int64
      additionalProperties: false
    NotificationPreferences:
      type: object
      required:
        - product_updates
        - weekly_summary
      properties:
        product_updates:
          type: boolean
        weekly_summary:
          type: boolean
    UpdateMyProfileRequest:
      type: object
      properties:
        display_name:
          type: [string, "null"]
        notification_preferences:
          $ref: "#/components/schemas/NotificationPreferences"
      additionalProperties: false
    ProductGrant:
      type: object
      required:
        - product
        - role
        - area
      properties:
        product:
          type: string
        role:
          type: string
        area:
          type: [string, "null"]
    SessionMembership:
      type: object
      required:
        - id
        - role
        - status
        - tenant_id
        - tenant_slug
        - tenant_display_name
        - tenant_kind
        - creator_enabled
        - casino_slug
        - team_role_code
        - team_role_label
      properties:
        id:
          type: string
        role:
          type: string
        status:
          type: string
        tenant_id:
          type: string
        tenant_slug:
          type: string
        tenant_display_name:
          type: string
        tenant_kind:
          type: string
        partner_status:
          type: string
        creator_enabled:
          type: boolean
        casino_slug:
          type: [string, "null"]
        team_role_code:
          type: [string, "null"]
          description: Rol de equipo de la membresía; null para el owner y el miembro "Personalizado".
        team_role_label:
          type: [string, "null"]
          description: Etiqueta del rol de equipo; solo se resuelve para la membresía del tenant activo.
    PublicUser:
      type: object
      required:
        - id
        - email
        - roles
        - principal_mode
        - tenant_id
        - tenant_slug
        - tenant_kind
        - creator_enabled
        - active_membership_id
        - casino_slug
        - notification_preferences
        - products
        - platform_admin
        - company_domains
        - company_account
        - platform_admin_available
      properties:
        id:
          type: string
        email:
          type: string
        roles:
          type: array
          items:
            type: string
        principal_mode:
          type: string
        tenant_id:
          type: [string, "null"]
        tenant_slug:
          type: [string, "null"]
        tenant_kind:
          type: [string, "null"]
        partner_status:
          type: string
        creator_enabled:
          type: boolean
        creator_disabled_reason:
          type: string
        active_membership_id:
          type: [string, "null"]
        display_name:
          type: string
        casino_slug:
          type: [string, "null"]
        notification_preferences:
          $ref: "#/components/schemas/NotificationPreferences"
        memberships:
          type: array
          items:
            $ref: "#/components/schemas/SessionMembership"
        products:
          type: array
          items:
            $ref: "#/components/schemas/ProductGrant"
        platform_admin:
          type: boolean
        company_domains:
          type: array
          items:
            type: string
        company_account:
          type: boolean
        platform_admin_available:
          type: boolean
    AuthResult:
      type: object
      required:
        - user
        - access_token
        - token_type
        - expires_in
      properties:
        user:
          $ref: "#/components/schemas/PublicUser"
        access_token:
          type: string
        token_type:
          type: string
        expires_in:
          type: integer
    ChangePasswordRequest:
      type: object
      required:
        - current_password
        - new_password
      properties:
        current_password:
          type: string
        new_password:
          type: string
      additionalProperties: false
    ApiTokenView:
      type: object
      required:
        - id
        - name
        - prefix
        - scopes
        - expires_at
        - last_used_at
        - revoked_at
        - created_at
      properties:
        id:
          type: string
        name:
          type: string
        prefix:
          type: string
        scopes:
          type: array
          items:
            type: string
        expires_at:
          type: string
          format: date-time
        last_used_at:
          type: [string, "null"]
          format: date-time
        revoked_at:
          type: [string, "null"]
          format: date-time
        created_at:
          type: string
          format: date-time
    ApiTokenListResponse:
      type: object
      required:
        - api_tokens
      properties:
        api_tokens:
          type: array
          items:
            $ref: "#/components/schemas/ApiTokenView"
    CreateApiTokenRequest:
      type: object
      required:
        - name
      properties:
        name:
          type: string
          description: Nombre del token (obligatorio, hasta 80 caracteres).
        scopes:
          type: array
          description: Scopes válidos, 'read' e 'import'.
          items:
            type: string
        expires_in_days:
          type: integer
          description: Vencimiento entre 1 y 180 días.
      additionalProperties: false
    CreateApiTokenResponse:
      type: object
      required:
        - token
        - api_token
      properties:
        token:
          type: string
          description: Secreto del token (cvr_...). Se devuelve una única vez y no puede recuperarse.
        api_token:
          $ref: "#/components/schemas/ApiTokenView"
    PartnerMetricRef:
      type: object
      required:
        - tenant_slug
        - display_name
      properties:
        tenant_slug:
          type: string
        display_name:
          type: string
    PartnerMetric:
      type: object
      required:
        - id
        - partner
        - export_kind
        - period_month
        - period_from
        - period_to
        - source_currency
        - players_registered
        - ftd_count
        - ftd_amount_minor
        - deposit_count
        - deposit_amount_minor
        - ngr_amount_minor
        - cpa_amount_minor
        - total_earnings_minor
        - ggr_amount_minor
        - bet_amount_minor
        - bet_count
        - withdraw_amount_minor
        - withdraw_count
        - source_as_of
      properties:
        id:
          type: string
        partner:
          $ref: "#/components/schemas/PartnerMetricRef"
        export_kind:
          type: string
        period_month:
          type: string
          description: YYYY-MM
        period_from:
          type: string
          format: date-time
        period_to:
          type: string
          format: date-time
        source_currency:
          type: string
        players_registered:
          type: integer
          format: int64
        ftd_count:
          type: integer
          format: int64
        ftd_amount_minor:
          type: integer
          format: int64
        deposit_count:
          type: integer
          format: int64
        deposit_amount_minor:
          type: integer
          format: int64
        ngr_amount_minor:
          type: integer
          format: int64
        cpa_amount_minor:
          type: integer
          format: int64
        total_earnings_minor:
          type: integer
          format: int64
        ggr_amount_minor:
          type: [integer, "null"]
          format: int64
        bet_amount_minor:
          type: [integer, "null"]
          format: int64
        bet_count:
          type: [integer, "null"]
          format: int64
        withdraw_amount_minor:
          type: [integer, "null"]
          format: int64
        withdraw_count:
          type: [integer, "null"]
          format: int64
        source_as_of:
          type: string
          format: date-time
    PartnerMetricsIngestionStatus:
      type: object
      required:
        - partner
        - coverage_from
        - coverage_to
        - last_ingested_at
        - has_gaps
        - gap_count
        - last_reason_code
      properties:
        partner:
          $ref: "#/components/schemas/PartnerMetricRef"
        coverage_from:
          type: [string, "null"]
          format: date-time
        coverage_to:
          type: [string, "null"]
          format: date-time
        last_ingested_at:
          type: [string, "null"]
          format: date-time
        has_gaps:
          type: boolean
        gap_count:
          type: integer
        last_reason_code:
          type: [string, "null"]
    PartnerMetricsPage:
      type: object
      required:
        - items
        - next_cursor
        - limit
        - ingestion
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/PartnerMetric"
        next_cursor:
          type: [string, "null"]
        limit:
          type: integer
        ingestion:
          type: array
          items:
            $ref: "#/components/schemas/PartnerMetricsIngestionStatus"
    PartnerDataMetricsRange:
      type: object
      required:
        - from
        - to
      properties:
        from:
          type: string
        to:
          type: string
    PartnerDataMetricsSources:
      type: object
      required:
        - deposits_from
        - bets_from
      properties:
        deposits_from:
          type: string
        bets_from:
          type: string
    PartnerDataMetricsRegistrationPoint:
      type: object
      required:
        - day
        - registered
        - ftd
      properties:
        day:
          type: string
        registered:
          type: integer
          format: int64
        ftd:
          type: integer
          format: int64
    PartnerDataMetricsSummary:
      type: object
      required:
        - currency
        - range
        - sources
        - registered
        - ftd
        - deposit_amount_minor
        - net_cash_minor
        - ngr_minor
        - conversion_ftd
        - series
        - low_population
        - min_players_threshold
      properties:
        currency:
          type: string
        range:
          $ref: "#/components/schemas/PartnerDataMetricsRange"
        sources:
          $ref: "#/components/schemas/PartnerDataMetricsSources"
        registered:
          type: integer
          format: int64
        ftd:
          type: integer
          format: int64
        deposit_amount_minor:
          type: [integer, "null"]
          format: int64
        net_cash_minor:
          type: [integer, "null"]
          format: int64
        ngr_minor:
          type: [integer, "null"]
          format: int64
        conversion_ftd:
          type: [number, "null"]
        series:
          type: array
          items:
            $ref: "#/components/schemas/PartnerDataMetricsRegistrationPoint"
        low_population:
          type: boolean
        min_players_threshold:
          type: integer
    PartnerInvitationAcceptanceRequest:
      type: object
      required:
        - token
        - password
      properties:
        token:
          type: string
        password:
          type: string
      additionalProperties: false
    PartnerInvitationAcceptance:
      type: object
      required:
        - user_id
        - tenant_id
        - tenant_slug
        - role
      properties:
        user_id:
          type: string
        tenant_id:
          type: string
        tenant_slug:
          type: string
        role:
          type: string
    NexorWebhookEvent:
      type: object
      description: >-
        Sobre del webhook de Nexor. Convray solo lee event_type, event_id,
        webhook_id y el cambio de estado con la referencia del jugador; el resto
        de la PII del sobre se descarta al deserializar.
      properties:
        event_type:
          type: string
        event_id:
          type: string
          description: Clave de idempotencia (obligatoria en el body o el header).
        webhook_id:
          type: string
        data:
          type: object
          properties:
            status_change:
              type: object
              properties:
                to_status:
                  type: object
                  properties:
                    key:
                      type: string
                    category:
                      type: string
            lead:
              type: object
              properties:
                external_id:
                  type: string
                metadata:
                  type: object
                  properties:
                    casino_player_ref:
                      type: string
    VipPaymentAlertPlayerFlags:
      type: object
      description: Banderas del jugador para los badges (etiquetas de Centrivo + estado de Players).
      properties:
        vip:
          type: boolean
        vip_level:
          type: string
          nullable: true
        vip_experience:
          type: boolean
        ex_vip:
          type: boolean
        partner_tag:
          type: boolean
        staff:
          type: boolean
        self_excluded:
          type: boolean
        blocked:
          type: boolean
        attention:
          type: boolean
    VipPaymentAlert:
      type: object
      description: >-
        Alerta preventiva de pagos VIP (PRD-50 Fase 5). detail y notified_channels
        son objetos sin PII más allá del Player ID de Juégalo y el ID de Centrivo
        del retiro.
      required:
        - id
        - external_player_ref
        - rule
        - severity
        - status
        - opened_at
        - player_flags
      properties:
        id:
          type: string
        external_player_ref:
          type: string
        rule:
          type: string
          description: deposit_failed_streak | withdrawal_high_pending | withdrawal_failed | self_excluded_deposit_attempt
        severity:
          type: string
          description: warning | critical
        detail:
          type: object
          additionalProperties: true
        status:
          type: string
          description: open | resolved | reviewed
        opened_at:
          type: string
          format: date-time
        resolved_at:
          type: string
          format: date-time
          nullable: true
        reviewed_by:
          type: string
          nullable: true
        review_note:
          type: string
          nullable: true
        notified_channels:
          type: object
          additionalProperties: true
        player_flags:
          $ref: "#/components/schemas/VipPaymentAlertPlayerFlags"
    VipPaymentAlertListResponse:
      type: object
      required:
        - alerts
        - can_admin
      properties:
        alerts:
          type: array
          items:
            $ref: "#/components/schemas/VipPaymentAlert"
        can_admin:
          type: boolean
          description: Si el usuario puede marcar revisadas y editar los ajustes (admin de Data).
    VipPaymentAlertResponse:
      type: object
      required:
        - alert
      properties:
        alert:
          $ref: "#/components/schemas/VipPaymentAlert"
    ReviewVipPaymentAlertRequest:
      type: object
      properties:
        note:
          type: string
          description: Nota de la revisión (opcional).
    VipAlertSettings:
      type: object
      description: Umbrales y canales de las alertas de pagos VIP de un casino.
      required:
        - enabled
        - failed_streak
        - withdrawal_high_minor
        - withdrawal_pending_hours
        - channel_in_app
        - channel_work
        - channel_email
        - email_recipients
      properties:
        enabled:
          type: boolean
        failed_streak:
          type: integer
          description: Depósitos fallidos consecutivos que abren alerta (mínimo 1).
        withdrawal_high_minor:
          type: integer
          format: int64
          description: Monto (CLP, unidad menor == mayor) desde el que un retiro es alto.
        withdrawal_pending_hours:
          type: integer
          description: Horas que un retiro alto puede estar pendiente antes de alertar.
        channel_in_app:
          type: boolean
        channel_work:
          type: boolean
        channel_email:
          type: boolean
        work_destinations:
          type: array
          description: >-
            Destinos de Work (tablero + columna) donde la alerta abre una tarjeta. Varios destinos:
            la alerta crea una tarjeta en cada uno (ruling 2026-09-24).
          items:
            $ref: "#/components/schemas/VipWorkDestination"
        work_author_user_id:
          type: string
          nullable: true
        email_recipients:
          type: array
          items:
            type: string
    VipWorkDestination:
      type: object
      required:
        - board_id
        - column_id
      properties:
        board_id:
          type: string
        column_id:
          type: string
    DataWorkTargetColumn:
      type: object
      required:
        - id
        - label
        - canonical_state
      properties:
        id:
          type: string
        label:
          type: string
        canonical_state:
          type: string
    DataWorkTargetBoard:
      type: object
      required:
        - id
        - label
        - is_default
        - archived
        - columns
      properties:
        id:
          type: string
        label:
          type: string
        is_default:
          type: boolean
        archived:
          type: boolean
        columns:
          type: array
          items:
            $ref: "#/components/schemas/DataWorkTargetColumn"
    DataWorkTargetArea:
      type: object
      required:
        - area_code
        - label
        - boards
      properties:
        area_code:
          type: string
        label:
          type: string
        boards:
          type: array
          items:
            $ref: "#/components/schemas/DataWorkTargetBoard"
    DataWorkTargetsResponse:
      type: object
      required:
        - areas
      properties:
        areas:
          type: array
          items:
            $ref: "#/components/schemas/DataWorkTargetArea"
    VipAlertSettingsResponse:
      type: object
      required:
        - settings
      properties:
        settings:
          $ref: "#/components/schemas/VipAlertSettings"
    PutVipAlertSettingsRequest:
      type: object
      required:
        - enabled
        - failed_streak
        - withdrawal_high_minor
        - withdrawal_pending_hours
        - channel_in_app
        - channel_work
        - channel_email
      properties:
        enabled:
          type: boolean
        failed_streak:
          type: integer
        withdrawal_high_minor:
          type: integer
          format: int64
        withdrawal_pending_hours:
          type: integer
        channel_in_app:
          type: boolean
        channel_work:
          type: boolean
        channel_email:
          type: boolean
        work_destinations:
          type: array
          description: Destinos de Work (tablero + columna) donde la alerta abre una tarjeta.
          items:
            $ref: "#/components/schemas/VipWorkDestination"
        work_author_user_id:
          type: string
          nullable: true
          description: Autor de la tarjeta de Work. Si se omite, se usa quien guarda los ajustes.
        email_recipients:
          type: array
          items:
            type: string
    VipProgramSettings:
      type: object
      description: >-
        Ajustes del programa de detección temprana de jugadores VIP de un casino. Montos en CLP (pesos
        enteros). label_top_pct es la fracción de la cohorte que se etiqueta como top (0 < pct < 1).
      required:
        - enabled
        - rule_version
        - new_day1_sum_minor
        - new_day7_list_sum_minor
        - new_day7_priority_sum_minor
        - new_scoring_days
        - escalation_rolling30_minor
        - control_pct
        - program_seed
        - label_top_pct
      properties:
        enabled:
          type: boolean
          description: El programa nace apagado hasta que Cumplimiento apruebe el filtro de juego responsable.
        rule_version:
          type: string
          description: Versión de las reglas de scoring (no vacía).
        new_day1_sum_minor:
          type: integer
          format: int64
          description: Suma de depósitos en las primeras 24 h que dispara la alerta del día 1.
        new_day7_list_sum_minor:
          type: integer
          format: int64
          description: Umbral a 7 días para entrar a la lista de seguimiento.
        new_day7_priority_sum_minor:
          type: integer
          format: int64
          description: Umbral a 7 días para prioridad (debe ser mayor o igual que el de lista).
        new_scoring_days:
          type: integer
          description: Ventana de scoring del carril de jugadores nuevos, en días (1 a 90).
        escalation_rolling30_minor:
          type: integer
          format: int64
          description: Umbral de depósitos en 30 días móviles que escala a un jugador existente.
        control_pct:
          type: integer
          description: Porcentaje del grupo de control del experimento A/B (0 a 50).
        program_seed:
          type: integer
          format: int64
          description: Semilla estable para asignar tratamiento/control por jugador.
        label_top_pct:
          type: number
          description: Fracción de la cohorte etiquetada como top a 90 días (0 < pct < 1).
        updated_at:
          type: string
          format: date-time
          nullable: true
          description: Última actualización; null si el casino todavía no tiene ajustes persistidos.
    VipProgramSettingsResponse:
      type: object
      required:
        - settings
      properties:
        settings:
          $ref: "#/components/schemas/VipProgramSettings"
    PutVipProgramSettingsRequest:
      type: object
      required:
        - enabled
        - rule_version
        - new_day1_sum_minor
        - new_day7_list_sum_minor
        - new_day7_priority_sum_minor
        - new_scoring_days
        - escalation_rolling30_minor
        - control_pct
        - program_seed
        - label_top_pct
      properties:
        enabled:
          type: boolean
        rule_version:
          type: string
        new_day1_sum_minor:
          type: integer
          format: int64
        new_day7_list_sum_minor:
          type: integer
          format: int64
        new_day7_priority_sum_minor:
          type: integer
          format: int64
        new_scoring_days:
          type: integer
        escalation_rolling30_minor:
          type: integer
          format: int64
        control_pct:
          type: integer
        program_seed:
          type: integer
          format: int64
        label_top_pct:
          type: number
    PaymentMethodAlertParams:
      type: object
      description: >-
        Parámetros de la regla de efectividad por método de pago (PRD-55). Los
        porcentajes son enteros de 0 a 100. Viajan en la salud (para pintar las
        bandas: bajo floor_pct = mal, entre floor_pct y recovery_pct = atención, sobre
        recovery_pct = bien) y en el detalle de cada alerta (los usados al decidir).
      required:
        - window_minutes
        - settle_minutes
        - min_attempts
        - floor_pct
        - critical_pct
        - drop_points
        - baseline_days
        - baseline_min_attempts
        - recovery_pct
        - resolve_min_attempts
        - no_traffic_hours
        - muted_methods
      properties:
        window_minutes:
          type: integer
        settle_minutes:
          type: integer
        min_attempts:
          type: integer
        floor_pct:
          type: integer
        critical_pct:
          type: integer
        drop_points:
          type: integer
        baseline_days:
          type: integer
        baseline_min_attempts:
          type: integer
        recovery_pct:
          type: integer
        resolve_min_attempts:
          type: integer
        no_traffic_hours:
          type: integer
        muted_methods:
          type: array
          items:
            type: string
    PaymentMethodBaseline:
      type: object
      description: >-
        Base histórica del método: efectividad de los últimos `days` días civiles
        CERRADOS de Santiago, desde el rollup diario. valid es true solo si esos días
        suman al menos baseline_min_attempts intentos terminales. rate es null sin
        intentos.
      required:
        - days
        - rate
        - attempts
        - valid
      properties:
        days:
          type: integer
        rate:
          type: number
          nullable: true
        attempts:
          type: integer
        valid:
          type: boolean
    PaymentMethodHealthCounts:
      type: object
      description: >-
        Conteos de un método en la ventana vigente. attempts = paid + failed +
        declined (intentos terminales); cancelled (cancelados por el operador) y open
        (pendientes) quedan fuera. rate es la efectividad en porcentaje con dos
        decimales, null si attempts es 0. failed_without_external_ref cuenta fallidos
        y rechazados sin External ID; players_failed es la cantidad de jugadores
        distintos con un fallido o rechazado (solo el conteo).
      required:
        - paid
        - failed
        - declined
        - cancelled
        - open
        - attempts
        - rate
        - failed_without_external_ref
        - players_failed
      properties:
        paid:
          type: integer
        failed:
          type: integer
        declined:
          type: integer
        cancelled:
          type: integer
        open:
          type: integer
        attempts:
          type: integer
        rate:
          type: number
          nullable: true
        failed_without_external_ref:
          type: integer
        players_failed:
          type: integer
    PaymentMethodHealthHour:
      type: object
      description: >-
        Conteos de un método en una hora UTC. hour_start es el inicio de la hora en
        ISO UTC (la vista la rotula en hora de Santiago). rate es null si attempts es 0.
      required:
        - hour_start
        - paid
        - failed
        - declined
        - cancelled
        - open
        - attempts
        - rate
      properties:
        hour_start:
          type: string
          format: date-time
        paid:
          type: integer
        failed:
          type: integer
        declined:
          type: integer
        cancelled:
          type: integer
        open:
          type: integer
        attempts:
          type: integer
        rate:
          type: number
          nullable: true
    PaymentMethodHealthMethod:
      type: object
      required:
        - payment_method
        - current
        - baseline
        - state
        - hours
      properties:
        payment_method:
          type: string
          description: Texto crudo de Centrivo (BankCard, Mach, BankTransfer, BankTransferCL, AstroPay…). Vacío = sin método.
        current:
          $ref: "#/components/schemas/PaymentMethodHealthCounts"
        baseline:
          $ref: "#/components/schemas/PaymentMethodBaseline"
        state:
          type: string
          enum:
            - alert
            - ok
            - low_volume
            - muted
          description: >-
            alert si el método tiene un episodio abierto o su ventana vigente dispara
            la regla; muted si el casino lo silenció; low_volume si tiene menos de
            min_attempts intentos terminales en la ventana; ok en otro caso.
        hours:
          type: array
          description: Una entrada por hora UTC del rango pedido (serie densa, en orden), la última es la hora en curso.
          items:
            $ref: "#/components/schemas/PaymentMethodHealthHour"
    PaymentMethodHistoryPoint:
      type: object
      description: Efectividad de un día (key YYYY-MM-DD) o de un mes (key YYYY-MM). rate es null si attempts es 0.
      required:
        - key
        - paid
        - attempts
        - rate
      properties:
        key:
          type: string
        paid:
          type: integer
        attempts:
          type: integer
        rate:
          type: number
          nullable: true
    PaymentMethodHistorySeries:
      type: object
      required:
        - payment_method
        - days
        - months
      properties:
        payment_method:
          type: string
          description: Texto crudo de Centrivo; vacío = sin método (en total siempre vacío).
        days:
          type: array
          description: Un punto por día civil de Santiago, los últimos 30 en orden; el último es hoy.
          items:
            $ref: "#/components/schemas/PaymentMethodHistoryPoint"
        months:
          type: array
          description: Un punto por mes calendario, los últimos 12 en orden; el último es el mes en curso hasta hoy.
          items:
            $ref: "#/components/schemas/PaymentMethodHistoryPoint"
    PaymentMethodHistory:
      type: object
      description: Efectividad por día y por mes desde el rollup diario de depósitos (misma definición que la base).
      required:
        - today
        - total
        - methods
      properties:
        today:
          type: string
          format: date
          description: Día civil de Santiago del cálculo.
        total:
          $ref: "#/components/schemas/PaymentMethodHistorySeries"
        methods:
          type: array
          description: Métodos con intentos en los 12 meses, de más a menos intentos y sin método al final.
          items:
            $ref: "#/components/schemas/PaymentMethodHistorySeries"
    PaymentMethodHealthWindow:
      type: object
      required:
        - from
        - to
        - minutes
      properties:
        from:
          type: string
          format: date-time
        to:
          type: string
          format: date-time
        minutes:
          type: integer
    PaymentMethodHealthResponse:
      type: object
      required:
        - generated_at
        - window
        - data_fresh_until
        - stale
        - enabled
        - params
        - methods
        - history
        - can_admin
      properties:
        generated_at:
          type: string
          format: date-time
        window:
          $ref: "#/components/schemas/PaymentMethodHealthWindow"
        data_fresh_until:
          type: string
          format: date-time
          nullable: true
          description: Cobertura de la última corrida exitosa de depósitos del conector; null si nunca corrió.
        stale:
          type: boolean
          description: true si la cobertura del conector tiene más de 2 horas (la vigilancia está en pausa).
        enabled:
          type: boolean
          description: Interruptor de la alerta del casino.
        params:
          $ref: "#/components/schemas/PaymentMethodAlertParams"
        methods:
          type: array
          description: Métodos con depósitos en el rango, en la ventana o con un episodio abierto; los de más intentos primero y sin método al final.
          items:
            $ref: "#/components/schemas/PaymentMethodHealthMethod"
        history:
          $ref: "#/components/schemas/PaymentMethodHistory"
        can_admin:
          type: boolean
          description: Si el usuario puede marcar revisadas y editar los ajustes (admin de Data).
    PaymentMethodAlertWindowDetail:
      type: object
      description: Ventana vigente del método al decidir, con sus conteos (misma definición que PaymentMethodHealthCounts).
      required:
        - from
        - to
        - minutes
        - paid
        - failed
        - declined
        - cancelled
        - open
        - attempts
        - rate
        - failed_without_external_ref
        - players_failed
      properties:
        from:
          type: string
          format: date-time
        to:
          type: string
          format: date-time
        minutes:
          type: integer
        paid:
          type: integer
        failed:
          type: integer
        declined:
          type: integer
        cancelled:
          type: integer
        open:
          type: integer
        attempts:
          type: integer
        rate:
          type: number
          nullable: true
        failed_without_external_ref:
          type: integer
        players_failed:
          type: integer
    PaymentMethodAlertEpisodeDetail:
      type: object
      description: >-
        Totales del episodio sobre [started_at, corte vigente): failed suma fallidos y
        rechazados, attempts = paid + failed, players_failed es la cantidad de
        jugadores distintos afectados y failed_without_external_ref_pct el porcentaje
        de fallidos sin External ID (null sin fallidos).
      required:
        - paid
        - failed
        - attempts
        - players_failed
        - failed_without_external_ref_pct
      properties:
        from:
          type: string
          format: date-time
        to:
          type: string
          format: date-time
        paid:
          type: integer
        failed:
          type: integer
        attempts:
          type: integer
        players_failed:
          type: integer
        failed_without_external_ref:
          type: integer
        failed_without_external_ref_pct:
          type: number
          nullable: true
    PaymentMethodAlertOtherMethod:
      type: object
      description: Efectividad de otro método con intentos en la misma ventana.
      required:
        - payment_method
        - rate
        - attempts
      properties:
        payment_method:
          type: string
        rate:
          type: number
        attempts:
          type: integer
    PaymentMethodAlertDetail:
      type: object
      description: Detalle del episodio (solo agregados, sin PII) tal como lo guardó el vigilante en su última evaluación.
      additionalProperties: true
      required:
        - window
        - baseline
        - episode
        - others
        - params
      properties:
        window:
          $ref: "#/components/schemas/PaymentMethodAlertWindowDetail"
        baseline:
          $ref: "#/components/schemas/PaymentMethodBaseline"
        episode:
          $ref: "#/components/schemas/PaymentMethodAlertEpisodeDetail"
        others:
          type: array
          items:
            $ref: "#/components/schemas/PaymentMethodAlertOtherMethod"
        params:
          $ref: "#/components/schemas/PaymentMethodAlertParams"
        trigger:
          type: string
          description: >-
            floor (bajo el piso absoluto) o drop (caída contra la base) en la última ventana
            que disparó. Se conserva mientras la ventana vigente no dispara, también al
            resolver; una ventana que vuelve a disparar lo reemplaza.
        last_traffic_at:
          type: string
          format: date-time
          description: Fin de la última ventana con intentos suficientes para medir (sirve para cerrar por falta de tráfico).
    PaymentMethodAlertChannelResult:
      type: object
      description: >-
        Resultado de la campana o del correo en un evento. status usa el mismo
        vocabulario que las Alertas VIP: enviado, desactivado, sin_configurar,
        sin_destinatarios, error o no_enviado (correo con un solo intento; detail
        explica el motivo sin PII y code trae el código SMTP o HTTP cuando lo hay).
      required:
        - status
      properties:
        status:
          type: string
          enum:
            - enviado
            - desactivado
            - sin_configurar
            - sin_destinatarios
            - error
            - no_enviado
        recipients:
          type: integer
          description: Destinatarios a los que se envió.
        detail:
          type: string
        code:
          type: integer
    PaymentMethodAlertWorkDestinationResult:
      type: object
      required:
        - board_id
        - column_id
        - status
      properties:
        board_id:
          type: string
        column_id:
          type: string
        status:
          type: string
          enum:
            - creada
            - error
        card_id:
          type: string
        detail:
          type: string
    PaymentMethodAlertWorkChannelResult:
      type: object
      description: >-
        Resultado de Work al abrir: una tarjeta por destino. status es creadas (al
        menos una), error (ninguna), desactivado o sin_configurar (sin destinos o sin
        autor).
      required:
        - status
      properties:
        status:
          type: string
          enum:
            - creadas
            - error
            - desactivado
            - sin_configurar
        created:
          type: integer
        destinations:
          type: array
          items:
            $ref: "#/components/schemas/PaymentMethodAlertWorkDestinationResult"
    PaymentMethodAlertEventChannels:
      type: object
      description: >-
        Canales de un evento. Al abrir se usan in_app, work y email; al escalar a
        crítica solo in_app; al recuperarse o cerrarse sin tráfico in_app y email.
      properties:
        in_app:
          $ref: "#/components/schemas/PaymentMethodAlertChannelResult"
        work:
          $ref: "#/components/schemas/PaymentMethodAlertWorkChannelResult"
        email:
          $ref: "#/components/schemas/PaymentMethodAlertChannelResult"
    PaymentMethodAlertNotifiedChannels:
      type: object
      description: Resultado de los canales por evento del episodio. Un evento que todavía no ocurrió no aparece.
      properties:
        open:
          $ref: "#/components/schemas/PaymentMethodAlertEventChannels"
        escalate:
          $ref: "#/components/schemas/PaymentMethodAlertEventChannels"
        resolve:
          $ref: "#/components/schemas/PaymentMethodAlertEventChannels"
    PaymentMethodAlert:
      type: object
      description: >-
        Episodio de la alerta de efectividad por método de pago (PRD-55): se abre
        cuando la ventana vigente de un método con intentos suficientes queda bajo el
        piso o cae demasiado contra su base, se actualiza mientras dura, escala a
        crítica (nunca baja) y se cierra solo al recuperarse (recovered) o tras horas
        sin tráfico medible (no_traffic). Solo agregados, sin PII.
      required:
        - id
        - payment_method
        - rule
        - severity
        - status
        - resolution
        - started_at
        - opened_at
        - last_evaluated_at
        - resolved_at
        - worst_rate
        - worst_window_start
        - worst_window_end
        - detail
        - notified_channels
        - reviewed_at
        - reviewed_by
        - review_note
      properties:
        id:
          type: string
          format: uuid
        payment_method:
          type: string
          description: Texto crudo de Centrivo. Vacío = sin método.
        rule:
          type: string
          enum:
            - approval_drop
        severity:
          type: string
          enum:
            - warning
            - critical
        status:
          type: string
          enum:
            - open
            - resolved
        resolution:
          type: string
          nullable: true
          enum:
            - recovered
            - no_traffic
            - null
        started_at:
          type: string
          format: date-time
          description: Inicio de la primera ventana que disparó.
        opened_at:
          type: string
          format: date-time
        last_evaluated_at:
          type: string
          format: date-time
        resolved_at:
          type: string
          format: date-time
          nullable: true
        worst_rate:
          type: number
          nullable: true
          description: Peor efectividad (porcentaje, dos decimales) entre las ventanas que dispararon.
        worst_window_start:
          type: string
          format: date-time
          nullable: true
        worst_window_end:
          type: string
          format: date-time
          nullable: true
        detail:
          $ref: "#/components/schemas/PaymentMethodAlertDetail"
        notified_channels:
          $ref: "#/components/schemas/PaymentMethodAlertNotifiedChannels"
        reviewed_at:
          type: string
          format: date-time
          nullable: true
        reviewed_by:
          type: string
          nullable: true
        review_note:
          type: string
          nullable: true
    PaymentMethodAlertListResponse:
      type: object
      required:
        - alerts
        - open_count
        - can_admin
      properties:
        alerts:
          type: array
          items:
            $ref: "#/components/schemas/PaymentMethodAlert"
        open_count:
          type: integer
          description: Episodios abiertos del casino (independiente del filtro de estado).
        can_admin:
          type: boolean
          description: Si el usuario puede marcar revisadas y editar los ajustes (admin de Data).
    PaymentMethodAlertResponse:
      type: object
      required:
        - alert
      properties:
        alert:
          $ref: "#/components/schemas/PaymentMethodAlert"
    ReviewPaymentMethodAlertRequest:
      type: object
      properties:
        note:
          type: string
          maxLength: 2000
          description: Nota de la revisión (opcional, hasta 2000 caracteres). Vacía se guarda como null.
    PaymentMethodAlertSettings:
      type: object
      description: >-
        Ajustes de la alerta de efectividad por método de pago de un casino. Los
        campos de destinos de Work, autor y correos son los mismos de
        VipAlertSettings. updated_at es null si el casino todavía no guardó ajustes
        (se devuelven los valores por defecto).
      required:
        - enabled
        - window_minutes
        - settle_minutes
        - min_attempts
        - floor_pct
        - critical_pct
        - drop_points
        - baseline_days
        - baseline_min_attempts
        - recovery_pct
        - resolve_min_attempts
        - no_traffic_hours
        - muted_methods
        - channel_in_app
        - channel_work
        - channel_email
        - work_destinations
        - work_author_user_id
        - email_recipients
        - updated_at
      properties:
        enabled:
          type: boolean
          default: true
        window_minutes:
          type: integer
          enum:
            - 30
            - 60
            - 120
          default: 60
          description: Largo de la ventana móvil (instantes, no horas civiles).
        settle_minutes:
          type: integer
          minimum: 0
          maximum: 60
          default: 10
          description: Minutos de asentamiento antes del corte.
        min_attempts:
          type: integer
          minimum: 1
          maximum: 10000
          default: 20
          description: Intentos terminales mínimos de la ventana para evaluar.
        floor_pct:
          type: integer
          minimum: 1
          maximum: 99
          default: 50
          description: Piso absoluto de efectividad; bajo él la ventana dispara.
        critical_pct:
          type: integer
          minimum: 1
          maximum: 99
          default: 35
          description: Bajo este porcentaje la alerta es crítica. Debe ser menor o igual a floor_pct.
        drop_points:
          type: integer
          minimum: 1
          maximum: 99
          default: 25
          description: Caída en puntos contra la base válida que dispara la alerta.
        baseline_days:
          type: integer
          enum:
            - 7
            - 14
            - 28
          default: 7
        baseline_min_attempts:
          type: integer
          minimum: 1
          maximum: 1000000
          default: 100
        recovery_pct:
          type: integer
          minimum: 1
          maximum: 100
          default: 65
          description: Efectividad para cerrar como recuperado. Debe ser mayor o igual a floor_pct.
        resolve_min_attempts:
          type: integer
          minimum: 1
          maximum: 10000
          default: 5
          description: Intentos mínimos de la ventana para cerrar o contar tráfico. Debe ser menor o igual a min_attempts.
        no_traffic_hours:
          type: integer
          minimum: 1
          maximum: 48
          default: 3
        muted_methods:
          type: array
          maxItems: 20
          description: Métodos que nunca abren alerta (texto crudo de Centrivo; vacío = sin método).
          items:
            type: string
            maxLength: 64
        channel_in_app:
          type: boolean
          default: true
        channel_work:
          type: boolean
          default: true
          description: >-
            Por defecto encendido, igual que el vigilante: sin destinos ni autor, el
            evento queda como sin_configurar hasta completarlos.
        channel_email:
          type: boolean
          default: false
        work_destinations:
          type: array
          description: >-
            Destinos de Work (tablero + columna); al abrir se crea una tarjeta en cada uno.
            El PUT admite hasta 20.
          items:
            $ref: "#/components/schemas/VipWorkDestination"
        work_author_user_id:
          type: string
          nullable: true
        email_recipients:
          type: array
          description: >-
            Correos que reciben el aviso. El PUT exige una dirección por elemento, sin
            nombre ni espacios, hasta 20 (los ajustes sembrados desde las Alertas VIP se
            devuelven tal como se copiaron hasta el próximo guardado).
          items:
            type: string
        updated_at:
          type: string
          format: date-time
          nullable: true
    PaymentMethodAlertSettingsResponse:
      type: object
      required:
        - settings
      properties:
        settings:
          $ref: "#/components/schemas/PaymentMethodAlertSettings"
    PutPaymentMethodAlertSettingsRequest:
      type: object
      description: >-
        Cuerpo del PUT. El interruptor, los parámetros y los canales son
        obligatorios (un campo ausente responde 422). Si viene updated_at se ignora.
      required:
        - enabled
        - window_minutes
        - settle_minutes
        - min_attempts
        - floor_pct
        - critical_pct
        - drop_points
        - baseline_days
        - baseline_min_attempts
        - recovery_pct
        - resolve_min_attempts
        - no_traffic_hours
        - channel_in_app
        - channel_work
        - channel_email
      properties:
        enabled:
          type: boolean
        window_minutes:
          type: integer
          enum:
            - 30
            - 60
            - 120
        settle_minutes:
          type: integer
          minimum: 0
          maximum: 60
        min_attempts:
          type: integer
          minimum: 1
          maximum: 10000
        floor_pct:
          type: integer
          minimum: 1
          maximum: 99
        critical_pct:
          type: integer
          minimum: 1
          maximum: 99
          description: Entre 1 y floor_pct.
        drop_points:
          type: integer
          minimum: 1
          maximum: 99
        baseline_days:
          type: integer
          enum:
            - 7
            - 14
            - 28
        baseline_min_attempts:
          type: integer
          minimum: 1
          maximum: 1000000
        recovery_pct:
          type: integer
          minimum: 1
          maximum: 100
          description: Entre floor_pct y 100.
        resolve_min_attempts:
          type: integer
          minimum: 1
          maximum: 10000
          description: Entre 1 y min_attempts.
        no_traffic_hours:
          type: integer
          minimum: 1
          maximum: 48
        muted_methods:
          type: array
          maxItems: 20
          items:
            type: string
            maxLength: 64
        channel_in_app:
          type: boolean
        channel_work:
          type: boolean
        channel_email:
          type: boolean
        work_destinations:
          type: array
          maxItems: 20
          items:
            $ref: "#/components/schemas/VipWorkDestination"
        work_author_user_id:
          type: string
          nullable: true
          description: >-
            Autor de la tarjeta de Work. Si se omite o es null, se usa quien guarda los
            ajustes. Un identificador que no corresponde a un usuario responde 422.
        email_recipients:
          type: array
          maxItems: 20
          description: >-
            Correos que reciben el aviso. Cada elemento es UNA dirección sin nombre ni
            espacios (hasta 254 caracteres); los duplicados sin distinguir mayúsculas
            se descartan.
          items:
            type: string
            format: email
            maxLength: 254
    DataBonusPurpose:
      type: string
      description: >-
        Propósito de negocio de la campaña (reglas de nombre editables; la marca manual gana). La
        marca de prueba va aparte (is_test / test_reason). campana_sportsbook (Campaña Sportsbook) es
        toda campaña con al menos un FreeBet de deporte (bonusType 2 de Centrivo): gana sobre las
        reglas de nombre y pierde solo ante la marca manual.
      enum:
        - regalia_vip
        - regalia_pro_vip
        - bono_deposito_vip
        - reactivacion
        - streamer_partner
        - retencion_fonda
        - registro
        - prueba
        - otro
        - mision
        - tienda
        - ruleta
        - campana
        - regalia
        - campana_sportsbook
    DataBonusVertical:
      type: string
      description: >-
        Vertical de un bono entregado (guardada, migración 20260927120000): sport (FreeBet de deporte o
        Wager cuyo producto en Centrivo es Sport), casino (FreeBet de casino, giros o Wager de Casino),
        cash (saldo real, Special) o unknown (Wager de ambos productos o sin definición cargada, u otro
        tipo).
      enum:
        - casino
        - sport
        - cash
        - unknown
    DataBonusCampaignVertical:
      type: string
      description: >-
        Vertical de una campaña: mixed si entregó bonos de casino y de deporte; casino o sport si solo
        uno de los dos; cash si solo saldo real; unknown si no. Una campaña sin bonos entregados usa las
        verticales de su definición.
      enum:
        - casino
        - sport
        - mixed
        - cash
        - unknown
    DataBonusVerticalGrants:
      type: object
      description: Bonos de casino y de deporte (con los dos en más de cero, la campaña es Mixta).
      required:
        - casino
        - sport
      properties:
        casino:
          type: integer
          format: int64
        sport:
          type: integer
          format: int64
    DataBonusLabel:
      type: string
      description: >-
        Etiqueta del bono: cobrado (terminado con canje mayor que 0), sin_cobro (terminado sin
        canje), vencido, cancelado, activo (nuevo, activo, pendiente, pausado o desconocido).
      enum:
        - cobrado
        - sin_cobro
        - vencido
        - cancelado
        - activo
    DataBonusFamily:
      type: string
      description: Familia del bono (bonusFamily de Centrivo 1–4); otra o nula = other.
      enum:
        - free_spins
        - free_bet
        - rollover
        - special
        - other
    DataBonusLabelCounts:
      type: object
      description: Bonos por etiqueta.
      required:
        - cobrado
        - sin_cobro
        - vencido
        - cancelado
        - activo
      properties:
        cobrado:
          type: integer
          format: int64
        sin_cobro:
          type: integer
          format: int64
        vencido:
          type: integer
          format: int64
        cancelado:
          type: integer
          format: int64
        activo:
          type: integer
          format: int64
    DataBonusSummary:
      type: object
      description: >-
        Resumen del módulo Bonos (PRD-52 ola 4). Montos CLP en unidades menores (minor = pesos).
      required:
        - period
        - kpis
        - by_purpose
        - by_family
        - by_vertical
        - series
        - tabs
        - top_campaigns
      properties:
        period:
          type: object
          description: Período resuelto (días civiles America/Santiago, to inclusivo).
          required:
            - from
            - to
          properties:
            from:
              type: string
              format: date
            to:
              type: string
              format: date
        kpis:
          type: object
          required:
            - given_minor
            - paid_minor
            - grants
            - players
            - labels
            - rollover_open
            - excluded_tests
          properties:
            given_minor:
              type: integer
              format: int64
              description: Regalado = Σ bonus_amount_minor.
            paid_minor:
              type: integer
              format: int64
              description: Cobrado a dinero real = Σ redeemed_amount_minor de los bonos cerrados.
            grants:
              type: integer
              format: int64
            players:
              type: integer
              format: int64
            labels:
              $ref: "#/components/schemas/DataBonusLabelCounts"
            rollover_open:
              type: integer
              format: int64
              description: Bonos con depósito disparador todavía abiertos.
            excluded_tests:
              type: integer
              format: int64
              description: Bonos de prueba o staff del período que no suman (0 con include_tests=true).
        by_purpose:
          type: array
          description: Por propósito, de mayor a menor regalado.
          items:
            type: object
            required:
              - purpose
              - grants
              - given_minor
              - paid_minor
            properties:
              purpose:
                $ref: "#/components/schemas/DataBonusPurpose"
              grants:
                type: integer
                format: int64
              given_minor:
                type: integer
                format: int64
              paid_minor:
                type: integer
                format: int64
        by_family:
          type: array
          description: free_spins, free_bet, rollover y special siempre (en ese orden); other solo si tiene bonos.
          items:
            type: object
            required:
              - family
              - grants
              - players
              - given_minor
              - paid_minor
            properties:
              family:
                $ref: "#/components/schemas/DataBonusFamily"
              grants:
                type: integer
                format: int64
              players:
                type: integer
                format: int64
              given_minor:
                type: integer
                format: int64
              paid_minor:
                type: integer
                format: int64
        by_vertical:
          type: array
          description: >-
            Por vertical del bono, en orden casino, sport, cash, unknown y solo las que tienen bonos:
            bonos, jugadores, campañas con bonos de esa vertical, regalado, cobrado y los Wager (bonos
            con rollover combinado) dentro de ella.
          items:
            type: object
            required:
              - vertical
              - grants
              - players
              - campaigns
              - given_minor
              - paid_minor
              - wager_grants
              - wager_given_minor
            properties:
              vertical:
                $ref: "#/components/schemas/DataBonusVertical"
              grants:
                type: integer
                format: int64
              players:
                type: integer
                format: int64
              campaigns:
                type: integer
                format: int64
              given_minor:
                type: integer
                format: int64
              paid_minor:
                type: integer
                format: int64
              wager_grants:
                type: integer
                format: int64
              wager_given_minor:
                type: integer
                format: int64
        series:
          type: object
          description: >-
            Regalado por vertical y cobrado en baldes de día, semana (lunes a domingo) o mes, en días
            civiles de America/Santiago, con todos los baldes del período (también los vacíos).
          required:
            - granularity
            - buckets
          properties:
            granularity:
              type: string
              enum:
                - day
                - week
                - month
            buckets:
              type: array
              items:
                type: object
                description: >-
                  start y end son los días naturales del balde (inclusivos) aunque el período lo corte;
                  partial es true si el balde se sale del período o todavía no termina (contiene hoy). Las
                  cifras cuentan solo los bonos del período.
                required:
                  - start
                  - end
                  - partial
                  - casino_given_minor
                  - sport_given_minor
                  - cash_given_minor
                  - unknown_given_minor
                  - paid_minor
                  - grants
                properties:
                  start:
                    type: string
                    format: date
                  end:
                    type: string
                    format: date
                  partial:
                    type: boolean
                  casino_given_minor:
                    type: integer
                    format: int64
                  sport_given_minor:
                    type: integer
                    format: int64
                  cash_given_minor:
                    type: integer
                    format: int64
                  unknown_given_minor:
                    type: integer
                    format: int64
                  paid_minor:
                    type: integer
                    format: int64
                  grants:
                    type: integer
                    format: int64
        tabs:
          type: object
          description: >-
            Conteos de pestañas: campañas del listado por tipo (misma regla que
            /data/bonuses/campaigns), bonos de rollover del período, packs de promo codes e ítems
            de /data/bonuses/review.
          required:
            - cross_platform
            - campaign
            - rollover
            - promo_packs
            - review
          properties:
            cross_platform:
              type: integer
              format: int64
            campaign:
              type: integer
              format: int64
            rollover:
              type: integer
              format: int64
            promo_packs:
              type: integer
              format: int64
            review:
              type: integer
              format: int64
        top_campaigns:
          type: array
          description: Hasta 6 campañas con bonos en el período, de mayor a menor regalado.
          items:
            $ref: "#/components/schemas/DataBonusCampaignRow"
    DataBonusCampaignRow:
      type: object
      description: >-
        Campaña con sus cifras (del período en el listado y el resumen; de toda su historia en la
        ficha). created_by y updated_by son operadores de Centrivo (staff), nunca jugadores.
      required:
        - kind
        - id
        - name
        - purpose
        - purpose_manual
        - status_code
        - created_at
        - created_by
        - grants
        - players
        - given_minor
        - paid_minor
        - labels
        - test_reason
        - is_test
        - vertical
        - vertical_grants
      properties:
        kind:
          type: string
          enum:
            - cross_platform
            - campaign
        id:
          type: integer
          format: int64
        name:
          type: string
        purpose:
          $ref: "#/components/schemas/DataBonusPurpose"
        purpose_manual:
          type: boolean
          description: El propósito se fijó a mano.
        status_code:
          type: integer
          description: Estado de la campaña en Centrivo (Draft 0, Scheduled 1, Active 2, Finished 5, Pending 7, InProcess 8).
        created_at:
          type: [string, "null"]
          format: date-time
        created_by:
          type: [string, "null"]
        grants:
          type: integer
          format: int64
        players:
          type: integer
          format: int64
        given_minor:
          type: integer
          format: int64
        paid_minor:
          type: integer
          format: int64
        labels:
          $ref: "#/components/schemas/DataBonusLabelCounts"
        test_reason:
          type: [string, "null"]
          description: Motivo de prueba (manual, nombre, receptores) o revisar (nombre de prueba con receptores reales).
          enum:
            - manual
            - nombre
            - receptores
            - revisar
            - null
        is_test:
          type: boolean
          description: "Prueba efectiva: sus bonos no suman salvo include_tests=true."
        vertical:
          $ref: "#/components/schemas/DataBonusCampaignVertical"
        vertical_grants:
          description: Sus bonos de casino y de deporte entre los que cuentan en la fila (ya filtrados por vertical).
          allOf:
            - $ref: "#/components/schemas/DataBonusVerticalGrants"
    DataBonusCampaignList:
      type: object
      required:
        - total
        - rows
      properties:
        total:
          type: integer
          format: int64
        rows:
          type: array
          items:
            $ref: "#/components/schemas/DataBonusCampaignRow"
    DataBonusCampaignDetail:
      description: "Ficha de una campaña: la fila (cifras de toda su historia) más datos, embudo, depósitos y receptores."
      allOf:
        - $ref: "#/components/schemas/DataBonusCampaignRow"
        - type: object
          required:
            - facts
            - funnel
            - deposits_window
            - repeats
            - recipients
          properties:
            facts:
              type: object
              required:
                - unit_names
                - bonus_names
                - trigger_type
                - starts_at
                - ends_at
                - updated_by
                - stats_synced_at
                - spin_count
                - spin_value_minor
                - wagering_coefficient
              properties:
                unit_names:
                  type: array
                  items:
                    type: string
                bonus_names:
                  type: array
                  items:
                    type: string
                trigger_type:
                  type: [integer, "null"]
                  description: Disparador del catálogo normal (0 sin disparador, 3 depósito, 1 y 6 otros); null en cross-platform.
                starts_at:
                  type: [string, "null"]
                  format: date-time
                ends_at:
                  type: [string, "null"]
                  format: date-time
                updated_by:
                  type: [string, "null"]
                stats_synced_at:
                  type: [string, "null"]
                  format: date-time
                spin_count:
                  type: [integer, "null"]
                  format: int64
                  description: Giros por bono más frecuente.
                spin_value_minor:
                  type: [integer, "null"]
                  format: int64
                  description: Valor por giro más frecuente.
                wagering_coefficient:
                  type: [number, "null"]
                  description: Coeficiente de rollover más frecuente (null = sin rollover).
            funnel:
              type: object
              description: Qué pasó con los bonos (activated = con fecha de activación).
              required:
                - delivered
                - activated
                - cobrado
                - sin_cobro
                - vencido
                - cancelado
                - activo
              properties:
                delivered:
                  type: integer
                  format: int64
                activated:
                  type: integer
                  format: int64
                cobrado:
                  type: integer
                  format: int64
                sin_cobro:
                  type: integer
                  format: int64
                vencido:
                  type: integer
                  format: int64
                cancelado:
                  type: integer
                  format: int64
                activo:
                  type: integer
                  format: int64
            deposits_window:
              type: object
              description: Depósitos pagados de los receptores `days` días antes y después del inicio de la campaña.
              required:
                - days
                - before
                - after
              properties:
                days:
                  type: integer
                before:
                  $ref: "#/components/schemas/DataBonusDepositWindowSide"
                after:
                  $ref: "#/components/schemas/DataBonusDepositWindowSide"
            repeats:
              type: object
              required:
                - players_with_many
                - max_per_player
              properties:
                players_with_many:
                  type: integer
                  format: int64
                max_per_player:
                  type: integer
                  format: int64
            recipients:
              type: object
              description: Receptores (solo Player ID) ordenados por cobrado; total = jugadores distintos.
              required:
                - total
                - rows
              properties:
                total:
                  type: integer
                  format: int64
                rows:
                  type: array
                  items:
                    type: object
                    required:
                      - player_ref
                      - grants
                      - labels
                      - given_minor
                      - paid_minor
                      - deposits_after_minor
                    properties:
                      player_ref:
                        type: string
                      grants:
                        type: integer
                        format: int64
                      labels:
                        $ref: "#/components/schemas/DataBonusLabelCounts"
                      given_minor:
                        type: integer
                        format: int64
                      paid_minor:
                        type: integer
                        format: int64
                      deposits_after_minor:
                        type: integer
                        format: int64
                        description: Depositado (pagado) en los `days` días posteriores al inicio.
    DataBonusDepositWindowSide:
      type: object
      required:
        - amount_minor
        - count
        - players
      properties:
        amount_minor:
          type: integer
          format: int64
        count:
          type: integer
          format: int64
        players:
          type: integer
          format: int64
    DataBonusRollover:
      type: object
      required:
        - kpis
        - by_vertical
        - campaigns
      properties:
        kpis:
          type: object
          required:
            - grants
            - deposit_minor
            - given_minor
            - turnover_required_minor
            - paid_minor
            - paid_grants
            - canceled_by_player
            - open
            - open_remaining_turnover_minor
            - deposit_found
          properties:
            grants:
              type: integer
              format: int64
            deposit_minor:
              type: integer
              format: int64
              description: Depositado que activó los bonos.
            given_minor:
              type: integer
              format: int64
            turnover_required_minor:
              type: integer
              format: int64
              description: Rollover exigido.
            paid_minor:
              type: integer
              format: int64
            paid_grants:
              type: integer
              format: int64
              description: Bonos cobrados (cumplieron).
            canceled_by_player:
              type: integer
              format: int64
            open:
              type: integer
              format: int64
            open_remaining_turnover_minor:
              type: integer
              format: int64
            deposit_found:
              type: integer
              format: int64
              description: Bonos cuyo depósito disparador está en los depósitos de Data.
        by_vertical:
          type: array
          description: >-
            Comparación Casino vs Deporte: por vertical del bono, en orden casino, sport, cash, unknown y
            solo las que tienen bonos con depósito.
          items:
            type: object
            required:
              - vertical
              - grants
              - deposit_minor
              - given_minor
              - turnover_required_minor
              - paid_minor
              - paid_grants
              - canceled
            properties:
              vertical:
                $ref: "#/components/schemas/DataBonusVertical"
              grants:
                type: integer
                format: int64
              deposit_minor:
                type: integer
                format: int64
              given_minor:
                type: integer
                format: int64
              turnover_required_minor:
                type: integer
                format: int64
              paid_minor:
                type: integer
                format: int64
              paid_grants:
                type: integer
                format: int64
                description: Bonos cobrados (cumplieron).
              canceled:
                type: integer
                format: int64
        campaigns:
          type: array
          description: Campañas con bono por depósito, de mayor a menor depositado.
          items:
            type: object
            required:
              - kind
              - id
              - name
              - grants
              - deposit_minor
              - given_minor
              - wagering_coefficient
              - paid_minor
              - cobrado
              - canceled
              - bonuses
              - vertical
              - vertical_grants
            properties:
              kind:
                type: string
                enum:
                  - cross_platform
                  - campaign
              id:
                type: integer
                format: int64
              name:
                type: string
              grants:
                type: integer
                format: int64
              deposit_minor:
                type: integer
                format: int64
              given_minor:
                type: integer
                format: int64
              wagering_coefficient:
                type: [number, "null"]
              paid_minor:
                type: integer
                format: int64
              cobrado:
                type: integer
                format: int64
              canceled:
                type: integer
                format: int64
              bonuses:
                type: array
                description: "Bonos del catálogo con depósito (más bonos primero), para /data/bonuses/rollover/bonus/{bonusId}."
                items:
                  type: object
                  required:
                    - bonus_id
                    - name
                    - grants
                    - vertical
                  properties:
                    bonus_id:
                      type: integer
                      format: int64
                    name:
                      type: string
                    grants:
                      type: integer
                      format: int64
                    vertical:
                      $ref: "#/components/schemas/DataBonusVertical"
              vertical:
                $ref: "#/components/schemas/DataBonusCampaignVertical"
              vertical_grants:
                description: Sus bonos con depósito de casino y de deporte del período.
                allOf:
                  - $ref: "#/components/schemas/DataBonusVerticalGrants"
    DataBonusRolloverBonus:
      type: object
      required:
        - bonus_id
        - name
        - vertical
        - total
        - rows
        - siblings
      properties:
        bonus_id:
          type: integer
          format: int64
        name:
          type: string
        vertical:
          $ref: "#/components/schemas/DataBonusVertical"
        total:
          type: integer
          format: int64
        siblings:
          type: array
          description: >-
            Bonos con depósito de la misma campaña, incluido este (más bonos primero), para verlos lado a
            lado: bonos, cobrado, cuántos cumplieron, cuántos se cancelaron y el rollover más frecuente.
          items:
            type: object
            required:
              - bonus_id
              - name
              - vertical
              - grants
              - paid_minor
              - paid_grants
              - canceled
              - wagering_coefficient
            properties:
              bonus_id:
                type: integer
                format: int64
              name:
                type: string
              vertical:
                $ref: "#/components/schemas/DataBonusVertical"
              grants:
                type: integer
                format: int64
              paid_minor:
                type: integer
                format: int64
              paid_grants:
                type: integer
                format: int64
              canceled:
                type: integer
                format: int64
              wagering_coefficient:
                type: [number, "null"]
        rows:
          type: array
          items:
            type: object
            required:
              - player_ref
              - player_bonus_id
              - triggered_at
              - deposit_minor
              - deposit_ref
              - deposit_found
              - given_minor
              - turnover_required_minor
              - turnover_remaining_minor
              - label
              - paid_minor
              - canceled_by
            properties:
              player_ref:
                type: string
              player_bonus_id:
                type: integer
                format: int64
              triggered_at:
                type: [string, "null"]
                format: date-time
              deposit_minor:
                type: integer
                format: int64
              deposit_ref:
                type: string
                description: ID del depósito disparador; enmascarado (••••1234) salvo con permiso de PII completa.
              deposit_found:
                type: boolean
              given_minor:
                type: integer
                format: int64
              turnover_required_minor:
                type: [integer, "null"]
                format: int64
              turnover_remaining_minor:
                type: [integer, "null"]
                format: int64
              label:
                $ref: "#/components/schemas/DataBonusLabel"
              paid_minor:
                type: integer
                format: int64
              canceled_by:
                type: [string, "null"]
                enum:
                  - player
                  - system
                  - null
    DataBonusPromoPackRow:
      type: object
      required:
        - id
        - name
        - type
        - used
        - codes
        - campaign_id
        - campaign_name
        - uses_loaded
        - uses_with_bonus
        - paid_minor
        - last_used_at
        - discarded
        - discarded_at
        - discard_note
      properties:
        id:
          type: integer
          format: int64
        name:
          type: string
        last_used_at:
          type: [string, "null"]
          format: date-time
          description: Último canje con fecha (null si el pack no tiene canjes con fecha).
        discarded:
          type: boolean
          description: Marcado a mano como descartado (no cuenta en los KPIs).
        discarded_at:
          type: [string, "null"]
          format: date-time
        discard_note:
          type: [string, "null"]
        type:
          type: string
          description: Tipo de pack (other = código desconocido de Centrivo).
          enum:
            - single
            - multiple
            - external
            - other
        used:
          type: integer
          format: int64
        codes:
          type: integer
          format: int64
        campaign_id:
          type: [integer, "null"]
          format: int64
        campaign_name:
          type: [string, "null"]
        uses_loaded:
          type: integer
          format: int64
        uses_with_bonus:
          type: integer
          format: int64
        paid_minor:
          type: integer
          format: int64
    DataBonusPromoPacks:
      type: object
      required:
        - kpis
        - total
        - rows
      properties:
        kpis:
          type: object
          description: Los packs del casino sin los descartados y sin filtros; discarded cuenta los descartados.
          required:
            - packs
            - used
            - codes
            - by_type
            - campaigns
            - discarded
          properties:
            discarded:
              type: integer
              format: int64
            packs:
              type: integer
              format: int64
            used:
              type: integer
              format: int64
            codes:
              type: integer
              format: int64
            by_type:
              type: object
              required:
                - single
                - multiple
                - external
              properties:
                single:
                  type: integer
                  format: int64
                multiple:
                  type: integer
                  format: int64
                external:
                  type: integer
                  format: int64
            campaigns:
              type: integer
              format: int64
        total:
          type: integer
          format: int64
        rows:
          type: array
          items:
            $ref: "#/components/schemas/DataBonusPromoPackRow"
    DataBonusPromoPackDiscard:
      type: object
      required:
        - pack
      properties:
        pack:
          $ref: "#/components/schemas/DataBonusPromoPackRow"
    DataBonusPromoUses:
      type: object
      required:
        - pack
        - total
        - rows
      properties:
        pack:
          $ref: "#/components/schemas/DataBonusPromoPackRow"
        total:
          type: integer
          format: int64
        rows:
          type: array
          items:
            type: object
            required:
              - use_id
              - code
              - player_ref
              - used_at
              - player_bonus_id
              - grant_label
              - paid_minor
              - is_grant_owner
            properties:
              use_id:
                type: integer
                format: int64
              code:
                type: [string, "null"]
              player_ref:
                type: string
              used_at:
                type: [string, "null"]
                format: date-time
              player_bonus_id:
                type: [integer, "null"]
                format: int64
              grant_label:
                description: Etiqueta del bono generado; null si el uso no generó bono o Data aún no lo tiene.
                oneOf:
                  - $ref: "#/components/schemas/DataBonusLabel"
                  - type: "null"
              paid_minor:
                type: integer
                format: int64
              is_grant_owner:
                type: boolean
                description: El uso dueño del bono (el primero); solo ese cuenta al sumar.
    DataBonusPromoCode:
      type: object
      required:
        - code
        - status
        - validity
        - uses
        - players
        - packs
        - total
        - rows
      properties:
        code:
          type: string
          description: El código buscado, normalizado (sin espacios al borde y en mayúsculas).
        status:
          type: string
          enum: [redeemed, not_redeemed, not_found]
        validity:
          type: string
          enum: [vigente, vencido, programado, sin_dato]
        uses:
          type: integer
          format: int64
          description: Canjes distintos (un canje con varios bonos cuenta una vez).
        players:
          type: integer
          format: int64
        packs:
          type: array
          items:
            type: object
            required:
              - id
              - name
              - type
              - campaign_id
              - campaign_name
              - campaign_status_code
              - campaign_starts_at
              - campaign_ends_at
              - validity
              - uses_synced_at
            properties:
              id:
                type: integer
                format: int64
              name:
                type: string
              type:
                type: string
              campaign_id:
                type: [integer, "null"]
                format: int64
              campaign_name:
                type: [string, "null"]
              campaign_status_code:
                type: [integer, "null"]
                description: Estado de la campaña en Centrivo (1 programada, 2 activa, 5 terminada, 7 pendiente, 8 en proceso).
              campaign_starts_at:
                type: [string, "null"]
                format: date-time
              campaign_ends_at:
                type: [string, "null"]
                format: date-time
              validity:
                type: string
                enum: [vigente, vencido, programado, sin_dato]
              uses_synced_at:
                type: [string, "null"]
                format: date-time
                description: Última lectura de los canjes del pack desde Centrivo.
        total:
          type: integer
          format: int64
          description: Filas de la lista (una por bono de cada canje).
        rows:
          type: array
          items:
            type: object
            required:
              - use_id
              - pack_id
              - pack_name
              - code
              - player_ref
              - used_at
              - player_bonus_id
              - grant_label
              - given_minor
              - paid_minor
            properties:
              use_id:
                type: integer
                format: int64
              pack_id:
                type: integer
                format: int64
              pack_name:
                type: string
              code:
                type: [string, "null"]
              player_ref:
                type: string
              used_at:
                type: [string, "null"]
                format: date-time
              player_bonus_id:
                type: [integer, "null"]
                format: int64
              grant_label:
                description: Etiqueta del bono generado; null si el canje no generó bono o Data aún no lo tiene.
                oneOf:
                  - $ref: "#/components/schemas/DataBonusLabel"
                  - type: "null"
              given_minor:
                type: integer
                format: int64
              paid_minor:
                type: integer
                format: int64
    DataBonusReview:
      type: object
      required:
        - items
      properties:
        items:
          type: array
          items:
            type: object
            required:
              - kind
              - title
              - detail
              - refs
            properties:
              kind:
                type: string
                enum:
                  - expired_share
                  - recreated
                  - uses_without_bonus
                  - pack_discardable
                  - pack_discarded_with_redemptions
                  - test_by_recipients
                  - test_review
              title:
                type: string
              detail:
                type: string
              refs:
                type: array
                description: Campañas (kind cross_platform o campaign) o packs (kind promo_pack), hasta 50.
                items:
                  type: object
                  required:
                    - kind
                    - id
                    - name
                  properties:
                    kind:
                      type: string
                      enum:
                        - cross_platform
                        - campaign
                        - promo_pack
                    id:
                      type: integer
                      format: int64
                    name:
                      type: string
    DataPlayerBonuses:
      type: object
      description: Sección Bonos de la ficha del jugador. Montos CLP en unidades menores.
      required:
        - kpis
        - by_family
        - by_vertical
        - played
        - total
        - rows
      properties:
        kpis:
          type: object
          description: Sin los bonos de prueba.
          required:
            - grants
            - campaigns
            - given_minor
            - paid_minor
            - labels
            - first_at
            - last_at
            - with_promo_code
          properties:
            grants:
              type: integer
              format: int64
            campaigns:
              type: integer
              format: int64
            given_minor:
              type: integer
              format: int64
            paid_minor:
              type: integer
              format: int64
            labels:
              $ref: "#/components/schemas/DataBonusLabelCounts"
            first_at:
              type: [string, "null"]
              format: date-time
            last_at:
              type: [string, "null"]
              format: date-time
            with_promo_code:
              type: integer
              format: int64
        by_family:
          type: array
          items:
            type: object
            required:
              - family
              - grants
              - used
              - given_minor
              - paid_minor
              - vencido
              - cancelado
              - spins
              - spins_used
              - spin_value_max_minor
            properties:
              family:
                $ref: "#/components/schemas/DataBonusFamily"
              grants:
                type: integer
                format: int64
              used:
                type: integer
                format: int64
                description: Bonos activados por el jugador.
              given_minor:
                type: integer
                format: int64
              paid_minor:
                type: integer
                format: int64
              vencido:
                type: integer
                format: int64
              cancelado:
                type: integer
                format: int64
              spins:
                type: integer
                format: int64
              spins_used:
                type: integer
                format: int64
              spin_value_max_minor:
                type: integer
                format: int64
        by_vertical:
          type: array
          description: >-
            Por vertical y tipo, sin los bonos de prueba: en orden casino, sport, cash, unknown (solo las
            que tienen bonos) con su subtotal y sus tipos por bonusType de Centrivo (free_spins giros,
            free_bet FreeBet, wager rollover combinado, cash saldo real, other el resto; solo los que tienen
            bonos). used = bonos activados; spins/spins_used = giros entregados y de bonos activados.
          items:
            type: object
            required:
              - vertical
              - grants
              - used
              - given_minor
              - paid_minor
              - types
            properties:
              vertical:
                $ref: "#/components/schemas/DataBonusVertical"
              grants:
                type: integer
                format: int64
              used:
                type: integer
                format: int64
              given_minor:
                type: integer
                format: int64
              paid_minor:
                type: integer
                format: int64
              types:
                type: array
                items:
                  type: object
                  required:
                    - type
                    - grants
                    - used
                    - given_minor
                    - paid_minor
                    - spins
                    - spins_used
                    - vencido
                    - cancelado
                  properties:
                    type:
                      type: string
                      enum:
                        - free_spins
                        - free_bet
                        - wager
                        - cash
                        - other
                    grants:
                      type: integer
                      format: int64
                    used:
                      type: integer
                      format: int64
                    given_minor:
                      type: integer
                      format: int64
                    paid_minor:
                      type: integer
                      format: int64
                    spins:
                      type: integer
                      format: int64
                    spins_used:
                      type: integer
                      format: int64
                    vencido:
                      type: integer
                      format: int64
                    cancelado:
                      type: integer
                      format: int64
        played:
          type: object
          required:
            - since
            - casino
            - sport
          properties:
            since:
              type: [string, "null"]
              format: date
            casino:
              $ref: "#/components/schemas/DataPlayerBonusPlayed"
            sport:
              $ref: "#/components/schemas/DataPlayerBonusPlayed"
        total:
          type: integer
          format: int64
        rows:
          type: array
          items:
            type: object
            required:
              - player_bonus_id
              - campaign_kind
              - campaign_id
              - campaign_name
              - purpose
              - family
              - delivered_at
              - label
              - given_minor
              - paid_minor
              - promo_code
              - is_test
              - vertical
            properties:
              player_bonus_id:
                type: integer
                format: int64
              campaign_kind:
                type: string
                enum:
                  - cross_platform
                  - campaign
              campaign_id:
                type: integer
                format: int64
              campaign_name:
                type: string
              purpose:
                $ref: "#/components/schemas/DataBonusPurpose"
              family:
                $ref: "#/components/schemas/DataBonusFamily"
              delivered_at:
                type: string
                format: date-time
              label:
                $ref: "#/components/schemas/DataBonusLabel"
              given_minor:
                type: integer
                format: int64
              paid_minor:
                type: integer
                format: int64
              promo_code:
                type: [string, "null"]
              is_test:
                type: boolean
              vertical:
                $ref: "#/components/schemas/DataBonusVertical"
    DataPlayerBonusPlayed:
      type: object
      description: Lo jugado con bono (apuestas, apostado y ganado).
      required:
        - bets
        - bet_minor
        - win_minor
      properties:
        bets:
          type: integer
          format: int64
        bet_minor:
          type: integer
          format: int64
        win_minor:
          type: integer
          format: int64
  responses:
    KickForbidden:
      description: Solo el owner streamer activo puede administrar este canal
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/ProblemBase"
    KickUnavailable:
      description: Kick, la verificación RSA o la auditoría no están disponibles
      content:
        application/problem+json:
          schema:
            oneOf:
              - $ref: "#/components/schemas/ProblemBase"
              - $ref: "#/components/schemas/AuditUnavailableProblem"
    YouTubeForbidden:
      description: Solo el owner streamer activo puede administrar esta conexión
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/YouTubeForbiddenProblem"
    YouTubeUnavailable:
      description: Las credenciales OAuth no están configuradas o la auditoría no está disponible
      content:
        application/problem+json:
          schema:
            oneOf:
              - $ref: "#/components/schemas/YouTubeNotConfiguredProblem"
              - $ref: "#/components/schemas/AuditUnavailableProblem"
    InvalidCredentials:
      description: Credenciales o token de invitación inválidos, sin enumerar cuentas
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/InvalidCredentialsProblem"
          example:
            type: https://api.convray.com/problems/invalid-credentials
            title: No se pudo iniciar sesión
            status: 401
            detail: El email o la contraseña no son válidos.
            instance: /api/v1/auth/sessions
            code: invalid_credentials
            request_id: req_01
    InvalidSession:
      description: Sesión administrativa o viewer ausente, inválida, expirada o revocada
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/InvalidSessionProblem"
          example:
            type: https://api.convray.com/problems/invalid-session
            title: La sesión no es válida
            status: 401
            detail: Inicia sesión nuevamente.
            instance: /api/v1/me
            code: invalid_session
            request_id: req_01
    PermissionDenied:
      description: Actor administrativo autenticado sin permiso para la acción
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/PermissionDeniedProblem"
          example:
            type: https://api.convray.com/problems/permission-denied
            title: Acción no permitida
            status: 403
            detail: No tienes permiso para realizar esta acción.
            instance: /api/v1/auth/sessions/current
            code: permission_denied
            request_id: req_01
    PermissionOrCSRFRejected:
      description: Actor sin permiso o Origin del navegador rechazado
      content:
        application/problem+json:
          schema:
            oneOf:
              - $ref: "#/components/schemas/PermissionDeniedProblem"
              - $ref: "#/components/schemas/CSRFRejectedProblem"
    CSRFRejected:
      description: El Origin del navegador no coincide con el frontend autorizado
      headers:
        Cache-Control:
          $ref: "#/components/headers/NoStore"
        X-Request-Id:
          $ref: "#/components/headers/RequestId"
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/CSRFRejectedProblem"
          example:
            type: https://api.convray.com/problems/csrf-rejected
            title: Solicitud rechazada
            status: 403
            detail: El origen de la solicitud no está autorizado.
            instance: /api/v1/auth/sessions/refresh
            code: csrf_rejected
            request_id: req_01
    RegistrationForbidden:
      description: Alta pública cerrada, registro casino prohibido u Origin del navegador rechazado
      headers:
        Cache-Control:
          $ref: "#/components/headers/NoStore"
        X-Request-Id:
          $ref: "#/components/headers/RequestId"
      content:
        application/problem+json:
          schema:
            oneOf:
              - $ref: "#/components/schemas/PublicRegistrationClosedProblem"
              - $ref: "#/components/schemas/PublicCasinoRegistrationForbiddenProblem"
              - $ref: "#/components/schemas/CSRFRejectedProblem"
    ResourceNotFound:
      description: |
        El recurso no existe o pertenece a un tenant/deal no visible. La
        respuesta no permite distinguir ambos casos.
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/ResourceNotFoundProblem"
          example:
            type: https://api.convray.com/problems/resource-not-found
            title: Recurso no encontrado
            status: 404
            detail: No se encontró el recurso solicitado.
            instance: /api/v1/auth/sessions/current
            code: resource_not_found
            request_id: req_01
    PartnerRegistrationConflict:
      description: Conflicto de identidad, contexto, consumo o idempotencia del onboarding
      headers:
        Cache-Control:
          $ref: "#/components/headers/NoStore"
        X-Request-Id:
          $ref: "#/components/headers/RequestId"
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/PartnerRegistrationConflictProblem"
          examples:
            emailTaken:
              value:
                type: https://api.convray.com/problems/email-taken
                title: No se pudo crear la cuenta
                status: 409
                detail: Ya existe una cuenta para ese email.
                instance: /api/v1/partner-registrations
                code: email_taken
                request_id: req_01
            partnerSlugTaken:
              value:
                type: https://api.convray.com/problems/partner-slug-taken
                title: No se pudo crear la cuenta
                status: 409
                detail: El identificador partner no está disponible.
                instance: /api/v1/partner-registrations
                code: partner_slug_taken
                request_id: req_01
    ValidationFailed:
      description: El payload no cumple el contrato
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/ValidationFailedProblem"
    VersionConflict:
      description: Estado, revisión, términos, saldo o idempotencia incompatibles
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/ProblemBase"
    IdempotencyConflict:
      description: La Idempotency-Key ya fue usada con un digest distinto
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/IdempotencyConflictProblem"
          example:
            type: https://api.convray.com/problems/idempotency-key-conflict
            title: La clave de idempotencia ya fue utilizada
            status: 409
            detail: Usa una nueva Idempotency-Key para un payload diferente.
            instance: /api/v1/partner/deals/541941f5-b6e5-4d18-8f76-9c81889661fc/tracking-links
            code: idempotency_key_conflict
            request_id: req_01
    RelationshipRuleViolation:
      description: Validación o restriction activa impiden crear el deal
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/RelationshipRuleViolationProblem"
    TrackingLinkNotFound:
      description: Token inválido o link no activo, sin distinguir el motivo
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/TrackingLinkNotFoundProblem"
          example:
            type: https://api.convray.com/problems/tracking-link-not-found
            title: Link no disponible
            status: 404
            detail: No se encontró un link de tracking activo.
            instance: /r/invalid-token
            code: tracking_link_not_found
            request_id: req_01
    TrackingRedirectUnavailable:
      description: La configuración validada o la persistencia durable no están disponibles
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/TrackingRedirectUnavailableProblem"
          example:
            type: https://api.convray.com/problems/click-persistence-unavailable
            title: El redirect no está disponible temporalmente
            status: 503
            detail: No se pudo confirmar el click de forma durable.
            instance: /r/AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8
            code: click_persistence_unavailable
            request_id: req_01
    EmailOTPExpired:
      description: El desafío del código venció, ya se usó o agotó sus intentos; hay que volver a iniciar sesión
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/ProblemBase"
    RateLimited:
      description: Límite temporal excedido
      headers:
        Cache-Control:
          $ref: "#/components/headers/PrivateNoStore"
        Retry-After:
          description: Segundos hasta permitir otro intento
          schema:
            type: integer
            minimum: 1
        X-Request-Id:
          $ref: "#/components/headers/RequestId"
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/RateLimitedProblem"
    RetentionProblem:
      description: >-
        Error de la API de retención en application/problem+json. code estable (invalid_parameter,
        invalid_cursor, invalid_token, forbidden, not_found, ambiguous_ref, method_not_allowed, internal; el 503
        suma unavailable e integration_paused); instance es el
        patrón de la ruta y los mensajes nunca llevan IDs, montos, códigos, correos ni hashes.
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/ProblemBase"
    IntegrationPaused:
      description: >-
        La API de soporte del tenant está en pausa (interruptor de la tarjeta en Data): code
        integration_paused. El token sigue vivo y vuelve a responder cuando se reanude; no lleva
        Retry-After porque la pausa la levanta una persona. La llamada queda auditada.
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/ProblemBase"
    RetentionUnavailable:
      description: >-
        Interruptor apagado, base no disponible, statement_timeout o, en email-check, PII que no se puede
        descifrar (code unavailable): reintentar después de Retry-After. También la integración del token
        en pausa desde Data (code integration_paused): el token sigue vivo y vuelve a responder cuando se
        reanude; esa respuesta no lleva Retry-After, queda auditada y no consume cupo.
      headers:
        Retry-After:
          description: Segundos hasta reintentar
          schema:
            type: integer
            minimum: 1
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/ProblemBase"
    AuditUnavailable:
      description: No se pudo persistir evidencia; la operación no continuó
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/AuditUnavailableProblem"
    FairnessUnavailable:
      description: Fairness o auditoría no disponibles; la ronda no fue confirmada
      content:
        application/problem+json:
          schema:
            oneOf:
              - $ref: "#/components/schemas/ProblemBase"
              - $ref: "#/components/schemas/AuditUnavailableProblem"
