# API de configuración administrativa de promociones

## 1. Objetivo

Este documento describe el contrato técnico backend para configurar:

- Alcance de farmacias participantes.
- Productos participantes.
- Reglas de generación de boletos por producto.
- Estado de completitud de una promoción.

Está pensado para que el frontend implemente la interfaz sin inspeccionar el código interno de Laravel.

## 2. Alcance

La implementación actual permite:

- Consultar la configuración completa de una promoción.
- Configurar `all` o `specific` para farmacias.
- Asociar productos activos.
- Configurar reglas por relación promoción-producto usando `required_quantity`, `tickets_awarded` y `calculation_mode`.
- Determinar si la promoción está completa.
- Bloquear programación o activación cuando la configuración esté incompleta.

Todavía no implementa:

- Registro de compras.
- Participaciones.
- Generación real de boletos.
- Numeración consecutiva.
- Promociones públicas para dependientes.

## 3. Permisos requeridos

| Permiso | Uso |
| --- | --- |
| `promotions.view` | Consultar `GET /configuration` |
| `promotions.manage_pharmacies` | Configurar alcance y farmacias específicas |
| `promotions.manage_products` | Configurar productos participantes |
| `promotions.manage_ticket_rules` | Configurar reglas de boletos por producto |

## 4. Política vigente de configuración

La configuración ya no depende solo del estado. También considera actividad operativa.

Se puede modificar cuando la promoción está en:

- `draft`
- `scheduled`, únicamente si `starts_at` todavía está en el futuro
- `active`, únicamente si no tiene actividad operativa
- `suspended`, únicamente si no tiene actividad operativa

No puede modificarse cuando:

- está `finished`
- está `cancelled`
- ya inició una `scheduled`
- existen aplicaciones promocionales, boletos o facturas relacionadas

Si no puede modificarse, la API responde `409` con `PROMOTION_CONFIGURATION_NOT_EDITABLE` y `errors.reasons`.

## 5. Tabla de endpoints

| Método | Endpoint | Permiso |
| --- | --- | --- |
| `GET` | `/api/v1/admin/promotions/{promotion}/configuration` | `promotions.view` |
| `PUT` | `/api/v1/admin/promotions/{promotion}/configuration` | `promotions.manage_pharmacies` + `promotions.manage_products` + `promotions.manage_ticket_rules` |
| `PUT` | `/api/v1/admin/promotions/{promotion}/pharmacies` | `promotions.manage_pharmacies` |
| `PUT` | `/api/v1/admin/promotions/{promotion}/products` | `promotions.manage_products` |
| `PUT` | `/api/v1/admin/promotions/{promotion}/ticket-rules` | `promotions.manage_ticket_rules` |

Todos usan:

- `auth:sanctum`
- JSON
- UUID público en `{promotion}`

La aceptación versionada de términos por parte de dependientes no se administra desde este módulo. La consulta y aceptación de términos vigentes está documentada en `docs/frontend/participant-promotions-api.md`.

## 6. Consulta de configuración

`GET /api/v1/admin/promotions/{promotion}/configuration`

Respuesta base:

```json
{
  "success": true,
  "message": "Configuración de la promoción obtenida correctamente.",
  "data": {
    "promotion": {
      "id": "uuid-publico",
      "name": "Rifa de temporada",
      "status": "draft",
      "status_label": "Borrador"
    },
    "pharmacies": {
      "scope": "specific",
      "scope_label": "Farmacias específicas",
      "selected_count": 2,
      "items": []
    },
    "products": [],
    "configuration": {
      "is_complete": false,
      "pharmacy_scope_configured": true,
      "specific_pharmacies_configured": true,
      "products_configured": false,
      "ticket_rules_configured": false,
      "terms_configured": false,
      "terms_and_conditions_configured": false,
      "terms_version_configured": false,
      "promotion_dates_valid": true,
      "missing_requirements": [
        "products",
        "ticket_rules",
        "terms_and_conditions",
        "terms_version"
      ],
      "specific_pharmacies_count": 2,
      "products_count": 0,
      "products_without_ticket_rules_count": 0,
      "inactive_pharmacies_count": 0,
      "inactive_products_count": 0
    },
    "capabilities": {
      "can_manage_pharmacies": true,
      "can_manage_products": true,
      "can_manage_ticket_rules": true,
      "can_update_configuration": true,
      "configuration_reasons": []
    }
  }
}
```

`promotion.activity` resume el contexto operativo:

- `has_invoice_applications`
- `has_generated_tickets`
- `has_related_invoices`
- `has_terms_acceptances`
- `has_operational_activity`

El frontend debe usar `capabilities.can_update_configuration` como fuente de verdad para habilitar guardado, y `configuration_reasons` para explicar restricciones.

## 7. Configuración de todas las farmacias

`PUT /api/v1/admin/promotions/{promotion}/pharmacies`

Payload:

```json
{
  "scope": "all"
}
```

Semántica:

- No es obligatorio almacenar filas en la tabla pivote.
- Se consideran elegibles todas las farmacias activas.
- Las farmacias inactivas no deben considerarse elegibles.
- Cuando se implementen futuras reglas de participación, nuevas farmacias activas también entrarán en este alcance general.

## 7.1 Guardado unificado de configuración

`PUT /api/v1/admin/promotions/{promotion}/configuration`

Este endpoint permite guardar en una sola llamada:

- alcance de farmacias
- farmacias específicas
- productos participantes
- reglas de boletos

Payload:

```json
{
  "scope": "specific",
  "pharmacy_ids": ["uuid-publico-1"],
  "product_ids": ["uuid-producto-1", "uuid-producto-2"],
  "products": [
    {
      "product_id": "uuid-producto-1",
      "required_quantity": 5,
      "tickets_awarded": 1,
      "calculation_mode": "per_complete_block"
    },
    {
      "product_id": "uuid-producto-2",
      "required_quantity": 3,
      "tickets_awarded": 2,
      "calculation_mode": "per_complete_block"
    }
  ]
}
```

Comportamiento:

- Ejecuta toda la actualización en una sola transacción lógica.
- Si falla cualquiera de las tres secciones, no se guarda ningún cambio parcial.
- Requiere los tres permisos de configuración.
- Las reglas deben corresponder exactamente a los productos enviados en `product_ids`.
- Devuelve la configuración completa ya actualizada.
- Si existe actividad operativa, responde `409 PROMOTION_CONFIGURATION_NOT_EDITABLE`.

## 8. Configuración de farmacias específicas

Payload:

```json
{
  "scope": "specific",
  "pharmacy_ids": [
    "uuid-publico-1",
    "uuid-publico-2"
  ]
}
```

Reglas:

- `scope` es obligatorio y solo acepta `all` o `specific`.
- Si `scope = specific`, `pharmacy_ids` debe contener al menos un UUID público.
- No se permiten repetidos.
- Todas las farmacias deben existir y estar activas.
- No se aceptan IDs numéricos, nombres ni códigos.

## 9. Configuración de productos

`PUT /api/v1/admin/promotions/{promotion}/products`

Payload:

```json
{
  "product_ids": [
    "uuid-publico-1",
    "uuid-publico-2"
  ]
}
```

Reglas:

- Debe incluir al menos un producto.
- Solo se aceptan productos activos.
- No se permiten repetidos.
- El endpoint sincroniza exactamente los productos enviados.
- Mantiene la regla completa para productos ya asociados.
- Los productos nuevos quedan sin regla configurada.
- No permite dejar la promoción sin productos.

## 10. Configuración de reglas de boletos

`PUT /api/v1/admin/promotions/{promotion}/ticket-rules`

Payload:

```json
{
  "products": [
    {
      "product_id": "uuid-publico-1",
      "required_quantity": 5,
      "tickets_awarded": 1,
      "calculation_mode": "per_complete_block"
    },
    {
      "product_id": "uuid-publico-2",
      "required_quantity": 3,
      "tickets_awarded": 2,
      "calculation_mode": "per_complete_block"
    }
  ]
}
```

Reglas implementadas:

- `required_quantity` debe ser entero positivo.
- `tickets_awarded` debe ser entero positivo.

## 10.1 Estructura de cada producto configurado

Cuando una promoción ya tiene productos asociados, `GET /configuration` devuelve cada fila con estructura anidada:

```json
{
  "product": {
    "id": "uuid-producto-1",
    "code": "PROD-001",
    "name": "Cortax",
    "presentation": "Caja de 20 tabletas",
    "status": "active"
  },
  "rule": {
    "required_quantity": 5,
    "tickets_awarded": 1,
    "calculation_mode": "per_complete_block",
    "calculation_mode_label": "Por cada bloque completo",
    "description": "Por cada 5 unidades genera 1 boleto"
  }
}
```

Reglas importantes:

- la información del producto configurado viene en la misma respuesta; React no debe depender del catálogo de `/products/options` para reconstruir nombre, código o presentación
- un producto configurado anteriormente sigue apareciendo aunque hoy esté inactivo
- el endpoint no expone `tickets_per_unit`
- el orden de productos debe ser determinista
- `calculation_mode` es obligatorio.
- El único valor soportado actualmente es `per_complete_block`.
- Deben enviarse reglas para todos los productos actualmente asociados.
- No se pueden enviar productos no asociados.
- Este endpoint actualiza reglas, no agrega ni elimina productos.

## 11. Estructura de completitud

La promoción se considera completa solo cuando:

- Tiene `pharmacy_scope`.
- Si es `specific`, tiene al menos una farmacia activa asociada.
- Tiene al menos un producto.
- Todos los productos están activos.
- Todos los productos tienen una regla válida completa.
- Tiene `terms_and_conditions`.
- Tiene `terms_version`.
- Si esos campos cambian, los dependientes deberán aceptar nuevamente la nueva versión o contenido desde el módulo de participante.
- Las fechas siguen siendo válidas.

## 12. Códigos de requisitos faltantes

`missing_requirements` puede incluir:

- `pharmacy_scope`
- `specific_pharmacies`
- `products`
- `ticket_rules`
- `terms_and_conditions`
- `terms_version`
- `promotion_dates`
- `inactive_pharmacies`
- `inactive_products`

Son códigos técnicos estables y deben usarse tal cual.

## 13. Reglas para activar o programar

Antes de permitir transiciones a:

- `scheduled`
- `active`

la API valida la completitud mediante el servicio central de configuración.

También aplica cuando una promoción `suspended` intenta volver a `active`.

Si falla:

- responde `409`
- usa `PROMOTION_CONFIGURATION_INCOMPLETE`
- no cambia el estado

## 14. Payloads completos

Farmacias `all`:

```json
{ "scope": "all" }
```

Farmacias `specific`:

```json
{
  "scope": "specific",
  "pharmacy_ids": ["uuid-1", "uuid-2"]
}
```

Productos:

```json
{
  "product_ids": ["uuid-1", "uuid-2"]
}
```

Reglas:

```json
{
  "products": [
    {
      "product_id": "uuid-1",
      "required_quantity": 5,
      "tickets_awarded": 1,
      "calculation_mode": "per_complete_block"
    },
    {
      "product_id": "uuid-2",
      "required_quantity": 3,
      "tickets_awarded": 2,
      "calculation_mode": "per_complete_block"
    }
  ]
}
```

## 15. Respuestas exitosas

Mensajes implementados:

- `Configuración de la promoción obtenida correctamente.`
- `Farmacias participantes configuradas correctamente.`
- `Productos participantes configurados correctamente.`
- `Reglas de generación de boletos configuradas correctamente.`

Las respuestas de escritura devuelven nuevamente la configuración completa.

## 16. Errores posibles

- `401` cuando no hay sesión
- `403` cuando falta permiso
- `404` cuando la promoción no existe
- `409` cuando la configuración no puede editarse o la promoción está incompleta para avanzar
- `422` cuando el payload no cumple validaciones
- `429` cuando se excede el rate limit

## 17. `error_code`

Los códigos relevantes son:

- `PROMOTION_CONFIGURATION_NOT_EDITABLE`
- `PROMOTION_CONFIGURATION_INCOMPLETE`
- `PROMOTION_STATUS_TRANSITION_NOT_ALLOWED`
- `VALIDATION_ERROR`
- `UNAUTHENTICATED`
- `FORBIDDEN`
- `RESOURCE_NOT_FOUND`
- `TOO_MANY_REQUESTS`

## 18. Ejemplos con Axios

```ts
const configuration = await api.get(`/api/v1/admin/promotions/${promotionId}/configuration`)
```

```ts
await api.put(`/api/v1/admin/promotions/${promotionId}/configuration`, {
  scope,
  pharmacy_ids,
  product_ids,
  products,
})
```

```ts
await api.put(`/api/v1/admin/promotions/${promotionId}/pharmacies`, {
  scope: 'specific',
  pharmacy_ids: selectedPharmacyIds,
})
```

```ts
await api.put(`/api/v1/admin/promotions/${promotionId}/products`, {
  product_ids: selectedProductIds,
})
```

```ts
await api.put(`/api/v1/admin/promotions/${promotionId}/ticket-rules`, {
  products: rows.map((row) => ({
    product_id: row.id,
    required_quantity: row.rule.required_quantity,
    tickets_awarded: row.rule.tickets_awarded,
    calculation_mode: row.rule.calculation_mode,
  })),
})
```

## 19. Uso de UUID públicos

- Todas las selecciones deben usar UUID público.
- Nunca enviar IDs internos numéricos.
- El backend resuelve los IDs internos.

## 20. Comportamiento recomendado de la interfaz

- Consultar primero `/configuration`.
- Mostrar la completitud y los requisitos faltantes.
- Deshabilitar acciones de programar o activar cuando `is_complete = false`, aunque Laravel volverá a validar.
- Mostrar el estado actual de farmacias y productos asociados.
- Cuando `scope = all`, explicar que se consideran elegibles todas las farmacias activas.

## 21. Estados de carga y errores recomendados

- Mostrar loading independiente para consulta y para cada acción `PUT`.
- Asociar errores `422` a campos concretos.
- Tratar `409` como conflicto funcional y mostrar el mensaje del backend.
- En `PROMOTION_CONFIGURATION_INCOMPLETE`, mostrar también `errors.configuration` si llega disponible.

## 22. Catálogos reutilizables

Para construir selectores, el frontend debe reutilizar:

- `GET /api/v1/admin/pharmacies/options`
- `GET /api/v1/admin/products/options`

Este módulo no duplica esos catálogos.

## 23. Pruebas manuales sugeridas

1. Configurar una promoción con `scope = all`.
2. Cambiar a `scope = specific` y seleccionar farmacias activas.
3. Intentar incluir una farmacia inactiva.
4. Asociar productos activos.
5. Configurar reglas de boletos para todos los productos.
6. Intentar omitir uno de los productos en `ticket-rules`.
7. Confirmar que una promoción incompleta no puede programarse.
8. Confirmar que una promoción completa sí puede programarse o activarse si las fechas son válidas.

## 24. Identificación de farmacias específicas

Cada opción de farmacia incluye `branch`. React debe presentar `código · nombre · sucursal` para distinguir establecimientos con el mismo código. El alcance `all` no cambia: cada farmacia física activa continúa participando de forma independiente.
