# Almacenaje y salida

Ingrese mercancías al almacén y luego sáquelas: configuración → cotizar → crear el almacenaje → pagar → listar artículos aún en existencias → estimar la salida → crear la salida → pagar → seguir → cancelar.

Esta guía documenta únicamente las operaciones siguientes. Complételas en secuencia. La autenticación usa una cuenta de **cliente**.

Sustituya `YOUR_HOST` y `ACCESS_TOKEN` por los valores de su entorno.

## 1. Acceso (cliente)

```bash
curl -X POST https://YOUR_HOST/api/v1/user/customer/login \
  -H "Content-Type: application/json" \
  -d '{"email":"you@example.com","password":"your_password"}'
```

```
Authorization: Bearer ACCESS_TOKEN
```

GraphQL usa la misma cabecera en `POST /api/graphql`.

**Verificación:** el login devuelve `access_token`. Una llamada posterior sin él devuelve `401`.

## 2. Configuración de almacenaje

**REST:** `GET /api/v1/customer/storage-orders/config` — [Manual REST](/api/documentation#/paths/v1-customer-storage-orders-config/get)

```bash
curl https://YOUR_HOST/api/v1/customer/storage-orders/config \
  -H "Authorization: Bearer ACCESS_TOKEN"
```

**GraphQL:** `customerStorageOrderConfig` ([Manual GraphQL](/api/graphql/documentation#/customer/customerStorageOrderConfig))

**Verificación:** ha capturado un `id` de almacén y, si el catálogo no está vacío, un `id` de embalaje.

## 3. Cotizar el almacenaje

**REST:** `POST /api/v1/customer/storage-orders/calculate-price`

```bash
curl -X POST https://YOUR_HOST/api/v1/customer/storage-orders/calculate-price \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "warehouse_id": 7,
    "start_date": "2026-09-10",
    "end_date": "2026-10-10",
    "items": [{
      "qty": 1,
      "length": 30,
      "width": 20,
      "height": 15,
      "dimension_unit": 2,
      "weight": 2,
      "weight_unit": 2
    }]
  }'
```

**Verificación:** `success` o `result` es true y tiene un precio. Falta `warehouse_id` / fechas es `400`.

## 4. Crear el pedido de almacenaje

**REST:** `POST /api/v1/customer/storage-orders`

```bash
curl -X POST https://YOUR_HOST/api/v1/customer/storage-orders \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: dev-store-001" \
  -d '{
    "warehouse_id": 7,
    "start_date": "2026-09-10",
    "end_date": "2026-10-10",
    "items": [{
      "description": "Box A",
      "qty": 1,
      "length": 30,
      "width": 20,
      "height": 15,
      "dimension_unit": 2,
      "weight": 2,
      "weight_unit": 2,
      "value": 100
    }]
  }'
```

**Verificación:** la respuesta tiene `data.id`. Guarde ese id de pedido de almacenaje.

## 5. Pagar el almacenaje

**REST:** `POST /api/v1/customer/storage-orders/{id}/pay`

```bash
curl -X POST https://YOUR_HOST/api/v1/customer/storage-orders/1024/pay \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{}'
```

Opcional: primero `GET /api/v1/customer/storage-orders/{id}/payment-info`.

**REST:** `GET /api/v1/customer/storage-orders/{id}` — [Manual REST](/api/documentation#/paths/v1-customer-storage-orders-id/get)

**GraphQL:** `customerStorageOrderShow` ([Manual GraphQL](/api/graphql/documentation#/customer/customerStorageOrderShow))

**Verificación:** el pedido de almacenaje está pagado / confirmado. `402` significa recargar el saldo y reintentar.

La salida de abajo solo funciona después de que los paquetes se hayan **recibido** en el almacén. En una prueba, espere hasta que el personal (o una recepción de prueba) los haya marcado como recibidos y entonces continúe.

## 6. Listar artículos aún en existencias

**REST:** `GET /api/v1/customer/shipout-orders/available-items` — [Manual REST](/api/documentation#/paths/v1-customer-shipout-orders-available-items/get)

```bash
curl "https://YOUR_HOST/api/v1/customer/shipout-orders/available-items?warehouse_id=7" \
  -H "Authorization: Bearer ACCESS_TOKEN"
```

**GraphQL:** `customerShipoutAvailableItems` ([Manual GraphQL](/api/graphql/documentation#/customer/customerShipoutAvailableItems))

**Verificación:** ha capturado uno o más `storage_package_ids` (ejemplo `5001`). Una lista vacía significa que aún no se ha recibido nada — no cree una salida. `403` significa que la salida está desactivada para este cliente.

## 7. Estimar y crear la salida

**REST:** `GET /api/v1/customer/shipout-orders/services?warehouse_id=7` — [Manual REST](/api/documentation#/paths/v1-customer-shipout-orders-services/get)

Capture un `service_code`.

**REST:** `POST /api/v1/customer/shipout-orders/services/{serviceCode}/estimate` — [Manual REST](/api/documentation#/paths/v1-customer-shipout-orders-services-serviceCode--estimate/post)

```bash
curl -X POST https://YOUR_HOST/api/v1/customer/shipout-orders/services/intl_express/estimate \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "warehouse_id": 7,
    "delivery_postcode": "M5V2H1",
    "delivery_country": "CA",
    "packages": [{
      "weight": 2.5,
      "length": 30,
      "width": 20,
      "height": 15,
      "weight_unit": 2,
      "dimension_unit": 2
    }]
  }'
```

**REST:** `POST /api/v1/customer/shipout-orders/services/{serviceCode}/orders` — [Manual REST](/api/documentation#/paths/v1-customer-shipout-orders-services-serviceCode--orders/post)

```bash
curl -X POST https://YOUR_HOST/api/v1/customer/shipout-orders/services/intl_express/orders \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: dev-shipout-001" \
  -d '{
    "warehouse_id": 7,
    "storage_package_ids": [5001],
    "delivery_name": "Jane Recipient",
    "delivery_telephone": "5555555555",
    "delivery_email": "jane@example.com",
    "delivery_address_1": "123 King St W",
    "delivery_city": "Toronto",
    "delivery_province": "ON",
    "delivery_country": "CA",
    "delivery_postcode": "M5V2H1"
  }'
```

**GraphQL:** `customerCreateShipoutOrder` ([Manual GraphQL](/api/graphql/documentation#/customer/customerCreateShipoutOrder))

**Verificación:** la respuesta tiene un `id` de salida. Los paquetes de almacenaje seleccionados quedan bloqueados en esta solicitud.

## 8. Pagar la salida, seguir, eventos

**REST:** `POST /api/v1/customer/shipout-orders/{id}/pay` — [Manual REST](/api/documentation#/paths/v1-customer-shipout-orders-id--pay/post)

```bash
curl -X POST https://YOUR_HOST/api/v1/customer/shipout-orders/8001/pay \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{}'
```

**GraphQL:** `customerPayShipout` ([Manual GraphQL](/api/graphql/documentation#/customer/customerPayShipout))

**REST:** `GET /api/v1/customer/shipout-orders/{id}` — [Manual REST](/api/documentation#/paths/v1-customer-shipout-orders-id/get)

Cuando existe un número de seguimiento:

```bash
curl https://YOUR_HOST/api/v1/tracking/SR123456789012
```

[Manual REST](/api/documentation#/operations/getPublicTracking) · [Manual GraphQL](/api/graphql/documentation#/tracking/trackingPublic)

**REST:** `PUT /api/v1/webhook-settings` — [Manual REST](/api/documentation#/paths/v1-webhook-settings/put)

**GraphQL:** `webhookSettingsUpdate` ([Manual GraphQL](/api/graphql/documentation#/webhooks/webhookSettingsUpdate)).

Verifique **v2**: `HMAC_SHA256(timestamp + "." + raw_body, secret)` contra `X-Webhook-Signature-V2`. Deduplique con `X-Webhook-Event-Id`. Responda **2xx en menos de 3 segundos**.

```php
$raw = file_get_contents('php://input');
$ts  = $_SERVER['HTTP_X_WEBHOOK_TIMESTAMP'] ?? '';
$sig = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE_V2'] ?? '';
$expected = hash_hmac('sha256', $ts . '.' . $raw, $sharedSecret);
if (!hash_equals($expected, $sig)) {
    http_response_code(401);
    exit;
}
```

**Verificación:** el pago registra un importe (o `402` / `422` con un motivo claro). El seguimiento público encuentra el envío cuando existe un número.

## 9. Cancelar una salida de prueba

**REST:** `POST /api/v1/customer/shipout-orders/{id}/cancel` — [Manual REST](/api/documentation#/paths/v1-customer-shipout-orders-id--cancel/post)

```bash
curl -X POST https://YOUR_HOST/api/v1/customer/shipout-orders/8001/cancel \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{}'
```

**GraphQL:** `customerCancelShipout` ([Manual GraphQL](/api/graphql/documentation#/customer/customerCancelShipout))

Esto libera el bloqueo de los paquetes de almacenaje. El almacenaje en sí se cancela con `POST /api/v1/customer/storage-orders/{id}/cancel` mientras aún esté permitido.

**Verificación:** `422` significa que este estado no se puede cancelar. Tras una cancelación de salida correcta, el paso 6 vuelve a listar los paquetes.

## Lista de pruebas

- [ ] La configuración de almacenaje devuelve un `id` de almacén.
- [ ] La cotización de almacenaje devuelve un precio.
- [ ] Crear el almacenaje devuelve `data.id`.
- [ ] Pagar el almacenaje se completa, **o** ha confirmado que hay que recargar el saldo.
- [ ] Artículos disponibles lista paquetes recibidos (`storage_package_ids`).
- [ ] La estimación de salida devuelve un precio o `has_items_needing_quote`.
- [ ] Crear la salida devuelve un `id` y bloquea esos paquetes.
- [ ] Pagar la salida se completa (o se entiende `402` / `422`).
- [ ] El seguimiento público encuentra el envío cuando existe un número de seguimiento.
- [ ] Cancelar la salida libera los paquetes, **o** este estado no se puede cancelar.
