# API de productos

## 1. Objetivo

Este módulo expone el catálogo administrativo general de productos para **Promotion Management System**.

Permite:

- Listar productos.
- Consultar detalle.
- Crear productos.
- Editar productos.
- Activar o desactivar productos.
- Obtener opciones reutilizables para selectores y autocompletados.

Importante:

- La cantidad de boletos no pertenece al catálogo general de productos.
- Los boletos por producto se configurarán posteriormente en la relación entre producto y promoción.
- El endpoint `/api/v1/admin/products/options` está preparado para reutilizarse en formularios de promociones.
- El catálogo de productos usa únicamente el código interno `code`.

## 2. Permisos

Todos los permisos usan `guard_name = web`.

| Permiso | Uso |
| --- | --- |
| `products.view_any` | Mostrar el módulo en el sidebar, listar productos y consultar `/options`. |
| `products.view` | Consultar el detalle de un producto. |
| `products.create` | Crear productos. |
| `products.update` | Editar productos. |
| `products.change_status` | Activar o desactivar productos. |

## 3. Sidebar y manejo de permisos

- React solo muestra `Productos` en el sidebar cuando el usuario autenticado tiene `products.view_any`.
- Las rutas frontend también validan permisos antes de renderizar cada pantalla.
- Laravel vuelve a validar autorización en cada endpoint mediante `ProductPolicy`.

## 4. Tabla de endpoints

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

Todos requieren:

- `auth:sanctum`
- `Accept: application/json`
- Sesión SPA con credenciales habilitadas

## 5. UUID público

- `{product}` siempre representa el UUID público del producto.
- La API no acepta ni expone el ID numérico interno.
- React debe usar `data.id` como UUID público en rutas y acciones.

## 6. Paginación

`GET /api/v1/admin/products` usa la convención estándar del proyecto:

```json
{
  "success": true,
  "message": "Productos obtenidos correctamente.",
  "data": [],
  "meta": {
    "current_page": 1,
    "per_page": 15,
    "total": 0,
    "last_page": 1
  }
}
```

Parámetros:

- `page`: mínimo `1`
- `per_page`: mínimo `1`, máximo `100`, predeterminado `15`

## 7. Filtros

`GET /api/v1/admin/products` admite:

- `search`
- `status`
- `presentation`
- `created_from`
- `created_to`
- `sort`
- `direction`

### Search

Busca por:

- `code`
- `name`
- `description`
- `presentation`

### Status

Valores válidos:

- `active`
- `inactive`

### Fechas

- `created_from` y `created_to` usan formato `Y-m-d`
- `created_to` incluye el día completo

### Ordenamiento

Campos permitidos:

- `created_at`
- `code`
- `name`
- `presentation`
- `status`

Direcciones permitidas:

- `asc`
- `desc`

Predeterminado:

- `name asc`

## 8. Payload de creación

`POST /api/v1/admin/products`

```json
{
  "code": "PROD-001",
  "name": "Producto de ejemplo",
  "description": "Descripción del producto.",
  "presentation": "Caja de 20 unidades",
  "status": "active"
}
```

Respuesta:

- HTTP `201`
- mensaje: `Producto creado correctamente.`

## 9. Payload de actualización

`PATCH /api/v1/admin/products/{product}`

```json
{
  "code": "PROD-001",
  "name": "Producto de ejemplo",
  "description": "Descripción actualizada.",
  "presentation": "Caja de 30 unidades"
}
```

Este endpoint no permite:

- Cambiar `status`
- Cambiar `uuid`
- Enviar IDs internos
- Definir boletos
- Asociar promociones

## 10. Payload de cambio de estado

`PATCH /api/v1/admin/products/{product}/status`

```json
{
  "status": "inactive"
}
```

Comportamiento:

- Solo acepta `active` o `inactive`
- Desactivar impide nuevas selecciones en `/options`
- No elimina historial

## 11. Respuesta de listado

```json
{
  "success": true,
  "message": "Productos obtenidos correctamente.",
  "data": [
    {
      "id": "uuid-publico",
      "code": "PROD-001",
      "name": "Producto de ejemplo",
      "description": "Descripción del producto.",
      "presentation": "Caja de 20 unidades",
      "status": "active",
      "created_at": "2026-07-15T12:00:00.000000Z",
      "updated_at": "2026-07-15T12:00:00.000000Z"
    }
  ],
  "meta": {
    "current_page": 1,
    "per_page": 15,
    "total": 1,
    "last_page": 1
  }
}
```

## 12. Respuesta de detalle

`GET /api/v1/admin/products/{product}`

Devuelve:

- `id`
- `code`
- `name`
- `description`
- `presentation`
- `status`
- `created_at`
- `updated_at`

No devuelve:

- ID interno
- campos de boletos
- relaciones de promociones

## 13. Endpoint de opciones

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

Parámetros:

- `search` opcional
- `limit` opcional, máximo `50`

Devuelve únicamente productos activos:

```json
{
  "success": true,
  "message": "Opciones de productos obtenidas correctamente.",
  "data": [
    {
      "id": "uuid-publico",
      "code": "PROD-001",
      "name": "Producto de ejemplo",
      "presentation": "Caja de 20 unidades"
    }
  ]
}
```

## 14. Validaciones

- `code`: requerido, máximo `50`, único
- `name`: requerido, máximo `150`
- `description`: opcional, máximo `1000`
- `presentation`: opcional, máximo `150`
- `status`: requerido en creación, valores `active|inactive`

Campos prohibidos:

- `id`
- `uuid`
- `external_code`
- `promotion_id`
- `tickets_quantity`
- `tickets_per_unit`
- `ticket_value`
- `created_at`
- `updated_at`

## 15. Normalización

- `code`: `trim`, sin espacios extremos, comparación por valor normalizado, almacenamiento en mayúsculas
- `name`, `description`, `presentation`: `trim`, colapso de espacios duplicados, conservación de acentos y Unicode

## 16. Campos opcionales

Pueden enviarse vacíos y se persistirán como `null`:

- `description`
- `presentation`

## 17. Códigos de error

| Código | HTTP | Uso |
| --- | --- | --- |
| `PRODUCT_CODE_ALREADY_EXISTS` | `422` | Código duplicado del producto |
| `VALIDATION_ERROR` | `422` | Error de validación |
| `UNAUTHENTICATED` | `401` | Sesión faltante o vencida |
| `FORBIDDEN` | `403` | Falta de permiso |
| `RESOURCE_NOT_FOUND` | `404` | UUID inexistente |
| `TOO_MANY_REQUESTS` | `429` | Límite de solicitudes |

## 18. Ejemplos con Axios

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

export async function getProducts(params: Record<string, unknown>) {
  const response = await api.get('/api/v1/admin/products', { params })
  return response.data
}
```

```ts
export async function createProduct(payload: {
  code: string
  name: string
  description?: string | null
  presentation?: string | null
  status: 'active' | 'inactive'
}) {
  const response = await api.post('/api/v1/admin/products', payload)
  return response.data
}
```

```ts
export async function changeProductStatus(id: string, status: 'active' | 'inactive') {
  const response = await api.patch(`/api/v1/admin/products/${id}/status`, { status })
  return response.data
}
```

## 19. Rutas frontend

- `/products`
- `/products/new`
- `/products/:id`
- `/products/:id/edit`

Protección esperada:

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

## 20. Pruebas manuales sugeridas

1. Iniciar sesión como administrador y confirmar visibilidad del menú `Productos`.
2. Crear un producto con código en minúsculas y verificar almacenamiento normalizado.
3. Editar un producto y enviar `description` y `presentation` vacíos.
4. Intentar duplicar `code` y confirmar error `422` sobre `errors.code`.
5. Enviar `external_code` y confirmar que la API lo rechaza.
6. Desactivar un producto y confirmar que desaparece de `/options`.
7. Entrar con un usuario dependiente y validar que no accede al módulo.
8. Forzar un UUID inexistente y verificar `RESOURCE_NOT_FOUND`.
