--- 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). --- ## 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` | 🟡 | | `user_id` | UUID (FK), nullable | Condicional | requerido si `tipo_promotor = usuario_plataforma`; debe existir en `users` | 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*