# Recolha e entrega

A recolha e a última milha com frota própria usam as mesmas API. Criar um pedido de recolha ou de entrega → imprimir a etiqueta local → rastrear → subscrever as notificações de eventos → cancelar um pedido de teste.

`type` seleciona o ponto: `D` entrega, `P` recolha. Execute os passos em sequência. A cotação é opcional e não é exigida antes de criar o pedido.

Substitua `YOUR_HOST` e `ACCESS_TOKEN` pelos valores do seu ambiente.

## 1. Autenticação

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

Coloque o `access_token` devolvido no cabeçalho do pedido:

```
Authorization: Bearer ACCESS_TOKEN
```

O GraphQL usa o mesmo cabeçalho em `POST /api/graphql`.

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

**Verificação:** o login devolve `access_token`. Os pedidos posteriores sem este token devolvem `401`.

## 2. Cotar (opcional)

Este passo é opcional. Devolve apenas uma cotação; nenhum pedido é criado. A criação não exige uma cotação prévia. Defina `type` como `D` (entrega) ou `P` (recolha).

**REST:** `POST /api/v1/orders/rate` — [Manual 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** (escalar JSON — sem selection set) (`ordersRate` ([Manual 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ção:** se executar esta chamada, `result` é true e `shipping_price` é um número. Repita com `type` `P` para cotar uma recolha. Um preço vazio significa que o código postal não está numa zona ativa. A criação não depende deste passo.

## 3. Criar o pedido

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

O pedido HTTP deve incluir `Idempotency-Key` para que uma repetição não possa criar um segundo pedido.

### Entrega (`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"
  }'
```

### Recolha (`type` `P`)

O mesmo endpoint. O endereço é o ponto de recolha.

```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 | Significado |
|---|---|
| `type` | `D` entrega, ou `P` recolha |
| `need_pick_up` | `0` — já está no armazém. `1` — um motorista deve recolher o volume |
| `ref` | Referência externa usada para pesquisa e reconciliação |
| `name` / address | Entrega: destinatário. Recolha: ponto de recolha |
| `auto_deduplication` | `1` recusa um segundo volume com a mesma `ref` de volume |

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

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

`orders_status_id` `2` é Novo. Guarde `id`, `tracking_number` e `ref`.

**Verificação:** envie o **mesmo** corpo com a mesma `Idempotency-Key` outra vez. Deve obter o mesmo `id` e não criar um segundo pedido.

O pedido **não** é criado quando:

| `code` | O que fazer |
|---|---|
| `INSUFFICIENT_BALANCE` | `insufficient_balance` contém required / available / shortfall. Carregue saldo e tente de novo. |
| `OUT_OF_DELIVERY_AREA` | O endereço está fora da área de serviço e a empresa apaga esses pedidos. Envie um endereço dentro da área de serviço. |
| `IDEMPOTENCY_CONFLICT` | A mesma chave de idempotência foi reutilizada com um corpo de pedido diferente. Emita uma chave nova. |

Um pedido fora de zona que é conservado pode mesmo assim devolver `result: true` com `shipping_price: null` e um `warning`. Leia esse campo.

## 4. Consultar o pedido

```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` — todos os pedidos da conta, os mais recentes primeiro, cada um com as suas encomendas e respetivas linhas de artigos. Envie `page` e `per_page` em conjunto para paginar (`per_page` no máximo 1000); sem eles recebe os 1000 pedidos mais recentes e um indicador `truncated`. [Manual REST](/api/documentation#/paths/v1-orders-list/get)

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

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

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

**Verificação:** o pedido pertence à conta autenticada. `ref` coincide com o valor que criou. `tracking_number` coincide com o passo 3.

## 5. Imprimir a etiqueta local

**REST:** `POST /api/v1/shipping/getShippingLabel` — [Manual 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` ou `REF`. `base64: 0` (predefinição) envia um PDF. Com `base64: 1`, todo o corpo da resposta é uma cadeia JSON de nível superior que contém o PDF em base64, e não um objeto com um campo `pdf_data`. Chame antes `POST /api/v2/shipping/getShippingLabel` se quiser receber a etiqueta dentro de um objeto JSON normal.

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

**Verificação:** o PDF abre. Uma etiqueta de entrega mostra o destinatário; uma etiqueta de recolha mostra o endereço de recolha. Um campo de endereço oculto é apresentado em branco na etiqueta.

## 6. Rastrear

Endpoint público de rastreio; não é necessário um token de acesso.

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

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

O mesmo URL aceita o seu `ref` quando foi guardado como número externo.

**GraphQL** (tipado — precisa de 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 }
  }
}
```

Ramifique por `tracking_event_status_id`, não por `description` (essa cadeia segue `Accept-Language`):

| `tracking_event_status_id` | Lado | Significado |
|---|---|---|
| `100` | ambos | Pedido recebido |
| `300` / `301` | entrega | Nas instalações |
| `450` | entrega | Em entrega |
| `500` | entrega | Entregue |
| `501` | entrega | Entrega falhada, é preciso um plano novo |
| `460` | recolha | Em recolha |
| `510` | recolha | Recolhido |
| `512` | recolha | Recolha falhada, tente mais tarde |
| `513` | recolha | Problema de recolha |

`data` vai do mais recente. Trate a primeira linha como atual. `deliveried: true` depois de `500`.

Em `500` ou `510`, `proofs[]` pode trazer `type` `1` (assinatura) ou `2` (foto), mais `file_id` e `signed_url`. Uma foto carregada depois desse evento não está nesse payload — subscreva `pod.files_updated` no passo 7.

**Verificação:** logo após criar, o evento mais recente é `100` e `deliveried` é false. Um número desconhecido é `result: false` / 404 — mostre um estado não encontrado; não invente eventos de rastreio.

## 7. Configurar notificações de eventos

Configure os URLs de callback exigidos por este fluxo:

| Definição | Evento | Quando |
|---|---|---|
| `order_create_webhook_url` | `order.created` | Persistir `id` e `tracking_number` |
| `order_status_change_webhook_url` | `order.status_change` | Estado visível ao cliente |
| `tracking_event_webhook_url` | `tracking.event` | Linha temporal de recolha ou entrega |
| `pod_files_webhook_url` | `pod.files_updated` | Foto/assinatura após a recolha ou a entrega |
| `order_cancel_failed_webhook_url` | `order.cancel_failed` | Um cancelamento que enviou foi recusado |

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

**GraphQL:** `webhookSettingsUpdate` ([Manual 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
  }'
```

Verifique **v2** sobre o corpo em bruto: `HMAC_SHA256(timestamp + "." + raw_body, secret)` contra `X-Webhook-Signature-V2`. Deduplique em `X-Webhook-Event-Id`. Responda **2xx em 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;
}
```

**Verificação:** crie um pedido de teste e veja `order.created` com o mesmo `id` / `tracking_number`. Uma assinatura inválida deve ser recusada pelo recetor com `401`. Uma segunda entrega do mesmo `X-Webhook-Event-Id` não deve ser processada duas vezes.

## 8. Cancelar um pedido de teste

**REST:** `POST /api/v1/orders/cancel` — [Manual REST](/api/documentation#/paths/v1-orders-cancel/post) — exatamente um de `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"}'
```

Já cancelado: `200` com `already_cancelled: true`. **GraphQL:** `ordersCancel` ([Manual GraphQL](/api/graphql/documentation#/orders/ordersCancel)).

**Verificação:** o rastreio público já não trata o envio como ativo. Se o cancelamento for recusado (`409`, por exemplo `ORDER_ALREADY_IN_DELIVERY`), dispara `order.cancel_failed`.

## 9. Criação em lote (opcional)

- `POST /api/v1/client/batchOrderCreate` — [Manual REST](/api/documentation#/paths/v1-client-batchOrderCreate/post) — espera até que cada linha tenha sido processada.
- `POST /api/v1/client/batchOrderCreateAsync` — [Manual REST](/api/documentation#/paths/v1-client-batchOrderCreateAsync/post) — devolve de imediato um identificador de job; consulte `GET /api/v1/client/async/{id}` — [Manual REST](/api/documentation#/paths/v1-client-async-id/get) ou receba `order.create_async`.

Os mesmos campos do passo 3, como array de pedidos. Cada linha pode ser `type` `D` ou `P`. **GraphQL:** `clientBatchOrderCreate` ([Manual GraphQL](/api/graphql/documentation#/client/clientBatchOrderCreate)) / `clientBatchOrderCreateAsync` ([Manual GraphQL](/api/graphql/documentation#/client/clientBatchOrderCreateAsync)).

**Verificação:** cada linha tem o seu `result`. Use o endpoint assíncrono ao enviar mais de cerca de 100 linhas.

## Lista de testes

Use um `ref` de teste como `DEV-LOCAL-001` / `DEV-PICKUP-001`:

- [ ] (Opcional) A cotação devolve um preço para um código postal na zona com `type` `D`.
- [ ] (Opcional) A cotação devolve um preço para um código postal na zona com `type` `P`.
- [ ] Criar uma entrega devolve `id` + `tracking_number`; a mesma `Idempotency-Key` não cria um segundo pedido.
- [ ] Criar uma recolha devolve `id` + `tracking_number`; `need_pick_up` é `1`.
- [ ] Lista / detalhe mostra o pedido desta conta.
- [ ] O PDF da etiqueta local abre e mostra o destinatário ou o endereço de recolha.
- [ ] O rastreio público devolve a linha temporal sem token; o evento mais recente é `100`.
- [ ] Chega `order.created`; a assinatura v2 verifica.
- [ ] Cancelar devolve `result: true` (ou `already_cancelled: true` na repetição).
