--- type: SPEC asset_id: SPEC-REB-RClub-PromotoresAfiliados-PlanImplementacion-v01 version: v01 status: Draft — propuesta para revisión de Gustavo antes de programar owner: Ángeles Bravo sherpa: Jay proyecto: RebelocityClub responde_a: SPEC-EL-RebelocityClub-PromotoresAfiliados-SolicitudCampos-v01 referencia_visual: MOCKUP-RCH-Eventos-ModalPromotor-v01.html fecha_creacion: 2026-07-22 --- # Plan de implementación — Modal "Agregar promotor" (Eventos · S2 Modalidades) ## Contexto Gustavo reportó que `MOCKUP-RCH-Eventos-v01.html` solo muestra la fila de un promotor ya creado (`@rebelocity_oficial`), pero el botón "+ Agregar promotor" no tiene modal asociado — el formulario de alta nunca se diseñó. Este documento propone el modelo de datos, las validaciones y las reglas de negocio para poder construirlo, y viene acompañado de `MOCKUP-RCH-Eventos-ModalPromotor-v01.html` con la vista del modal. **Cómo leer este documento:** cada campo/regla está marcado como **✅ Confirmado** (ya visible en la fila de ejemplo del mockup original) o **🟡 Propuesta** (no existía antes, es una decisión nueva que se está proponiendo aquí — revisar y corregir si no aplica). --- ## 0. Corrección — no existe tabla `users` genérica Gustavo reportó (`OUT-EL-MensajeAngeles-BuscadorUsuarioPromotores-v01.md`, 2026-07-23) que el `user_id UUID` y el endpoint `GET /usuarios/buscar?q=` propuestos en la v01 de este documento **no son construibles tal cual** — el proyecto no tiene una tabla `users` genérica. La identidad real vive repartida en `colaborador` (wikiname), `perfil` (nombre, email, foto) y `puesto` (id operativo que ya usan el Directorio y los 4 chats). Mismo patrón que ya resolvió los bugs de mensajería en tiempo real. Esta sección corrige esa parte del modelo. El resto del documento (comisión, código de referido, vigencia, modalidades, borrado) no se ve afectado y sigue vigente tal como está. ### Decisiones (Ángeles, 2026-07-23) | # | Pregunta | Decisión | |---|---|---| | 1 | ¿Contra qué universo de usuarios busca el autocomplete? | Reutilizar el mismo query que ya usa el Directorio de Miembros (`colaborador`+`perfil`+`puesto`, `origen = 169`). Incluye atletas, coaches y clubes — no hay que construir nada nuevo. | | 2 | ¿Qué identificador guarda `event_promotores`? | `puesto` (id operativo), no `wikiname`. El wikiname es solo la etiqueta visible en el resultado de búsqueda. | | 3 | ¿Se excluye a alguien de los resultados? | Sí — excluir al organizador del propio evento (evita que se autoasigne como su propio promotor/referido). | | 4 | ¿Por qué campos busca el autocomplete? | Nombre + wikiname. Sin teléfono ni email — no se exponen en ningún otro punto de la plataforma. | | 5 | ¿Quién puede ser promotor? | Cualquier miembro (atletas, coaches, clubes) — mismo universo que el Directorio, sin restringir el rol. | | 6 | Un usuario ya agregado como promotor de un evento, ¿sigue apareciendo en el buscador de ESE evento? | No, se oculta solo para ese evento (evita duplicarlo). Sigue apareciendo normal al buscar promotores para otros eventos. | --- ## 1. Respuestas a las preguntas abiertas de la SPEC original | # | Pregunta | Respuesta propuesta | |---|---|---| | 1 | ¿El modal cambia según el evento/modalidad? | 🟡 No. El modal es el mismo formulario siempre. Lo único condicional es si el promotor aplica a todas las modalidades o solo a algunas (ver §3.6) — eso se resuelve dentro del propio modal, no antes de abrirlo. | | 2 | Lista completa de campos | Ver tabla en §2 — se agregaron 3 campos nuevos a los 5 ya visibles: **tipo de promotor** (usuario de la plataforma vs. colaborador externo), **vigencia** (opcional) y **notas internas** (opcional). | | 3 | Validaciones por campo | Ver columna "Validación" en §2. | | 4 | ¿Aplica a todas las modalidades o se puede restringir? | 🟡 Se puede restringir. Default: aplica a todas (toggle activado). Si se desactiva, se debe elegir al menos una modalidad. | | 5 | ¿Qué pasa al eliminar un promotor con usos ya generados? | 🟡 Mismo patrón que las modalidades con `cupos_vendidos > 0`: se bloquea el borrado físico y se ofrece "Desactivar" en su lugar (ver §4.2). | --- ## 2. Modelo de datos | Campo | Tipo | Requerido | Validación | Default | Estado | |---|---|---|---|---|---| | `promotor_id` | UUID | — | autogenerado (PK) | — | 🟡 | | `evento_id` | UUID (FK) | Sí | debe existir | — | 🟡 | | `tipo_promotor` | enum: `usuario_plataforma` \| `externo` | Sí | — | `usuario_plataforma` | 🟡 | | `puesto_id` | id operativo (FK), nullable | Condicional | requerido si `tipo_promotor = usuario_plataforma`; debe existir en `puesto`, resuelto vía join `colaborador`+`perfil`+`puesto` (ver §0) | null | 🟡 | | `nombre_externo` | string, nullable | Condicional | requerido si `tipo_promotor = externo`, máx. 60 caracteres | null | 🟡 | | `canal` | enum: `instagram` \| `facebook` \| `tiktok` \| `whatsapp` \| `otro` | Sí | select cerrado (no texto libre) | `instagram` | ✅ | | `codigo_referido` | string | Sí | 4–20 caracteres, alfanumérico en mayúsculas, **único dentro del mismo `evento_id`** (no global) | sugerido automático, editable | ✅ (campo) / 🟡 (regla de unicidad) | | `comision_pct` | decimal | Sí | 0–100, puede ser 0 | 5 | ✅ (campo) / 🟡 (rango) | | `limite_usos` | int, nullable | No | entero positivo; vacío = ilimitado (mismo patrón que "Cupo máximo" de modalidades) | null | ✅ (campo) / 🟡 (regla vacío=ilimitado) | | `usos_actuales` | int | — | contador de solo lectura, no editable desde el modal | 0 | 🟡 | | `aplica_todas_modalidades` | boolean | Sí | — | true | 🟡 | | `modalidades_aplicables` | array\, nullable | Condicional | requerido (mínimo 1) si `aplica_todas_modalidades = false` | [] | 🟡 | | `vigencia_desde` | date, nullable | No | si se define, debe ser ≤ `vigencia_hasta` | null | 🟡 | | `vigencia_hasta` | date, nullable | No | si se define, debe ser ≥ fecha actual al crear | null | 🟡 | | `notas_internas` | text, nullable | No | máx. 300 caracteres, nunca visible al promotor ni al público | null | 🟡 | | `estado` | enum: `activo` \| `inactivo` | Sí | se puede crear ya inactivo | `activo` | ✅ (campo) / 🟡 (editable al crear) | | `link_personalizado` | string, derivado (solo lectura) | — | `{base_url}/e/{evento_slug}?ref={codigo_referido}`, se regenera si cambia el código | — | 🟡 | | `created_at` / `updated_at` | timestamp | — | automático | — | — | --- ## 3. Reglas de negocio ### 3.1 Unicidad del código de referido Único **por evento**, no global — dos eventos distintos pueden reutilizar el mismo código (ej. `REB2026` en dos carreras diferentes). Validar con debounce al perder foco en el campo, contra los códigos ya usados en ese `evento_id`. ### 3.2 Tipo de promotor - `usuario_plataforma`: buscador con autocomplete contra `GET /usuarios/buscar?q=`, debe resolver a un `user_id` real antes de guardar. - `externo`: campo de texto libre (`nombre_externo`), para colaboradores/influencers sin cuenta en Rebelocity Club. ### 3.3 Comisión 0–100%, acepta 0 (permite promotores de solo tracking, sin pago de comisión). ### 3.4 Límite de usos Vacío = ilimitado. Si tiene valor, debe ser entero positivo. Al alcanzar `usos_actuales = limite_usos`, el código deja de aplicar en checkout (validación de backend, no solo de UI). ### 3.5 Vigencia (opcional) Si no se define, el código es válido mientras el evento esté publicado. Si se define `vigencia_hasta` y ya pasó, el código deja de aplicar aunque `estado = activo` — esta validación debe vivir en el checkout, no solo en el modal. ### 3.6 Alcance por modalidad - `aplica_todas_modalidades = true` (default): el código es válido sin importar qué modalidad de boleto compre el referido. - `aplica_todas_modalidades = false`: el modal despliega checkboxes de las modalidades del evento; se debe elegir al menos una. El checkout debe validar que la modalidad comprada esté en `modalidades_aplicables`. ### 3.7 Eliminar un promotor - Si `usos_actuales = 0`: eliminación física normal, con confirmación simple. - Si `usos_actuales > 0`: se bloquea el borrado físico (mismo patrón que modalidades con `cupos_vendidos > 0`). El botón "Eliminar" se reemplaza por "Desactivar" (`estado = inactivo`), que detiene nuevos usos sin borrar el historial de ventas ya atribuidas a ese promotor. --- ## 4. Endpoints propuestos ``` GET /eventos/:eventoId/promotores → listado (ya existe implícito en la fila mostrada hoy) POST /eventos/:eventoId/promotores → alta PUT /eventos/:eventoId/promotores/:promotorId → edición DELETE /eventos/:eventoId/promotores/:promotorId → borrado físico (solo si usos_actuales = 0) o 409 si no PATCH /eventos/:eventoId/promotores/:promotorId/estado → desactivar/reactivar GET /usuarios/buscar?q= → autocomplete para tipo_promotor = usuario_plataforma ``` --- ## 5. Comportamiento del modal (UX) - Se abre con los dos botones "+ Agregar promotor" que ya existen en el mockup (encabezado de la sección y el `add-btn` al final de la lista) — ambos deben apuntar al mismo modal. - Modo edición: el ícono ✏️ de una fila ya creada abre el mismo modal, pre-poblado. - Campos condicionales: - `nombre_externo` solo visible si `tipo_promotor = externo`. - Checkboxes de modalidades solo visibles si se desactiva "Aplica a todas las modalidades". - `vigencia_hasta` solo visible si se activa "Agregar vigencia". - El link personalizado se muestra como texto de solo lectura debajo del código, actualizándose en vivo mientras se edita el código. - Validación de código duplicado se muestra inline, sin bloquear el resto del formulario. --- ## 6. Referencia visual Ver `MOCKUP-RCH-Eventos-ModalPromotor-v01.html` — solo contiene el modal (no el formulario completo de Eventos), reutilizando los mismos tokens de color y componentes (`.btn`, `.form-group`, `.tog-field`, `.badge`) ya usados en `MOCKUP-RCH-Eventos-v01.html` para consistencia visual. --- *Spec v01 · Rebelocity Club · Ángeles Bravo · 2026-07-22*