# Usługi wysyłkowe

API usług wysyłkowych pozwala kontu klienta rezerwować usługi wysyłkowe, które jego operator logistyczny skonfigurował i do niego przypisał. Własny system klienta wyświetla usługi, z których może korzystać, wczytuje zasady jednej usługi, wycenia przesyłkę, tworzy zamówienie wysyłki, opłaca je z salda konta i śledzi przesyłkę aż do doręczenia. Ten przewodnik jest przeznaczony dla programistów, którzy łączą system importera, sprzedawcy lub hurtownika z obsługującym go operatorem logistycznym.

## 1. Co można zbudować

Wszystkie przykłady w tym przewodniku korzystają z jednego scenariusza. **Harbourline Imports Inc.**, importer herbaty z Toronto, ma konto klienta u swojego operatora logistycznego. Operator oferuje usługę `intl_express` (International Express) ze swojego magazynu Toronto Hub (identyfikator magazynu `7`). Harbourline dostarcza do Toronto Hub dwa kartony próbek herbaty dla dystrybutora w Seattle w ramach swojego zamówienia zakupu `HLI-PO-1058`.

- **Rezerwacja z systemu zamówień zakupu.** Po zatwierdzeniu zamówienia zakupu system Harbourline wycenia przesyłkę w usłudze `intl_express`, tworzy zamówienie wysyłki z numerem zamówienia zakupu jako referencją i opłaca je z przedpłaconego salda konta, bez otwierania portalu operatora przez kogokolwiek.
- **Kontrola ceny przed zobowiązaniem.** Kupiec w Harbourline widzi fracht, dopłaty, podatek i sumę za dwa kartony przed rezerwacją przesyłki, a przesyłka, której usługa nie potrafi wycenić, zostaje zatrzymana, zanim powstanie zamówienie.
- **Status przesyłki w systemie ERP.** Numer śledzenia każdego kartonu jest zapisywany przy zamówieniu zakupu; webhooki przekazują status zamówienia i oś czasu śledzenia do ERP, a nocne zadanie uzgadnia dane z listą zamówień.
- **Kontrolowane zmiany.** Nieopłaconą rezerwację poprawia się na miejscu, a rezerwację, która nie jest już potrzebna, anuluje się ze zwrotem opłaconej kwoty na kredyt konta.

## 2. Zakres tego przewodnika

Tej rodziny endpointów należy używać, gdy wywołujący jest **klientem** firmy logistycznej i rezerwuje jedną z własnych usług wysyłkowych tej firmy: firma ustala plan cenowy, magazyny, dopłaty i opakowania oraz przypisuje usługi klientowi. Klient widzi i rezerwuje wyłącznie przypisane mu usługi.

W następujących przypadkach należy użyć innej rodziny:

- Wywołującym jest sama firma logistyczna (konto firmowe), która rezerwuje odbiory i dostawy tego samego dnia lub lokalne własną flotą: należy zapoznać się z przewodnikiem **Odbiór i dostawa (własna flota)**.
- Wywołujący kupuje etykiety przewoźników (na przykład UPS lub FedEx) po wynegocjowanych stawkach konta: należy zapoznać się z przewodnikiem **Etykiety przewoźnika**.
- Klient przechowuje towar w magazynie operatora i wysyła go z zapasu: należy zapoznać się z przewodnikiem **Magazynowanie i wydanie**.

**Uniorder: jedno API dla każdej przesyłki** (`/api/v1/uniorder/...`) to zalecany jeden punkt wejścia dla nowych integracji dostawy lokalnej i etykiet przewoźników. Uniorder nie obejmuje usług wysyłkowych: zamówienia usług wysyłkowych tworzy się i obsługuje wyłącznie przez opisane tutaj endpointy `/api/v1/customer/shipping-orders/...`.

## 3. Przed rozpoczęciem

- **Typ konta.** Konto **klienta** firmy logistycznej z **uprawnieniem API** włączonym przez firmę. Konto firmowe ani konto pracownika nie może zalogować się przez opisane poniżej logowanie klienta.
- **Przypisanie usługi.** Firma musi przypisać klientowi co najmniej jedną aktywną usługę wysyłkową. Klient bez przypisanej usługi otrzymuje pustą listę usług.
- **Dane testowe.** Należy uzgodnić z firmą testowy kod usługi, testowy magazyn i niewielkie przedpłacone saldo na koncie testowym. Należy używać referencji takiej jak `HLI-PO-1058` lub `DEV-SHIP-001`, aby zamówienia testowe można było łatwo znaleźć i anulować.
- **Obsługa tokenu.** API należy wywoływać wyłącznie z serwera. Hasła i tokenu dostępu nie należy udostępniać w przeglądarkach ani w klientach mobilnych. Token wygasa tydzień po zalogowaniu (`expires_at`); przed jego wygaśnięciem należy zalogować się ponownie.
- **Symbole zastępcze.** Należy zamienić `YOUR_HOST` na nazwę hosta firmy logistycznej, a `ACCESS_TOKEN` na token zwrócony w kroku logowania.
- **Błędy w JSON.** W każdym żądaniu należy wysyłać `Accept: application/json`, aby błędy walidacji zwracały JSON zamiast przekierowania.

## 4. Logowanie jako klient

Logowanie wymienia adres e-mail i hasło klienta na token bearer. Każde kolejne wywołanie w tym przewodniku wysyła ten token.

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

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

```json
{
  "result": true,
  "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOi...",
  "token_type": "Bearer",
  "expires_at": "2026-10-05 09:15:00",
  "expires_timestamp": 1791206100,
  "name": "Harbourline Imports Inc."
}
```

- `access_token`: należy go wysyłać w każdym żądaniu jako `Authorization: Bearer ACCESS_TOKEN`. GraphQL używa tego samego nagłówka na `POST /api/graphql`.
- `expires_at` / `expires_timestamp`: nowe logowanie należy zaplanować przed tym czasem.

**Weryfikacja:** odpowiedź zawiera `result: true` i `access_token`. Żądanie bez tokenu zwraca `401`; logowanie kontem, które nie jest kontem klienta lub nie ma uprawnienia API, również zwraca `401`.

## 5. Lista usług przypisanych do klienta

Lista usług informuje integrację, które kody usług może rezerwować oraz czy każda usługa przyjmuje dostarczenie do magazynu, odbiór, czy oba warianty. Należy zapisać `service_code`; każde kolejne wywołanie usługi go używa.

**REST:** `GET /api/v1/customer/shipping-orders/services` — [Podręcznik REST](/api/documentation#/paths/v1-customer-shipping-orders-services/get)

```bash
curl https://YOUR_HOST/api/v1/customer/shipping-orders/services \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Accept: application/json"
```

```json
{
  "result": true,
  "data": {
    "services": [
      {
        "id": 12,
        "service_code": "intl_express",
        "name": { "en": "International Express" },
        "offer_pickup": true,
        "allow_warehouse_delivery": true,
        "support_multi_package": true,
        "allow_special_requirements": false,
        "allow_purchase_supplies": true,
        "send_confirmation_email": true,
        "warehouses": [{ "id": 7, "name": "Toronto Hub" }]
      }
    ]
  }
}
```

- `service_code`: parametr ścieżki każdego kolejnego wywołania usługi.
- `offer_pickup` / `allow_warehouse_delivery`: dozwolone wartości `origin_type` (`pickup` / `warehouse`).
- `support_multi_package`: czy jedno zamówienie może zawierać więcej niż jedną pozycję paczek.
- Pusta tablica `services` oznacza, że temu klientowi nie przypisano żadnej usługi.

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

```graphql
query CustomerShippingOrderServices {
  customerShippingOrderServices
}
```

**Weryfikacja:** lista zawiera co najmniej jedną usługę, a jej `service_code` został zapisany (w tym przewodniku: `intl_express`).

## 6. Wczytanie konfiguracji usługi

Konfiguracja zwraca wszystko, czego potrzebuje formularz zamówienia jednej usługi: magazyny przyjmujące dostarczenia, dopłaty do wyboru, katalog opakowań i materiałów, jednostki oraz kraje, z których usługa może odbierać i do których może dostarczać. Przed wyceną lub utworzeniem czegokolwiek należy sprawdzić dane zamówienia pod kątem tej konfiguracji.

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

```bash
curl https://YOUR_HOST/api/v1/customer/shipping-orders/services/intl_express/config \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Accept: application/json"
```

```json
{
  "result": true,
  "data": {
    "service": {
      "service_code": "intl_express",
      "offer_pickup": true,
      "allow_warehouse_delivery": true,
      "allow_special_requirements": false,
      "allow_purchase_supplies": true,
      "weight_mode": 2,
      "volumetric_factor": 5000
    },
    "warehouses": [
      {
        "id": 7,
        "name": "Toronto Hub",
        "address_1": "250 Dundas St W",
        "city": "Toronto",
        "province": "ON",
        "country": "CA",
        "postcode": "M5T 2Z5",
        "telephone": "4165550100"
      }
    ],
    "surcharges": [],
    "has_surcharges": false,
    "packagings": [],
    "products": [],
    "weight_units": { "2": { "name": "weight_kg", "accuracy": 3 } },
    "dimension_units": { "2": { "name": "dimension_cm", "accuracy": 1 } },
    "delivery_allowed_countries": ["CA", "US"],
    "pickup_allowed_countries": ["CA"]
  }
}
```

- `warehouses[].id`: `warehouse_id` do wysłania, gdy `origin_type` ma wartość `warehouse`. Identyfikator spoza tej listy zostaje odrzucony przy tworzeniu.
- `service.weight_mode`: pola paczki wymagane przez plan cenowy: `0` waga rzeczywista (weight), `1` waga gabarytowa (długość, szerokość i wysokość), `2` waga rozliczeniowa (oba zestawy). `null` oznacza, że usługa jest wyceniana ręcznie. Wysłanie wagi i wszystkich trzech wymiarów spełnia każdy tryb.
- `delivery_allowed_countries` / `pickup_allowed_countries`: kraj docelowy lub kraj odbioru spoza tych list należy odrzucić przed wywołaniem wyceny.
- `surcharges[].id`, `packagings[].id`, `products[].id`: identyfikatory opcjonalnych dopłat, opakowań i zakupu materiałów.
- `weight_units` / `dimension_units`: jednostki paczek wysyła się jako liczby. Należy wysyłać `weight_unit: 2` (kg) i `dimension_unit: 2` (cm), jak we wszystkich przykładach w tym przewodniku; obie wartości są też domyślne, gdy pola zostaną pominięte.

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

```graphql
query CustomerShippingOrderServiceConfig {
  customerShippingOrderServiceConfig(serviceCode: "intl_express")
}
```

**Weryfikacja:** `result` ma wartość `true`, a w przypadku dostarczenia do magazynu `warehouses` zawiera magazyn, który ma zostać użyty. `403` oznacza, że usługa nie jest przypisana do tego klienta; `404` oznacza, że kod usługi nie istnieje lub jest nieaktywny.

## 7. Wycena

Wycena oblicza cenę przesyłki według planu cenowego usługi, niczego nie zapisując. Należy pokazać sumę kupcowi i nie tworzyć zamówienia, gdy wycena zgłasza odmowę.

**REST:** `POST /api/v1/customer/shipping-orders/services/{serviceCode}/estimate-price` — [Podręcznik REST](/api/documentation#/paths/v1-customer-shipping-orders-services-serviceCode--estimate-price/post)

```bash
curl -X POST https://YOUR_HOST/api/v1/customer/shipping-orders/services/intl_express/estimate-price \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{
    "origin_type": "warehouse",
    "warehouse_id": 7,
    "delivery_postcode": "98104",
    "delivery_country": "US",
    "packages": [{
      "weight": 12,
      "length": 50,
      "width": 40,
      "height": 35,
      "weight_unit": 2,
      "dimension_unit": 2,
      "quantity": 2
    }]
  }'
```

```json
{
  "result": true,
  "data": {
    "needs_manual_quote": false,
    "shipping_fee": 118.40,
    "shipping_fee_total": 131.20,
    "fuel_surcharge": 12.80,
    "pickup_fee": null,
    "surcharges_total": 0,
    "sub_total": 131.20,
    "tax": { "total_tax": 17.06 },
    "total": 148.26,
    "currency": "CAD",
    "all_fees_calculated": true,
    "has_items_needing_quote": false,
    "unpriced_items": [],
    "refused": false,
    "refusal_message": null
  }
}
```

- `origin_type`: `warehouse` (klient dostarcza towar do magazynu; należy wysłać `warehouse_id`) lub `pickup` (operator odbiera towar; należy wysłać `pickup_postcode` i `pickup_country`). Należy używać wyłącznie wartości dozwolonej w kroku 5.
- `packages`: jedna pozycja na każdą grupę identycznych paczek; `quantity` mnoży pozycję.
- `total` i `currency`: kwota do wyświetlenia. `total` ma wartość `null`, dopóki którakolwiek opłata nie jest obliczona.
- `needs_manual_quote` / `has_items_needing_quote`: firma wycenia zamówienie ręcznie; zamówienie można utworzyć, a opłaca się je po ustaleniu ceny przez firmę.
- `refused` / `refusal_message`: usługa odrzuca przesyłki, których nie potrafi wycenić. Nie należy tworzyć zamówienia; zamiast tego należy wyświetlić `refusal_message`.
- Opcjonalne dane wejściowe: `surcharges`, `products` (mapa identyfikatorów produktów na ilości, uwzględniana tylko wtedy, gdy `allow_purchase_supplies` ma wartość true), `has_special_requirements`, `coupon_code`.

**Weryfikacja:** `result` ma wartość `true`, `refused` ma wartość `false`, a `total` ma wartość lub `needs_manual_quote` ma wartość `true`.

## 8. Utworzenie zamówienia wysyłki

Wywołanie tworzenia rezerwuje przesyłkę w usłudze. Integracja zapisuje zwrócone `id` przy własnym zamówieniu zakupu; każde kolejne wywołanie używa tego identyfikatora.

**REST:** `POST /api/v1/customer/shipping-orders/services/{serviceCode}/orders` — [Podręcznik REST](/api/documentation#/paths/v1-customer-shipping-orders-services-serviceCode--orders/post)

Należy wysłać `Idempotency-Key` utworzony z własnego stałego identyfikatora (tutaj numeru zamówienia zakupu). Ponowienie z tym samym kluczem i tą samą treścią zwraca pierwszą odpowiedź z `"replayed": true` i nagłówkiem `Idempotency-Replayed: true` oraz nie tworzy drugiego zamówienia.

```bash
curl -X POST https://YOUR_HOST/api/v1/customer/shipping-orders/services/intl_express/orders \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -H "Idempotency-Key: hli-po-1058" \
  -d '{
    "origin_type": "warehouse",
    "warehouse_id": 7,
    "reference": "HLI-PO-1058",
    "delivery_name": "Cascade Tea Distributors",
    "delivery_telephone": "2065550143",
    "delivery_email": "receiving@cascadetea.example",
    "delivery_address_1": "300 5th Ave S",
    "delivery_city": "Seattle",
    "delivery_province": "WA",
    "delivery_country": "US",
    "delivery_postcode": "98104",
    "note": "Two cartons of sample tea, dock door B",
    "package": [{
      "description": "Sample tea carton",
      "weight": 12,
      "length": 50,
      "width": 40,
      "height": 35,
      "weight_unit": 2,
      "dimension_unit": 2,
      "value": 380,
      "quantity": 2
    }]
  }'
```

```json
{
  "result": true,
  "data": {
    "id": 9001,
    "status": 0,
    "total_price": 148.26,
    "price_breakdown": { "total": 148.26, "currency": "CAD" },
    "promotion_id": null,
    "tracking_number": null
  }
}
```

- Klucz paczek w treści żądania to przy tworzeniu `package` (przy wycenie `packages`). Każda pozycja z `quantity` równym N staje się N paczkami, a każda paczka otrzymuje własny numer śledzenia.
- Pola wymagane: `delivery_name`, `delivery_telephone`, `delivery_address_1`, `delivery_city`, `delivery_province`, `delivery_country`, `delivery_postcode`, `origin_type`, `package[].weight`; dodatkowo `warehouse_id` dla `warehouse` albo `pickup_name`, `pickup_telephone`, `pickup_address_1`, `pickup_city`, `pickup_province`, `pickup_country`, `pickup_postcode` dla `pickup`.
- Pola opcjonalne: `reference` (zapisywane jako `reference_number` zamówienia), `delivery_email`, `delivery_address_2`, `scheduled_date`, `time_window`, `note`, `special_requirements` (tablica wierszy tekstu, uwzględniana tylko wtedy, gdy usługa na to pozwala), `products`, `surcharges`, `coupon_code`.
- `id`: należy go zapisać. `status` `0` oznacza Pending (oczekuje na płatność).
- `total_price`: kwota pobierana w kroku 9. Wynosi `0`, dopóki zamówienie oczekuje na ręczną wycenę.
- `tracking_number` na poziomie zamówienia ma wartość `null`; numery śledzenia znajdują się na paczkach i odczytuje się je w kroku 10.
- Dla nowego zamówienia endpoint odpowiada HTTP `201`.

**Weryfikacja:** odpowiedź zawiera `result: true` i `id`. Powtórzenie tego samego żądania z tym samym `Idempotency-Key` zwraca to samo `id` z `"replayed": true`.

## 9. Opłacenie zamówienia z salda konta

Zamówienia wysyłki opłaca się w całości z salda konta klienta. Opłacone zamówienie przechodzi ze stanu Pending do stanu Confirmed, a operator rozpoczyna jego obsługę.

Najpierw należy odczytać kwotę:

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

```bash
curl https://YOUR_HOST/api/v1/customer/shipping-orders/9001/payment-info \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Accept: application/json"
```

```json
{
  "result": true,
  "data": {
    "order_id": 9001,
    "currency": "CAD",
    "total_price": "148.26",
    "paid_amount": "0.00",
    "remaining_balance": "148.26",
    "user_balance": "500.00",
    "has_sufficient_balance": true,
    "shortfall": 0,
    "payment_options": [
      { "type": "remaining_balance", "amount": 148.26 }
    ]
  }
}
```

Następnie należy zapłacić:

**REST:** `POST /api/v1/customer/shipping-orders/{id}/pay` — [Podręcznik REST](/api/documentation#/paths/v1-customer-shipping-orders-id--pay/post)

```bash
curl -X POST https://YOUR_HOST/api/v1/customer/shipping-orders/9001/pay \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{"payment_type":"remaining_balance"}'
```

```json
{
  "result": true,
  "message": "Payment of $148.26 processed successfully.",
  "data": {
    "order_id": 9001,
    "status": 1,
    "amount_paid": "148.26"
  }
}
```

- `has_sufficient_balance` / `shortfall`: gdy saldo nie pokrywa `remaining_balance`, przed płatnością należy doładować konto.
- `payment_type`: obsługiwana jest wyłącznie wartość `remaining_balance`; zawsze pobierana jest cała pozostała kwota.
- `data.status` `1` oznacza Confirmed.

**Weryfikacja:** wywołanie płatności zwraca `result: true` i `status` `1`, a drugie wywołanie `payment-info` zwraca `400`, ponieważ zamówienie jest w pełni opłacone. Wywołanie płatności przy niewystarczającym saldzie zwraca `422` i niczego nie pobiera.

## 10. Pobranie zamówienia i śledzenie paczek

Wywołanie szczegółów zwraca bieżący status i numer śledzenia każdej paczki. Numery śledzenia paczek należy zapisać przy zamówieniu zakupu; publiczne śledzenie przyjmuje każdy z nich.

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

```bash
curl https://YOUR_HOST/api/v1/customer/shipping-orders/9001 \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Accept: application/json"
```

```json
{
  "result": true,
  "data": {
    "id": 9001,
    "status": 1,
    "status_name": "Confirmed",
    "can_edit": false,
    "can_cancel": true,
    "shipping_code": "K7RW2Q",
    "tracking_number": null,
    "reference_number": "HLI-PO-1058",
    "origin_type": "warehouse",
    "warehouse_id": 7,
    "delivery_address": { "name": "Cascade Tea Distributors", "city": "Seattle", "country": "US" },
    "package_count": 2,
    "packages": [
      { "id": 55101, "description": "Sample tea carton", "tracking_number": "SR123456789012", "weight": 12, "weight_unit": 2 },
      { "id": 55102, "description": "Sample tea carton", "tracking_number": "SR123456789013", "weight": 12, "weight_unit": 2 }
    ],
    "total_price": 148.26
  }
}
```

- `status`: `0` Pending, `1` Confirmed, `2` In Transit, `3` Shipped, `4` Cancelled, `5` Failed, `6` Partially Picked Up, `7` Picked Up, `8` Processing.
- `can_edit` / `can_cancel`: czy krok 12 jest obecnie dozwolony.
- `packages[].tracking_number`: numery do zapisania i śledzenia.
- `shipping_code`: kod przyjmowany przez ekrany przyjęcia w magazynie; należy go wydrukować na dokumentach dostarczenia.

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

```graphql
query CustomerShippingOrderShow {
  customerShippingOrderShow(id: 9001) {
    result
    message
    data {
      id
      status
      status_name
      can_cancel
      reference_number
      packages {
        id
        tracking_number
        weight
      }
      total_price
    }
  }
}
```

Aby uzgodnić wszystkie zamówienia jednej usługi, na przykład w nocnym zadaniu, należy je wyświetlić z filtrem. Filtr `id` dopasowuje identyfikator zamówienia, numer śledzenia lub referencję.

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

```bash
curl "https://YOUR_HOST/api/v1/customer/shipping-orders/services/intl_express/orders?id=HLI-PO-1058&created_at_from=2026-09-01&per_page=20" \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Accept: application/json"
```

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

Publiczne śledzenie nie wymaga tokenu i zwraca oś czasu zdarzeń jednej paczki:

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

```bash
curl https://YOUR_HOST/api/v1/tracking/SR123456789012 \
  -H "Accept: application/json"
```

```json
{
  "result": true,
  "deliveried": false,
  "data": [
    {
      "tracking_event_status_id": 1,
      "otep_status": "received",
      "description": "Received at warehouse",
      "location_city": "Toronto",
      "updated_at": "2026-09-29 10:42:00"
    }
  ]
}
```

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

```graphql
query TrackingPublic {
  trackingPublic(trackingNumber: "SR123456789012") {
    result
    message
    deliveried
    data {
      tracking_event_status_id
      otep_status
      description
      location_city
      updated_at
    }
  }
}
```

**Weryfikacja:** szczegóły zwracają zamówienie tego klienta z jednym numerem śledzenia na paczkę, a publiczne śledzenie zwraca `result: true` dla numeru śledzenia paczki. Identyfikator zamówienia innego klienta zwraca `404`.

## 11. Odbieranie webhooków

Webhooki dostarczają na serwer utworzenie zamówienia, zmiany statusu i zdarzenia śledzenia, dzięki czemu integracja nie musi odpytywać API. Konto klienta samo konfiguruje swoje adresy URL webhooków i sekret podpisu.

Po utworzeniu zamówienia wysyłki operator tworzy także powiązane zamówienie odbioru dla swojego zespołu dyspozytorskiego. Webhooki są wysyłane dla tego powiązanego zamówienia: jego `ref` ma postać `Shipping-Pickup-{shipping order id}` (na przykład `Shipping-Pickup-9001`), a każda jego paczka zawiera numer śledzenia paczki wysyłki w `external_tracking_number`. Przychodzące zdarzenia należy dopasowywać według tych dwóch pól.

| Ustawienie | Zdarzenie | Działanie integracji |
|---|---|---|
| `order_create_webhook_url` | `order.created` | Powiązać zdarzenie z zamówieniem wysyłki przez `ref` i `packages[].external_tracking_number` |
| `tracking_event_webhook_url` | `tracking.event` | Dołączyć zdarzenie do osi czasu paczki |
| `order_status_change_webhook_url` | `order.status_change` | Zaktualizować status wyświetlany w systemie |

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

```bash
curl -X PUT https://YOUR_HOST/api/v1/webhook-settings \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{
    "order_create_webhook_url": "https://erp.harbourline-imports.example/hooks/order-created",
    "tracking_event_webhook_url": "https://erp.harbourline-imports.example/hooks/tracking",
    "order_status_change_webhook_url": "https://erp.harbourline-imports.example/hooks/status",
    "webhook_sign_secret": "hli-webhook-secret-7f2c9a1e5b"
  }'
```

```json
{
  "result": true,
  "changed_keys": [
    "order_create_webhook_url",
    "tracking_event_webhook_url",
    "order_status_change_webhook_url",
    "webhook_sign_secret"
  ],
  "recipient_type": "customer",
  "settings": {
    "webhook_sign_secret": "*************************1e5b",
    "tracking_event_webhook_url": "https://erp.harbourline-imports.example/hooks/tracking"
  }
}
```

- Zmieniają się tylko przesłane klucze; pusty ciąg znaków usuwa adres URL. `webhook_sign_secret` musi mieć od 16 do 255 znaków, a dopóki sekret jest pusty, żaden webhook nie jest wysyłany.
- `recipient_type` ma wartość `customer` dla konta klienta.

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

```graphql
mutation WebhookSettingsUpdate {
  webhookSettingsUpdate(
    order_status_change_webhook_url: "https://erp.harbourline-imports.example/hooks/status"
    tracking_event_webhook_url: "https://erp.harbourline-imports.example/hooks/tracking"
  )
}
```

Należy zweryfikować podpis **v2** na surowej treści: `HMAC_SHA256(timestamp + "." + raw_body, secret)` względem `X-Webhook-Signature-V2`. Duplikaty należy eliminować według `X-Webhook-Event-Id`. Należy odpowiedzieć **2xx w czasie poniżej 3 sekund** i przetworzyć zdarzenie później.

```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:** po wywołaniu ustawień jedno testowe utworzenie generuje zdarzenie `order.created`, którego `ref` ma postać `Shipping-Pickup-{id}` z identyfikatorem nowego zamówienia wysyłki, a weryfikacja podpisu kończy się powodzeniem.

## 12. Zmiana lub anulowanie zamówienia

Zamówienie można poprawić, dopóki jest w stanie Pending (przed płatnością), i anulować, dopóki jest w stanie Pending lub Confirmed. Anulowanie opłaconego zamówienia zwraca opłaconą kwotę na kredyt konta.

Aby wprowadzić zmianę, należy ponownie wysłać całe zamówienie z tymi samymi polami co w kroku 8. Cena zostaje przeliczona.

**REST:** `PUT /api/v1/customer/shipping-orders/{id}` — [Podręcznik REST](/api/documentation#/paths/v1-customer-shipping-orders-id/put)

```bash
curl -X PUT https://YOUR_HOST/api/v1/customer/shipping-orders/9001 \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{
    "origin_type": "warehouse",
    "warehouse_id": 7,
    "reference": "HLI-PO-1058",
    "delivery_name": "Cascade Tea Distributors",
    "delivery_telephone": "2065550143",
    "delivery_email": "receiving@cascadetea.example",
    "delivery_address_1": "300 5th Ave S",
    "delivery_address_2": "Suite 210",
    "delivery_city": "Seattle",
    "delivery_province": "WA",
    "delivery_country": "US",
    "delivery_postcode": "98104",
    "package": [{
      "description": "Sample tea carton",
      "weight": 12,
      "length": 50,
      "width": 40,
      "height": 35,
      "weight_unit": 2,
      "dimension_unit": 2,
      "value": 380,
      "quantity": 2
    }]
  }'
```

```json
{
  "result": true,
  "data": {
    "id": 9001,
    "status": 0,
    "total_price": 148.26,
    "promotion_id": null,
    "coupon_code": null,
    "promotion_discount": null
  }
}
```

Aby anulować:

**REST:** `POST /api/v1/customer/shipping-orders/{id}/cancel` — [Podręcznik REST](/api/documentation#/paths/v1-customer-shipping-orders-id--cancel/post)

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

```json
{
  "result": true,
  "data": {
    "id": 9001,
    "status": 4,
    "refund_amount": 148.26
  },
  "message": "Order cancelled. $148.26 refunded to your credit."
}
```

- `status` `4` oznacza Cancelled. Powiązane zamówienie odbioru zostaje usunięte.
- `refund_amount`: kwota zwrócona na kredyt konta; `0` dla nieopłaconego zamówienia.
- Przed zaoferowaniem tych działań użytkownikom należy odczytać `can_edit` i `can_cancel` z kroku 10.

**Weryfikacja:** anulowanie zwraca `status` `4`, a szczegóły pokazują `status_name` `Cancelled`. Drugie anulowanie lub anulowanie zamówienia w stanie In Transit lub późniejszym zwraca `403` z komunikatem `This order can no longer be cancelled.`; zmiana opłaconego zamówienia zwraca `403`.

## 13. Obsługa błędów

| Sytuacja | Status HTTP | Kod | Działanie integracji |
|---|---|---|---|
| Brakujący, wygasły lub nieprawidłowy token; logowanie kontem innym niż konto klienta lub bez uprawnienia API | `401` | — | Zalogować się ponownie; jeśli samo logowanie się nie powiedzie, poprosić firmę o sprawdzenie typu konta i uprawnienia API |
| Usługa nie jest przypisana do tego klienta | `403` | — | Ponownie odczytać listę usług (krok 5) i rezerwować wyłącznie przypisane usługi |
| API jest wywoływane z sesji aplikacji platformy, w której aplikacji wyłączono zamówienia wysyłki | `403` | `APP_CAPABILITY_DISABLED` | Poprosić firmę o włączenie zamówień wysyłki dla aplikacji |
| Nieznany lub nieaktywny kod usługi; nie znaleziono identyfikatora zamówienia dla tego klienta | `404` | — | Odświeżyć listę usług; sprawdzić zapisany identyfikator zamówienia |
| Brak wymaganego pola lub nieprawidłowe pole | `422` | — | Odczytać `errors` z treści odpowiedzi, poprawić pola i wysłać ponownie |
| Typ punktu początkowego nieoferowany przez usługę lub magazyn spoza listy usługi | `422` | — | Użyć `origin_type` i `warehouse_id` z kroków 5 i 6 |
| Usługa nie potrafi wycenić przesyłki i odrzuca przesyłki bez wyceny | `422` | `unpriced_refused` | Nic nie zostało utworzone; wyświetlić `message` i nie ponawiać bez zmian |
| Zamówione materiały są niedostępne w magazynie | `422` | — | Odczytać `stock_shortages`, zmniejszyć ilości i wysłać ponownie |
| Ten sam `Idempotency-Key` z inną treścią | `409` | `IDEMPOTENCY_CONFLICT` | Dla nowego zamówienia użyć nowego klucza; nigdy nie używać klucza ponownie dla innej treści |
| Ponowienie w trakcie przetwarzania pierwszego żądania z tym kluczem | `409` | `IDEMPOTENCY_IN_PROGRESS` | Odczekać `Retry-After` sekund, a następnie ponowić z tym samym kluczem i treścią |
| Płatność przy niewystarczającym saldzie | `422` | — | Doładować konto, a następnie ponownie zapłacić |
| Informacje o płatności lub płatność dla zamówienia w pełni opłaconego | `400` | — | Traktować zamówienie jako opłacone; odczytać szczegóły |
| Anulowanie po opuszczeniu przez zamówienie stanu Pending lub Confirmed | `403` | — | Poinformować, że zamówienia nie można już anulować; skontaktować się z firmą |
| Zmiana po płatności | `403` | — | Anulować i utworzyć nowe zamówienie lub skontaktować się z firmą |
| Błąd serwera podczas wyceny, tworzenia, płatności lub anulowania | `500` | — | Ponowić jeden raz; przy tworzeniu ponowić z tym samym `Idempotency-Key` |

## Lista testów

Należy użyć referencji testowej, takiej jak `DEV-SHIP-001` lub `HLI-PO-1058`:

- [ ] Logowanie klienta zwraca `access_token`; żądanie bez tokenu zwraca `401`.
- [ ] Lista usług nie jest pusta i zapisano jeden `service_code`.
- [ ] Konfiguracja zwraca magazyny, jednostki i dozwolone kraje tej usługi, a formularz z nich korzysta.
- [ ] Wycena zwraca `total` (lub `needs_manual_quote: true`), a odrzucona przesyłka nie zostaje utworzona.
- [ ] Utworzenie zwraca `id`; ten sam `Idempotency-Key` z tą samą treścią zwraca to samo `id` z `"replayed": true`.
- [ ] Płatność kończy się powodzeniem i status zmienia się na Confirmed albo potwierdzono, że niewystarczające saldo zwraca `422` i niczego nie pobiera.
- [ ] Szczegóły pokazują zamówienie tego klienta z jednym numerem śledzenia na paczkę, a publiczne śledzenie odnajduje każdą paczkę.
- [ ] Webhooki są skonfigurowane z sekretem podpisu; testowe utworzenie generuje `order.created` z `ref` `Shipping-Pickup-{id}`, a weryfikacja podpisu kończy się powodzeniem.
- [ ] Anulowanie zamówienia testowego zwraca `status` `4` i oczekiwany `refund_amount`; drugie anulowanie zwraca `403`.
