diff --git a/docs/superpowers/specs/2026-07-02-campaign-manager-approval-design.md b/docs/superpowers/specs/2026-07-02-campaign-manager-approval-design.md new file mode 100644 index 0000000..9f36628 --- /dev/null +++ b/docs/superpowers/specs/2026-07-02-campaign-manager-approval-design.md @@ -0,0 +1,97 @@ +# Aprobación de campañas por rol Manager + +**Fecha:** 2026-07-02 + +## Contexto + +Hoy `red_lead`/`blue_lead` crean, editan, añaden/quitan tests y **activan** campañas directamente, incluida la fecha de inicio (`start_date`). No hay ningún control de aprobación intermedio, ni un registro dedicado de quién modifica qué en una campaña (solo el `AuditLog` genérico, sin endpoint de lectura para campañas). + +Roles actuales: `admin`, `red_lead`, `blue_lead`, `red_tech`, `blue_tech`, `viewer`. +Estados actuales de campaña: `draft → active → completed → archived`. + +## Objetivo + +Los leads solo pueden "montar" campañas (crear, nombrar, añadir/quitar tests) mientras están en borrador. Un nuevo rol `manager` aprueba la campaña y es quien fija las fechas. Una vez activa, cualquier cambio de tests requiere justificación y pasa de nuevo por aprobación del manager antes de aplicarse. Todo queda registrado en un timeline consultable. + +## Roles + +- Nuevo rol **`manager`**, transversal (no separado por equipo red/blue). +- `admin` conserva capacidad de aprobar/rechazar, como respaldo (patrón ya usado en el resto de la app). +- `red_lead`/`blue_lead` pierden la capacidad de activar campañas directamente y de fijar `start_date`. + +## Máquina de estados de campaña + +``` +draft ──submit(lead/admin)──> pending_approval ──approve(manager/admin, fija start_date)──> active + │ + └──reject(manager/admin, motivo obligatorio)──> draft +active ──complete (sin cambios)──> completed ──archive (sin cambios)──> archived +``` + +Reglas: +- `submit` requiere que la campaña tenga ≥1 test (misma regla que hoy exige `activate`). +- `start_date` no es editable por el lead (se oculta/bloquea en creación y edición). Solo el manager la fija al aprobar; es obligatoria para poder aprobar. +- Edición de nombre/descripción/tests solo permitida en `draft`. En `pending_approval` la campaña queda congelada. +- Rechazo devuelve la campaña a `draft`, guardando el motivo; el lead puede corregir y reenviar. + +## Modificación de campaña activa (añadir/quitar test) + +Ninguna modificación de tests se aplica directamente sobre una campaña `active`. Se crea una entidad `CampaignModificationRequest`: + +- Campos: `campaign_id`, `requested_by`, `action` (`add_test` | `remove_test`), `test_id`, `justification` (obligatoria), `status` (`pending`/`approved`/`rejected`), `reviewed_by`, `reviewed_at`, `review_notes`, `created_at`. +- Mientras la solicitud está `pending`, la campaña sigue `active` sin ningún cambio real en `CampaignTest`. +- Manager aprueba → se aplica el cambio real (añade/quita la fila de `CampaignTest`) y la solicitud pasa a `approved`. +- Manager rechaza → no se aplica ningún cambio; la solicitud pasa a `rejected` con `review_notes`. + +## Timeline / auditoría + +Se reutiliza el `AuditLog` genérico existente (`entity_type="campaign"`), sin tabla nueva. Nuevas acciones registradas: + +`submit_campaign_for_approval`, `approve_campaign`, `reject_campaign`, `request_campaign_modification`, `approve_campaign_modification`, `reject_campaign_modification` + +Nuevo endpoint de lectura `GET /campaigns/{id}/timeline` que devuelve las entradas de `AuditLog` para esa campaña ordenadas cronológicamente (mismo patrón que el timeline ya existente para tests). + +## Endpoints backend + +| Endpoint | Rol | Efecto | +|---|---|---| +| `POST /campaigns/{id}/submit` | lead/admin | draft → pending_approval | +| `GET /campaigns/pending-approval` | manager/admin | cola de campañas pendientes | +| `POST /campaigns/{id}/approve` | manager/admin | fija fechas, pending_approval → active | +| `POST /campaigns/{id}/reject` | manager/admin | motivo obligatorio, pending_approval → draft | +| `POST /campaigns/{id}/modification-requests` | lead/admin | crea solicitud (solo si active) | +| `GET /campaigns/modification-requests/pending` | manager/admin | cola de solicitudes pendientes | +| `POST /campaigns/modification-requests/{id}/approve` | manager/admin | aplica el cambio de test | +| `POST /campaigns/modification-requests/{id}/reject` | manager/admin | nota obligatoria, no aplica cambio | +| `GET /campaigns/{id}/timeline` | cualquiera autenticado | historial de la campaña | + +Los endpoints existentes `PATCH /campaigns/{id}`, `POST/DELETE .../tests` pasan a devolver 409 fuera de `draft`. + +## Modelo de datos + +`backend/app/models/campaign.py`: +- Ampliar enum de `status` con `pending_approval`. +- Añadir `approved_by` (FK users, nullable), `approved_at` (datetime, nullable), `rejection_reason` (text, nullable). +- `start_date` pasa a nullable en creación (solo se rellena en `approve`). + +Nuevo modelo `CampaignModificationRequest` (tabla nueva, migración Alembic): +- `id`, `campaign_id` (FK), `requested_by` (FK users), `action` (enum), `test_id` (FK tests), `justification` (text, not null), `status` (enum, default `pending`), `reviewed_by` (FK users, nullable), `reviewed_at` (datetime, nullable), `review_notes` (text, nullable), `created_at`. + +## Frontend + +- Rol `manager` añadido al selector de roles en gestión de usuarios. +- Botón "Enviar a aprobación" reemplaza "Activar" para el lead en campañas `draft`. +- Página/cola de aprobación para manager: lista de campañas `pending_approval` con modal de aprobar (pide `start_date` obligatoria) y modal de rechazar (pide motivo obligatorio). +- En campaña `active`: botón "Solicitar modificación" (lead) con formulario de justificación obligatoria; sección de solicitudes pendientes con aprobar/rechazar (manager, rechazo con nota obligatoria). +- Pestaña "Timeline" en el detalle de campaña, listando las entradas de auditoría en orden cronológico (mismo patrón visual que el timeline ya existente en tests). + +## Testing + +Tests pytest cubriendo: +- Permisos por rol en cada endpoint nuevo (lead no puede aprobar, manager no puede montar/editar tests directamente, tech no puede ni montar ni aprobar). +- `approve` falla sin `start_date`. +- `reject` falla sin motivo. +- `submit` falla si la campaña no tiene tests. +- Campaña `active` no cambia su lista de tests hasta que se aprueba la `CampaignModificationRequest`. +- Rechazo de campaña vuelve a `draft` conservando el motivo. +- `GET /campaigns/{id}/timeline` devuelve las entradas en orden cronológico correcto.