# Stockage et expédition

Entrez des marchandises en entrepôt, puis expédiez les articles stockés : configuration → tarifer → créer le stockage → payer → lister les articles encore en stock → estimer l’expédition → créer l’expédition → payer → suivre → annuler.

Ce guide documente uniquement les opérations suivantes. Exécutez-les dans l’ordre. L’authentification utilise un compte **client**.

Remplacez `YOUR_HOST` et `ACCESS_TOKEN` par les valeurs de votre environnement.

## 1. Authentification (client)

```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 utilise le même en-tête sur `POST /api/graphql`.

**Vérification :** la connexion renvoie `access_token`. Un appel ultérieur sans lui renvoie `401`.

## 2. Configuration du stockage

**REST :** `GET /api/v1/customer/storage-orders/config` — [Manuel 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` ([Manuel GraphQL](/api/graphql/documentation#/customer/customerStorageOrderConfig))

**Vérification :** vous avez relevé un `id` d’entrepôt et, si le catalogue n’est pas vide, un `id` d’emballage.

## 3. Tarifer le stockage

**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
    }]
  }'
```

**Vérification :** `success` ou `result` est true et vous avez un prix. `warehouse_id` / dates manquants donnent `400`.

## 4. Créer la commande de stockage

**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
    }]
  }'
```

**Vérification :** la réponse a `data.id`. Stockez cet id de commande de stockage.

## 5. Payer le stockage

**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 '{}'
```

Optionnel : d’abord `GET /api/v1/customer/storage-orders/{id}/payment-info`.

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

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

**Vérification :** la commande de stockage est payée / confirmée. `402` signifie recharger le solde, puis réessayer.

L’expédition ci-dessous ne fonctionne qu’après que les colis ont été **réceptionnés** en entrepôt. Pour un test, attendez que le personnel (ou une réception de test) les ait marqués comme réceptionnés, puis continuez.

## 6. Lister les articles encore en stock

**REST :** `GET /api/v1/customer/shipout-orders/available-items` — [Manuel 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` ([Manuel GraphQL](/api/graphql/documentation#/customer/customerShipoutAvailableItems))

**Vérification :** vous avez relevé un ou plusieurs `storage_package_ids` (exemple `5001`). Une liste vide signifie que rien n’est encore réceptionné — ne créez pas d’expédition. `403` signifie que l’expédition est désactivée pour ce client.

## 7. Estimer et créer l’expédition

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

Relevez un `service_code`.

**REST :** `POST /api/v1/customer/shipout-orders/services/{serviceCode}/estimate` — [Manuel 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` — [Manuel 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` ([Manuel GraphQL](/api/graphql/documentation#/customer/customerCreateShipoutOrder))

**Vérification :** la réponse a un `id` d’expédition. Les colis de stockage sélectionnés sont verrouillés sur cette demande.

## 8. Payer l’expédition, suivre, événements

**REST :** `POST /api/v1/customer/shipout-orders/{id}/pay` — [Manuel 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` ([Manuel GraphQL](/api/graphql/documentation#/customer/customerPayShipout))

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

Lorsqu’un numéro de suivi existe :

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

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

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

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

Vérifiez **v2** : `HMAC_SHA256(timestamp + "." + raw_body, secret)` contre `X-Webhook-Signature-V2`. Dédupliquez sur `X-Webhook-Event-Id`. Répondez **2xx en moins de 3 secondes**.

```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;
}
```

**Vérification :** le paiement enregistre un montant (ou `402` / `422` avec un motif clair). Le suivi public trouve l’envoi dès qu’un numéro existe.

## 9. Annuler une expédition de test

**REST :** `POST /api/v1/customer/shipout-orders/{id}/cancel` — [Manuel 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` ([Manuel GraphQL](/api/graphql/documentation#/customer/customerCancelShipout))

Cela libère le verrou des colis de stockage. Le stockage lui-même s’annule avec `POST /api/v1/customer/storage-orders/{id}/cancel` tant que c’est encore autorisé.

**Vérification :** `422` signifie que ce statut ne peut pas être annulé. Après une annulation d’expédition réussie, l’étape 6 liste à nouveau les colis.

## Liste de tests

- [ ] La configuration du stockage renvoie un `id` d’entrepôt.
- [ ] Le tarif du stockage renvoie un prix.
- [ ] La création du stockage renvoie `data.id`.
- [ ] Le paiement du stockage réussit, **ou** vous avez confirmé que le solde doit être rechargé.
- [ ] Les articles disponibles listent les colis réceptionnés (`storage_package_ids`).
- [ ] L’estimation d’expédition renvoie un prix ou `has_items_needing_quote`.
- [ ] La création d’expédition renvoie un `id` et verrouille ces colis.
- [ ] Le paiement d’expédition réussit (ou `402` / `422` est compris).
- [ ] Le suivi public trouve l’envoi dès qu’un numéro de suivi existe.
- [ ] L’annulation d’expédition libère les colis, **ou** ce statut ne peut pas être annulé.
