# Odbiór i dostawa

Odbiór i ostatnia mila własną flotą korzystają z tych samych interfejsów API. Utworzenie zamówienia odbioru lub dostawy → druk lokalnej etykiety → śledzenie → subskrypcja powiadomień o zdarzeniach → anulowanie zamówienia testowego.

`type` wybiera punkt: `D` dostawa, `P` odbiór. Wykonaj kroki w kolejności. Wycena jest opcjonalna i nie jest wymagana przed utworzeniem zamówienia.

Zamień `YOUR_HOST` i `ACCESS_TOKEN` na wartości ze swojego środowiska.

## 1. Uwierzytelnienie

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

Umieść zwrócony `access_token` w nagłówku żądania:

```
Authorization: Bearer ACCESS_TOKEN
```

GraphQL używa tego samego nagłówka na `POST /api/graphql`.

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

**Weryfikacja:** logowanie zwraca `access_token`. Kolejne żądania bez tego tokenu zwracają `401`.

## 2. Wycena (opcjonalnie)

Ten krok jest opcjonalny. Zwraca wyłącznie wycenę; zamówienie nie jest tworzone. Utworzenie nie wymaga wcześniejszej wyceny. Ustaw `type` na `D` (dostawa) lub `P` (odbiór).

**REST:** `POST /api/v1/orders/rate` — [Podręcznik 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** (skalar JSON — bez selection set) (`ordersRate` ([Podręcznik 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 }]
  )
}
```

**Weryfikacja:** jeśli wykonasz to wywołanie, `result` jest true, a `shipping_price` jest liczbą. Powtórz z `type` `P`, aby wycenić odbiór. Brak ceny oznacza, że kod pocztowy nie jest w aktywnym obszarze. Utworzenie nie zależy od tego kroku.

## 3. Utworzenie zamówienia

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

Żądanie musi zawierać `Idempotency-Key`, żeby ponowienie nie mogło utworzyć drugiego zamówienia.

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

### Odbiór (`type` `P`)

Ten sam endpoint. Adres to punkt odbioru.

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

| Pole | Znaczenie |
|---|---|
| `type` | `D` dostawa lub `P` odbiór |
| `need_pick_up` | `0` — paczka jest już w magazynie. `1` — kierowca musi odebrać paczkę |
| `ref` | Zewnętrzny numer używany do wyszukiwania i uzgadniania |
| `name` / address | Dostawa: odbiorca. Odbiór: punkt odbioru |
| `auto_deduplication` | `1` odrzuca drugą paczkę z tą samą `ref` paczki |

**GraphQL:** `clientOrderCreate` ([Podręcznik GraphQL](/api/graphql/documentation#/client/clientOrderCreate)) (skalar JSON).

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

`orders_status_id` `2` to Nowe. Zapisz `id`, `tracking_number` i `ref`.

**Weryfikacja:** wyślij **to samo** ciało z tym samym `Idempotency-Key` ponownie. Musisz dostać to samo `id` i nie utworzyć drugiego zamówienia.

Zamówienie **nie** jest tworzone, gdy:

| `code` | Co zrobić |
|---|---|
| `INSUFFICIENT_BALANCE` | `insufficient_balance` zawiera required / available / shortfall. Doładuj, potem spróbuj ponownie. |
| `OUT_OF_DELIVERY_AREA` | Adres leży poza obszarem obsługi, a firma usuwa takie zamówienia. Prześlij adres w obszarze obsługi. |
| `IDEMPOTENCY_CONFLICT` | Ten sam klucz idempotencji został użyty ponownie z innym ciałem żądania. Wystaw nowy klucz. |

Zachowane zamówienie poza obszarem może mimo to zwrócić `result: true` z `shipping_price: null` i `warning`. Przeczytaj to pole.

## 4. Pobranie zamówienia

```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` — wszystkie zamówienia konta, najnowsze na początku, każde wraz z paczkami i pozycjami towarowymi. Przekaż `page` i `per_page` razem, aby stronicować (`per_page` maksymalnie 1000); bez nich otrzymasz 1000 najnowszych zamówień i znacznik `truncated`. [Podręcznik REST](/api/documentation#/paths/v1-orders-list/get)

**GraphQL:** `ordersList` ([Podręcznik GraphQL](/api/graphql/documentation#/orders/ordersList))

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

**GraphQL:** `orders` ([Podręcznik GraphQL](/api/graphql/documentation#/orders/orders))

**Weryfikacja:** zamówienie należy do uwierzytelnionego konta. `ref` zgadza się z wartością, którą utworzyłeś. `tracking_number` zgadza się z krokiem 3.

## 5. Druk lokalnej etykiety

**REST:** `POST /api/v1/shipping/getShippingLabel` — [Podręcznik 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` lub `REF`. `base64: 0` (domyślnie) przesyła PDF. Przy `base64: 1` całe ciało odpowiedzi jest ciągiem JSON najwyższego poziomu zawierającym PDF w base64, a nie obiektem z polem `pdf_data`. Wywołaj zamiast tego `POST /api/v2/shipping/getShippingLabel`, jeśli chcesz otrzymać etykietę wewnątrz zwykłego obiektu JSON.

**GraphQL:** `shippingGetShippingLabel` ([Podręcznik GraphQL](/api/graphql/documentation#/shipping/shippingGetShippingLabel)). `shippingGetShippingLabelV2` ([Podręcznik GraphQL](/api/graphql/documentation#/shipping/shippingGetShippingLabelV2)) zawsze zwraca JSON (`pdf_data`).

**Weryfikacja:** PDF się otwiera. Etykieta dostawy pokazuje odbiorcę; etykieta odbioru pokazuje adres odbioru. Ukryte pole adresu jest na etykiecie puste.

## 6. Śledzenie

Publiczny endpoint śledzenia; token dostępu nie jest wymagany.

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

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

Ten sam URL przyjmuje Twój `ref`, gdy został zapisany jako numer zewnętrzny.

**GraphQL** (typowany — potrzebuje 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 }
  }
}
```

Rozgałęziaj po `tracking_event_status_id`, nie po `description` (ten ciąg podąża za `Accept-Language`):

| `tracking_event_status_id` | Strona | Znaczenie |
|---|---|---|
| `100` | oba | Zamówienie przyjęte |
| `300` / `301` | dostawa | W placówce |
| `450` | dostawa | W drodze do dostawy |
| `500` | dostawa | Dostarczone |
| `501` | dostawa | Dostawa nieudana, potrzebny nowy plan |
| `460` | odbiór | W drodze po odbiór |
| `510` | odbiór | Odebrane |
| `512` | odbiór | Odbiór nieudany, spróbuj później |
| `513` | odbiór | Problem z odbiorem |

`data` jest od najnowszego. Traktuj pierwszy wiersz jako bieżący. `deliveried: true` po `500`.

Przy `500` lub `510` `proofs[]` może zawierać `type` `1` (podpis) lub `2` (zdjęcie) oraz `file_id` i `signed_url`. Zdjęcie wgrane po tym zdarzeniu nie jest w tym payloadzie — subskrybuj `pod.files_updated` w kroku 7.

**Weryfikacja:** zaraz po utworzeniu najnowsze zdarzenie to `100`, a `deliveried` jest false. Nieznany numer to `result: false` / 404 — pokaż stan nie znaleziono; nie wymyślaj zdarzeń śledzenia.

## 7. Konfiguracja powiadomień o zdarzeniach

Skonfiguruj URL-e zwrotne wymagane przez ten przepływ:

| Ustawienie | Zdarzenie | Kiedy |
|---|---|---|
| `order_create_webhook_url` | `order.created` | Zapisać `id` i `tracking_number` |
| `order_status_change_webhook_url` | `order.status_change` | Status widoczny dla klienta |
| `tracking_event_webhook_url` | `tracking.event` | Oś czasu odbioru lub dostawy |
| `pod_files_webhook_url` | `pod.files_updated` | Zdjęcie/podpis po odbiorze lub dostawie |
| `order_cancel_failed_webhook_url` | `order.cancel_failed` | Anulowanie, które wysłałeś, zostało odrzucone |

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

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

Weryfikuj **v2** na surowym ciele: `HMAC_SHA256(timestamp + "." + raw_body, secret)` wobec `X-Webhook-Signature-V2`. Deduplikuj po `X-Webhook-Event-Id`. Odpowiedz **2xx w mniej niż 3 sekundy**.

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

**Weryfikacja:** utwórz jedno zamówienie testowe i zobacz `order.created` z tym samym `id` / `tracking_number`. Nieprawidłowy podpis musi zostać odrzucony przez odbiorcę kodem `401`. Drugie dostarczenie tego samego `X-Webhook-Event-Id` nie może zostać przetworzone dwa razy.

## 8. Anulowanie zamówienia testowego

**REST:** `POST /api/v1/orders/cancel` — [Podręcznik REST](/api/documentation#/paths/v1-orders-cancel/post) — dokładnie jedno z `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"}'
```

Już anulowane: `200` z `already_cancelled: true`. **GraphQL:** `ordersCancel` ([Podręcznik GraphQL](/api/graphql/documentation#/orders/ordersCancel)).

**Weryfikacja:** publiczne śledzenie nie traktuje już przesyłki jako aktywnej. Jeśli anulowanie jest odrzucone (`409`, np. `ORDER_ALREADY_IN_DELIVERY`), odpala się `order.cancel_failed`.

## 9. Tworzenie partii (opcjonalnie)

- `POST /api/v1/client/batchOrderCreate` — [Podręcznik REST](/api/documentation#/paths/v1-client-batchOrderCreate/post) — czeka, aż każdy wiersz zostanie przetworzony.
- `POST /api/v1/client/batchOrderCreateAsync` — [Podręcznik REST](/api/documentation#/paths/v1-client-batchOrderCreateAsync/post) — zwraca natychmiast identyfikator zadania; odpytuj `GET /api/v1/client/async/{id}` — [Podręcznik REST](/api/documentation#/paths/v1-client-async-id/get) lub bierz `order.create_async`.

Te same pola co w kroku 3, jako tablica zamówień. Każdy wiersz może mieć `type` `D` lub `P`. **GraphQL:** `clientBatchOrderCreate` ([Podręcznik GraphQL](/api/graphql/documentation#/client/clientBatchOrderCreate)) / `clientBatchOrderCreateAsync` ([Podręcznik GraphQL](/api/graphql/documentation#/client/clientBatchOrderCreateAsync)).

**Weryfikacja:** każdy wiersz ma własne `result`. Użyj asynchronicznego endpointu, gdy wysyłasz więcej niż około 100 wierszy.

## Lista testów

Użyj testowego `ref` takiego jak `DEV-LOCAL-001` / `DEV-PICKUP-001`:

- [ ] (Opcjonalnie) Wycena zwraca cenę dla kodu pocztowego w obszarze z `type` `D`.
- [ ] (Opcjonalnie) Wycena zwraca cenę dla kodu pocztowego w obszarze z `type` `P`.
- [ ] Utworzenie dostawy zwraca `id` + `tracking_number`; ten sam `Idempotency-Key` nie tworzy drugiego zamówienia.
- [ ] Utworzenie odbioru zwraca `id` + `tracking_number`; `need_pick_up` ma wartość `1`.
- [ ] Lista / szczegół pokazuje zamówienie na tym koncie.
- [ ] PDF lokalnej etykiety otwiera się i pokazuje odbiorcę lub adres odbioru.
- [ ] Publiczne śledzenie zwraca oś czasu bez tokenu; najnowsze zdarzenie to `100`.
- [ ] Przychodzi `order.created`; podpis v2 weryfikuje się.
- [ ] Anulowanie zwraca `result: true` (lub `already_cancelled: true` przy ponowieniu).
