docs(campaigns): design spec for manager approval workflow

This commit is contained in:
kitos
2026-07-02 14:53:01 +02:00
parent f1e0e0acf0
commit 74cb946317
@@ -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.