# Ritiro e consegna

Il ritiro e l’ultimo miglio con flotta propria usano le stesse API. Creare un ordine di ritiro o di consegna → stampare l’etichetta locale → tracciare → iscriversi alle notifiche degli eventi → annullare un ordine di prova.

`type` seleziona la fermata: `D` consegna, `P` ritiro. Completate i passaggi in sequenza. Il preventivo è facoltativo e non è richiesto prima della creazione.

Sostituite `YOUR_HOST` e `ACCESS_TOKEN` con i valori del vostro ambiente.

## 1. Accesso

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

Inserite l’`access_token` restituito nell’intestazione della richiesta:

```
Authorization: Bearer ACCESS_TOKEN
```

GraphQL usa la stessa intestazione su `POST /api/graphql`.

[Manuale REST](/api/documentation#/paths/v1-user-login/post) · [Manuale GraphQL](/api/graphql/documentation#/user/userLogin)

**Verifica:** il login restituisce `access_token`. Le richieste successive senza questo token restituiscono `401`.

## 2. Quotare (facoltativo)

Questo passaggio è facoltativo. Restituisce solo un preventivo; non viene creato alcun ordine. La creazione non richiede un preventivo precedente. Impostate `type` su `D` (consegna) o `P` (ritiro).

**REST:** `POST /api/v1/orders/rate` — [Manuale REST](/api/documentation#/paths/v1-orders-rate/post)

```bash
curl -X POST https://YOUR_HOST/api/v1/orders/rate \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "D",
    "from_postcode": "H9S2H9",
    "from_country": "CA",
    "to_postcode": "H4B2T5",
    "to_country": "CA",
    "packages": [{
      "weight": 1,
      "weight_unit": 2,
      "length": 30,
      "width": 20,
      "height": 10,
      "dimension_unit": 2
    }]
  }'
```

`weight_unit`: `1` g, `2` kg, `3` oz, `4` lb. `dimension_unit`: `1` mm, `2` cm, `3` m, `4` in.

```json
{
  "result": true,
  "shipping_price": "6.99",
  "currency": "CAD",
  "price_details": {
    "shipping_fee": "6.99",
    "tax_details": [{ "tax_name": "HST", "tax_rate": "13.00", "tax": "0.91" }]
  }
}
```

**GraphQL** (scalare JSON — senza selection set) (`ordersRate` ([Manuale GraphQL](/api/graphql/documentation#/orders/ordersRate))):

```graphql
mutation {
  ordersRate(
    type: "D"
    from_postcode: "H9S2H9"
    from_country: "CA"
    to_postcode: "H4B2T5"
    to_country: "CA"
    packages: [{ weight: 1, weight_unit: 2, length: 30, width: 20, height: 10, dimension_unit: 2 }]
  )
}
```

**Verifica:** se eseguite questa chiamata, `result` è true e `shipping_price` è un numero. Ripetete con `type` `P` per quotare un ritiro. Un prezzo vuoto significa che il CAP non è in una zona attiva. La creazione non dipende da questo passaggio.

## 3. Creare l’ordine

**REST:** `POST /api/v1/client/orderCreate` — [Manuale REST](/api/documentation#/paths/v1-client-orderCreate/post)

La richiesta deve includere `Idempotency-Key` così un ritentativo non può creare un secondo ordine.

### Consegna (`type` `D`)

```bash
curl -X POST https://YOUR_HOST/api/v1/client/orderCreate \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: dev-local-001" \
  -d '{
    "type": "D",
    "need_pick_up": 0,
    "ref": "DEV-LOCAL-001",
    "name": "Jane Recipient",
    "telephone": "5555555555",
    "email": "jane@example.com",
    "address_1": "2667 boul des sources",
    "city": "Pointe-Claire",
    "province": "QC",
    "postcode": "H9S2H9",
    "country": "Canada",
    "packages": 1,
    "packagesDetail": [{
      "ref": "PKG-001",
      "weight": 1,
      "weight_unit": 2,
      "length": 30,
      "width": 20,
      "height": 10,
      "dimension_unit": 2
    }],
    "delivery_instruction": "Leave at the side door"
  }'
```

### Ritiro (`type` `P`)

Stesso endpoint. L’indirizzo è la fermata di ritiro.

```bash
curl -X POST https://YOUR_HOST/api/v1/client/orderCreate \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: dev-pickup-001" \
  -d '{
    "type": "P",
    "need_pick_up": 1,
    "ref": "DEV-PICKUP-001",
    "name": "Jane Sender",
    "telephone": "5555555555",
    "email": "jane@example.com",
    "address_1": "2667 boul des sources",
    "city": "Pointe-Claire",
    "province": "QC",
    "postcode": "H9S2H9",
    "country": "Canada",
    "packages": 1,
    "packagesDetail": [{
      "ref": "PKG-P-001",
      "weight": 1,
      "weight_unit": 2,
      "length": 30,
      "width": 20,
      "height": 10,
      "dimension_unit": 2
    }],
    "pickup_instruction": "Ring the side bell"
  }'
```

| Campo | Significato |
|---|---|
| `type` | `D` consegna, oppure `P` ritiro |
| `need_pick_up` | `0` — già in magazzino. `1` — un autista deve ritirare il collo |
| `ref` | Riferimento esterno usato per la ricerca e la riconciliazione |
| `name` / address | Consegna: destinatario. Ritiro: fermata di ritiro |
| `auto_deduplication` | `1` rifiuta un secondo collo con la stessa `ref` di collo |

**GraphQL:** `clientOrderCreate` ([Manuale GraphQL](/api/graphql/documentation#/client/clientOrderCreate)) (scalare JSON).

```json
{ "result": true, "id": 12345, "tracking_number": "SR123456789012", "orders_status_id": 2 }
```

`orders_status_id` `2` è Nuovo. Conservate `id`, `tracking_number` e `ref`.

**Verifica:** inviate lo **stesso** corpo con lo stesso `Idempotency-Key` di nuovo. Dovete ottenere lo stesso `id` e non creare un secondo ordine.

L’ordine **non** viene creato quando:

| `code` | Cosa fare |
|---|---|
| `INSUFFICIENT_BALANCE` | `insufficient_balance` contiene required / available / shortfall. Ricaricate, poi ritentate. |
| `OUT_OF_DELIVERY_AREA` | L’indirizzo è fuori dall’area di servizio e l’azienda elimina quegli ordini. Inviate un indirizzo compreso nell’area di servizio. |
| `IDEMPOTENCY_CONFLICT` | La stessa chiave di idempotenza è stata riutilizzata con un corpo della richiesta diverso. Generate una nuova chiave. |

Un ordine fuori zona conservato può comunque restituire `result: true` con `shipping_price: null` e un `warning`. Leggete quel campo.

## 4. Consultare l’ordine

```bash
curl "https://YOUR_HOST/api/v1/orders/list?page=1&per_page=50" \
  -H "Authorization: Bearer ACCESS_TOKEN"

curl https://YOUR_HOST/api/v1/orders/12345 \
  -H "Authorization: Bearer ACCESS_TOKEN"
```

**REST:** `GET /api/v1/orders/list` — tutti gli ordini dell’account, i più recenti per primi, ognuno con i suoi collo e le relative righe articolo. Inviate `page` e `per_page` insieme per paginare (`per_page` al massimo 1000); senza di essi ricevete i 1000 ordini più recenti e un indicatore `truncated`. [Manuale REST](/api/documentation#/paths/v1-orders-list/get)

**GraphQL:** `ordersList` ([Manuale GraphQL](/api/graphql/documentation#/orders/ordersList))

**REST:** `GET /api/v1/orders/{orderId}` — [Manuale REST](/api/documentation#/paths/v1-orders-orderId/get)

**GraphQL:** `orders` ([Manuale GraphQL](/api/graphql/documentation#/orders/orders))

**Verifica:** l’ordine appartiene all’account autenticato. `ref` coincide con il valore che avete creato. `tracking_number` coincide con il passo 3.

## 5. Stampare l’etichetta locale

**REST:** `POST /api/v1/shipping/getShippingLabel` — [Manuale REST](/api/documentation#/paths/v1-shipping-getShippingLabel/post)

```bash
curl -X POST https://YOUR_HOST/api/v1/shipping/getShippingLabel \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "id": "SR123456789012",
    "type": "TRACKING_NUMBER",
    "base64": 1,
    "hide_sender_address": 0,
    "hide_receiver_address": 0
  }'
```

`type`: `TRACKING_NUMBER`, `ORDER_ID` o `REF`. `base64: 0` (predefinito) invia un PDF. Con `base64: 1` l'intero corpo della risposta è una stringa JSON di primo livello che contiene il PDF in base64, non un oggetto con un campo `pdf_data`. Richiamare invece `POST /api/v2/shipping/getShippingLabel` per ricevere l'etichetta all'interno di un normale oggetto JSON.

**GraphQL:** `shippingGetShippingLabel` ([Manuale GraphQL](/api/graphql/documentation#/shipping/shippingGetShippingLabel)). `shippingGetShippingLabelV2` ([Manuale GraphQL](/api/graphql/documentation#/shipping/shippingGetShippingLabelV2)) restituisce sempre JSON (`pdf_data`).

**Verifica:** il PDF si apre. Un’etichetta di consegna mostra il destinatario; un’etichetta di ritiro mostra l’indirizzo di ritiro. Un campo indirizzo nascosto è reso vuoto sull’etichetta.

## 6. Tracciare

Endpoint pubblico di tracciamento; non è richiesto un token di accesso.

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

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

La stessa URL accetta il vostro `ref` quando è stato salvato come numero esterno.

**GraphQL** (tipizzato — serve un selection set):

```graphql
query {
  trackingPublic(trackingNumber: "SR123456789012") {
    result
    deliveried
    returntosender
    rejectedbyrecipient
    postcode
    data {
      tracking_event_status_id
      tracking_event_key
      description
      updated_at_localized
    }
    proofs { file_id type full_url signed_url }
  }
}
```

Diramatevi su `tracking_event_status_id`, non su `description` (quella stringa segue `Accept-Language`):

| `tracking_event_status_id` | Lato | Significato |
|---|---|---|
| `100` | entrambi | Ordine ricevuto |
| `300` / `301` | consegna | In magazzino |
| `450` | consegna | In consegna |
| `500` | consegna | Consegnato |
| `501` | consegna | Consegna fallita, serve un nuovo piano |
| `460` | ritiro | In ritiro |
| `510` | ritiro | Ritirato |
| `512` | ritiro | Ritiro fallito, riprovare più tardi |
| `513` | ritiro | Problema di ritiro |

`data` è dal più recente. Trattate la prima riga come stato attuale. `deliveried: true` dopo `500`.

Su `500` o `510`, `proofs[]` può contenere `type` `1` (firma) o `2` (foto), più `file_id` e `signed_url`. Una foto caricata dopo quell’evento non è in quel payload — iscrivetevi a `pod.files_updated` al passo 7.

**Verifica:** subito dopo la creazione, l’evento più recente è `100` e `deliveried` è false. Un numero sconosciuto è `result: false` / 404 — mostrate uno stato non trovato; non inventate eventi di tracking.

## 7. Configurare le notifiche degli eventi

Configurate gli URL di callback richiesti da questo flusso:

| Impostazione | Evento | Quando |
|---|---|---|
| `order_create_webhook_url` | `order.created` | Persistere `id` e `tracking_number` |
| `order_status_change_webhook_url` | `order.status_change` | Stato visibile al cliente |
| `tracking_event_webhook_url` | `tracking.event` | Cronologia di ritiro o consegna |
| `pod_files_webhook_url` | `pod.files_updated` | Foto/firma dopo il ritiro o la consegna |
| `order_cancel_failed_webhook_url` | `order.cancel_failed` | Un annullamento che avete inviato è stato rifiutato |

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

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

```bash
curl -X PUT https://YOUR_HOST/api/v1/webhook-settings \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "order_create_webhook_url": "https://erp.example.com/hooks/superroute",
    "order_status_change_webhook_url": "https://erp.example.com/hooks/superroute",
    "tracking_event_webhook_url": "https://erp.example.com/hooks/superroute",
    "webhook_sign_secret": "YOUR_LONG_RANDOM_SECRET",
    "webhook_verify_ssl": 1
  }'
```

Verificate **v2** sul corpo grezzo: `HMAC_SHA256(timestamp + "." + raw_body, secret)` contro `X-Webhook-Signature-V2`. Eseguite la deduplicazione su `X-Webhook-Event-Id`. Rispondete **2xx in meno di 3 secondi**.

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

**Verifica:** create un ordine di prova e vedete `order.created` con lo stesso `id` / `tracking_number`. Una firma non valida deve essere rifiutata dal ricevitore con `401`. Un secondo invio dello stesso `X-Webhook-Event-Id` non deve essere elaborato due volte.

## 8. Annullare un ordine di prova

**REST:** `POST /api/v1/orders/cancel` — [Manuale REST](/api/documentation#/paths/v1-orders-cancel/post) — esattamente uno tra `order_id`, `tracking_number`, `external_tracking_number`.

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

Già annullato: `200` con `already_cancelled: true`. **GraphQL:** `ordersCancel` ([Manuale GraphQL](/api/graphql/documentation#/orders/ordersCancel)).

**Verifica:** il tracking pubblico non tratta più la spedizione come attiva. Se l’annullamento è rifiutato (`409`, ad esempio `ORDER_ALREADY_IN_DELIVERY`), scatta `order.cancel_failed`.

## 9. Creazione in lotto (opzionale)

- `POST /api/v1/client/batchOrderCreate` — [Manuale REST](/api/documentation#/paths/v1-client-batchOrderCreate/post) — attende finché ogni riga è stata elaborata.
- `POST /api/v1/client/batchOrderCreateAsync` — [Manuale REST](/api/documentation#/paths/v1-client-batchOrderCreateAsync/post) — restituisce immediatamente un identificatore di job; interrogate `GET /api/v1/client/async/{id}` — [Manuale REST](/api/documentation#/paths/v1-client-async-id/get) o prendete `order.create_async`.

Stessi campi del passo 3, come array di ordini. Ogni riga può essere `type` `D` o `P`. **GraphQL:** `clientBatchOrderCreate` ([Manuale GraphQL](/api/graphql/documentation#/client/clientBatchOrderCreate)) / `clientBatchOrderCreateAsync` ([Manuale GraphQL](/api/graphql/documentation#/client/clientBatchOrderCreateAsync)).

**Verifica:** ogni riga ha il proprio `result`. Usate l’endpoint asincrono quando inviate più di circa 100 righe.

## Elenco di verifica

Usate un `ref` di prova come `DEV-LOCAL-001` / `DEV-PICKUP-001`:

- [ ] (Facoltativo) La quotazione restituisce un prezzo per un CAP in zona con `type` `D`.
- [ ] (Facoltativo) La quotazione restituisce un prezzo per un CAP in zona con `type` `P`.
- [ ] La creazione di una consegna restituisce `id` + `tracking_number`; lo stesso `Idempotency-Key` non crea un secondo ordine.
- [ ] La creazione di un ritiro restituisce `id` + `tracking_number`; `need_pick_up` è `1`.
- [ ] Elenco / dettaglio mostra l’ordine di questo account.
- [ ] Il PDF dell’etichetta locale si apre e mostra il destinatario o l’indirizzo di ritiro.
- [ ] Il tracking pubblico restituisce la cronologia senza token; l’evento più recente è `100`.
- [ ] Arriva `order.created`; la firma v2 verifica.
- [ ] L’annullamento restituisce `result: true` (o `already_cancelled: true` al ritentativo).
