# Storage and ship-out

Check goods into warehouse storage, then ship stored items out: config → quote → create storage → pay → list items still in stock → estimate ship-out → create ship-out → pay → track → cancel.

This playbook documents the following operations only. Complete them in sequence. Authentication uses a **customer** account.

Replace `YOUR_HOST` and `ACCESS_TOKEN` with values from your environment.

## 1. Login (customer)

```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 uses the same header on `POST /api/graphql`.

**Verification:** login returns `access_token`. Subsequent requests without this token return `401`.

## 2. Storage config

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

**Verification:** you captured a warehouse `id` and, if the catalog is not empty, a packaging `id`.

## 3. Quote storage

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

**Verification:** `success` or `result` is true and you have a price. Missing `warehouse_id` / dates is `400`.

## 4. Create the storage order

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

**Verification:** response has `data.id`. Store that storage order id.

## 5. Pay storage

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

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

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

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

**Verification:** the storage order is paid / confirmed. `402` means top up the wallet, then retry.

Ship-out below only works after packages are **received** in the warehouse. For a test, wait until staff (or a test receive) has marked them received, then continue.

## 6. List items still in stock

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

**Verification:** you captured one or more `storage_package_ids` (example `5001`). Empty list means nothing is received yet — do not create a ship-out. `403` means ship-out is disabled for this customer.

## 7. Estimate and create the ship-out

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

Capture a `service_code`.

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

**Verification:** response has a ship-out `id`. Selected storage packages are locked to this request.

## 8. Pay the ship-out, track, and subscribe to events

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

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

When a tracking number exists:

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

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

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

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

Verify **v2**: `HMAC_SHA256(timestamp + "." + raw_body, secret)` against `X-Webhook-Signature-V2`. Deduplicate on `X-Webhook-Event-Id`. Answer **2xx in under 3 seconds**.

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

**Verification:** pay records an amount (or `402` / `422` with a clear reason). Public tracking finds the shipment once a number exists.

## 9. Cancel a test ship-out

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

This releases the storage-package lock. Storage itself is cancelled with `POST /api/v1/customer/storage-orders/{id}/cancel` while it is still allowed.

**Verification:** `422` means this status cannot be cancelled. After a successful ship-out cancel, step 6 lists the packages again.

## Test checklist

- [ ] Storage config returns a warehouse `id`.
- [ ] Storage quote returns a price.
- [ ] Storage create returns `data.id`.
- [ ] Storage pay succeeds, **or** you confirmed the wallet must be topped up.
- [ ] Available-items lists received packages (`storage_package_ids`).
- [ ] Ship-out estimate returns a price or `has_items_needing_quote`.
- [ ] Ship-out create returns an `id` and locks those packages.
- [ ] Ship-out pay succeeds (or `402` / `422` is understood).
- [ ] Public tracking finds the shipment once a tracking number exists.
- [ ] Ship-out cancel releases the packages, **or** this status cannot be cancelled.
