# Datos y API

Descripción del esquema **declarado en migraciones**, no de una inspección de la base de datos desplegada.

## Modelo de datos

```text
orders (cabecera) 1 ───── N order_details (renglones)
                              ·
                              · relación lógica por SKU, sin FK
                              ·
                            items (catálogo)

users y auth_*: tablas administradas por Shield
```

### `orders`

| Campo | Definición |
| --- | --- |
| `id` | INT unsigned autoincremental, PK. |
| `client` | INT con `constraint = 8`. Esto no valida ocho dígitos y no conserva ceros iniciales. |
| `status` | ENUM `pending`, `processing`, `completed`, `cancelled`; predeterminado `pending`. |
| `created_at` | DATETIME, `CURRENT_TIMESTAMP`. |
| `updated_at` | DATETIME nullable, actualización automática declarada en SQL. |
| `deleted_at` | DATETIME nullable para soft delete. |

### `items`

| Campo | Definición |
| --- | --- |
| `id` | INT unsigned autoincremental, PK. |
| `sku` | VARCHAR(100), sin índice único declarado. |
| `description` | TEXT nullable. |

### `order_details`

| Campo | Definición |
| --- | --- |
| `id` | INT unsigned autoincremental, PK. |
| `order` | INT unsigned; FK a `orders.id`. |
| `sku` | INT, distinto del VARCHAR del catálogo; no tiene FK a `items`. |
| `quantity` | INT; sin restricción SQL de rango 1–99 declarada. |
| `status` | Mismo ENUM y valor predeterminado que la cabecera. |
| `created_at`, `updated_at`, `deleted_at` | Metadatos agregados en una migración posterior. |

No hay campos de usuario creador, responsable de finalización o responsable de borrado en las tablas de negocio. No se declararon índices adicionales de búsqueda por cliente, estado o fecha en las migraciones propias.

## Migraciones propias, en orden

1. `2026-04-24-190224_CreateOrderTable.php`: crea `orders`.
2. `2026-04-24-205147_CreateItemsTable.php`: crea `items`.
3. `2026-04-24-225430_CreateOrderDetailTable.php`: crea `order_details` y su FK.
4. `2026-04-25-174309_AddStatusToOrderDetailsTable.php`: agrega estado a los detalles.
5. `2026-04-27-011035_AddMetadataToOrderdetailTable.php`: agrega fechas y borrado lógico.

Shield y su dependencia Settings aportan migraciones en sus namespaces; una instalación nueva debe considerar también esas migraciones. El esquema propio utiliza `ENUM`, InnoDB y expresiones MySQL como `ON UPDATE CURRENT_TIMESTAMP`, por lo que las pruebas SQLite de ejemplo no acreditan su compatibilidad ni su reversión.

## Rutas implementadas

Confirmadas con `php spark routes`. «Sin filtro de sesión» describe la configuración de aplicación observada, no controles de red externos.

| Método y ruta | Acción | Acceso configurado |
| --- | --- | --- |
| `GET /` | Formulario y catálogo. | Sin filtro de sesión. |
| `GET /dashboard` | Gestión de pedidos. | `session`. |
| `GET /stats` | Vista pendiente. | `session`. |
| `GET /order` | Lista plana de cabeceras y detalles. | Sin filtro de sesión. |
| `POST /order` | Crea pedido y renglones. | Sin filtro de sesión. |
| `POST /complete/{id}` | Completa un detalle. | Sin filtro de sesión. |
| `PATCH /complete-batch` | Completa varios detalles. | Sin filtro de sesión. |
| `DELETE /order/{id}` | Borra lógicamente un detalle; exige ID numérico en esta ruta. | Sin filtro de sesión. |

### Crear pedido

Contenido que envía actualmente el frontend: `application/x-www-form-urlencoded`.

```text
client=10045123
items[0][sku]=123456
items[0][quantity]=2
items[1][sku]=789012
items[1][quantity]=1
```

El controlador usa `getPost()`, no `getJSON()`. En éxito responde HTTP **201** con `Orden creada exitosamente`, sin devolver el nuevo ID. La excepción capturada al insertar detalles devuelve un error mediante `fail()`; los errores de cabecera y los retornos falsos de persistencia no tienen tratamiento equivalente.

### Consultar pedidos

`GET /order` devuelve una colección directamente, sin envoltura `data` y sin paginación del servidor. Cada fila incluye:

- Los campos `o.*` de la cabecera, incluido `id` y `status`.
- `detail_id`, `sku`, `quantity`, `detail_status`.

El dashboard espera `id` como cadena para aplicar `padStart()` y `created_at.date` para formar la referencia visible. Estos supuestos deben verificarse si cambian los casts de entidades o `numberNative` de la conexión. Al usar `LEFT JOIN`, una cabecera sin detalles puede aparecer con campos de detalle nulos.

### Completar un detalle

`POST /complete/42`: el `42` representa `order_details.id`. No requiere cuerpo. Responde con la entidad del detalle. El controlador no comprueba si `find()` devolvió un registro antes de asignar `status`.

### Completar por lote

```http
PATCH /complete-batch
Content-Type: application/json

{"ids": [42, 43]}
```

Si `ids` falta, está vacío o no es un array, devuelve error de validación. En éxito devuelve un objeto como `{"updated": 2}`. **Ese número es el tamaño de la lista recibida**, no el número comprobado de registros realmente modificados.

### Borrar detalle

`DELETE /order/42`: el ID corresponde al detalle. Devuelve **404** si no se encuentra; al terminar correctamente usa `respondDeleted()` con el mensaje `Detalle de orden borrado` (HTTP 200 por defecto en este helper). La operación de detalle y posible cabecera se ejecuta dentro de una transacción.

## Rutas generadas sin implementación

`$routes->resource('order')` también registra rutas hacia `Order::new`, `edit`, `show`, `update` y `delete`, métodos ausentes en ese controlador. Se observan `GET /order/new`, `GET /order/{id}/edit`, `GET /order/{id}`, `PATCH/PUT /order/{id}` y una ruta DELETE genérica adicional.

La ruta DELETE numérica explícita está registrada antes de la genérica. Es importante no confundir esa operación sobre detalles con un CRUD REST completo de cabeceras.

## Autenticación

Shield registra login, logout, registro, enlaces mágicos y acciones de autenticación. El login tiene vista personalizada y usa correo/contraseña. Aunque `register` y `login/magic-link` aparecen en la tabla de rutas, `Auth.php` configura `allowRegistration = false` y `allowMagicLinkLogins = false`; la mera presencia de una ruta no implica que esa función esté habilitada.
