# API de promociones administrativas

## Objetivo

Este documento describe el contrato técnico del módulo administrativo de promociones para la SPA React y el backend Laravel.

La implementación actual permite:

- Listar promociones.
- Consultar detalle.
- Crear promociones.
- Editar promociones cuando su estado lo permite.
- Cambiar su estado mediante transiciones controladas.

Todavía no están implementados:

- Farmacias participantes.
- Productos participantes.
- Reglas de boletos.
- Participaciones.
- Generación de boletos.

## Permisos

- `promotions.view_any`
- `promotions.view`
- `promotions.create`
- `promotions.update`
- `promotions.change_status`

El sidebar solo debe mostrar `Promociones` cuando el usuario tenga `promotions.view_any`.

## Tipos

- `raffle` → `Rifa`
- `contest` → `Concurso`
- `purchase_campaign` → `Campaña por compra`

## Estados

- `draft` → `Borrador`
- `scheduled` → `Programada`
- `active` → `Activa`
- `suspended` → `Suspendida`
- `finished` → `Finalizada`
- `cancelled` → `Cancelada`

## Transiciones permitidas

- `draft` → `scheduled`, `active`, `cancelled`
- `scheduled` → `active`, `suspended`, `cancelled`
- `active` → `suspended`, `finished`, `cancelled`
- `suspended` → `active`, `finished`, `cancelled`

No se permite reactivar o reprogramar promociones `finished` o `cancelled`.

## Endpoints

| Método | Endpoint | Permiso |
| --- | --- | --- |
| GET | `/api/v1/admin/promotions` | `promotions.view_any` |
| POST | `/api/v1/admin/promotions` | `promotions.create` |
| GET | `/api/v1/admin/promotions/{promotion}` | `promotions.view` |
| PATCH | `/api/v1/admin/promotions/{promotion}` | `promotions.update` |
| PATCH | `/api/v1/admin/promotions/{promotion}/status` | `promotions.change_status` |

Todos usan `auth:sanctum` y UUID público.

## Filtros y paginación

Parámetros admitidos en listado:

- `page`
- `per_page` máximo `100`
- `search`
- `status`
- `type`
- `starts_from`
- `starts_to`
- `ends_from`
- `ends_to`
- `created_by`
- `sort`
- `direction`

Orden permitido:

- `created_at`
- `name`
- `type`
- `starts_at`
- `ends_at`
- `status`

Predeterminado: `created_at desc`.

## Payload de creación

```json
{
  "name": "Rifa de temporada",
  "description": "Promoción dirigida a dependientes de farmacias participantes.",
  "type": "raffle",
  "prize": "Un motor",
  "starts_at": "2026-08-01T00:00:00-04:00",
  "ends_at": "2026-09-30T23:59:59-04:00",
  "terms_and_conditions": "Texto de términos.",
  "terms_version": "1.0"
}
```

La creación siempre guarda `status = draft`.

## Payload de actualización

```json
{
  "name": "Rifa ajustada",
  "description": "Descripción actualizada.",
  "type": "contest",
  "prize": "Premio actualizado",
  "starts_at": "2026-08-05T08:00:00-04:00",
  "ends_at": "2026-09-20T18:00:00-04:00",
  "terms_and_conditions": "Términos actualizados.",
  "terms_version": "1.1"
}
```

No permite cambiar estado ni relaciones futuras.

## Payload de cambio de estado

```json
{
  "status": "active"
}
```

## Respuesta de listado

```json
{
  "success": true,
  "message": "Promociones obtenidas correctamente.",
  "data": [
    {
      "id": "uuid-publico",
      "name": "Rifa de temporada",
      "description": "Descripción",
      "type": "raffle",
      "type_label": "Rifa",
      "prize": "Un motor",
      "starts_at": "2026-08-01T04:00:00.000000Z",
      "ends_at": "2026-10-01T03:59:59.000000Z",
      "status": "draft",
      "status_label": "Borrador",
      "created_by": {
        "id": "uuid-usuario",
        "first_name": "Albert",
        "last_name": "Mata",
        "full_name": "Albert Mata"
      },
      "created_at": "2026-07-15T15:00:00.000000Z",
      "updated_at": "2026-07-15T15:00:00.000000Z"
    }
  ],
  "meta": {
    "current_page": 1,
    "per_page": 15,
    "total": 1,
    "last_page": 1
  }
}
```

## Respuesta de detalle

Incluye además:

- `terms_and_conditions`
- `terms_version`
- `updated_by`
- `available_status_transitions`

## Validaciones y reglas

- `ends_at` debe ser posterior a `starts_at`.
- No se acepta `uuid`, `status`, `created_by`, `updated_by`, timestamps ni relaciones futuras en creación.
- No se acepta `status` en actualización general.
- Solo se pueden editar promociones `draft`, `scheduled` y `suspended`.
- `scheduled` requiere inicio futuro.
- `scheduled` y `active` requieren fecha final futura.

## Códigos de error esperados

- `UNAUTHENTICATED`
- `FORBIDDEN`
- `RESOURCE_NOT_FOUND`
- `VALIDATION_ERROR`
- `PROMOTION_NOT_EDITABLE`
- `PROMOTION_STATUS_TRANSITION_NOT_ALLOWED`
- `TOO_MANY_REQUESTS`

## Axios

```ts
import { api } from '../lib/api'

export async function getPromotions(params = {}) {
  const response = await api.get('/api/v1/admin/promotions', { params })
  return response.data
}
```

```ts
await api.patch(`/api/v1/admin/promotions/${id}/status`, { status: 'active' })
```

## Manejo de permisos en React

- `/promotions` requiere `promotions.view_any`
- `/promotions/new` requiere `promotions.create`
- `/promotions/:id` requiere `promotions.view`
- `/promotions/:id/edit` requiere `promotions.update`

Laravel siempre vuelve a validar autorización y estado.

## Configuración pendiente

La vista de detalle debe dejar claro que siguen pendientes:

- Farmacias participantes.
- Productos participantes.
- Reglas de boletos.

## Pruebas manuales sugeridas

1. Crear una promoción y confirmar que se guarda en `draft`.
2. Editar una promoción `draft`.
3. Intentar editar una promoción `active` y verificar conflicto.
4. Programar una promoción con fecha futura válida.
5. Intentar programar una promoción vencida.
6. Confirmar que un dependiente recibe `403`.
