# Wycena i zamówienie w jednym przepływie

Ten przewodnik omawia API Uniorder (`/api/v1/uniorder/...`) żądanie po żądaniu, w kolejności, w jakiej buduje się integrację: uwierzytelnienie, wycena, utworzenie z wybranym `rate_id`, druk etykiety, odczyt, śledzenie i anulowanie zamówienia oraz przetwarzanie przesyłek w partiach. Jedna wycena wymienia wszystkie sposoby, w jakie konto może wysłać paczkę: dostawę realizowaną przez samą firmę oraz, na żądanie, wszystkie usługi etykiet przewoźników. Zamówienie z `rate_id` tworzy zamówienie dla tej usługi: zamówienie dostawy lub zamówienie etykiety z etykietą kupioną w wycenionej usłudze przewoźnika. Przewodnik jest przeznaczony dla twórców sklepów internetowych, systemów zarządzania zamówieniami i systemów ERP, które wysyłają przesyłki przez konto firmowe.

## 1. Co można zbudować

Poniższe przykłady dotyczą jednej firmy: **Fleurs du Plateau**, kwiaciarni pod adresem 4500 Rue Saint-Denis, Montreal (H2J 2L3), która sprzedaje bukiety online. Typowa przesyłka to jedno pudełko o wadze 1,2 kg i wymiarach 40 × 25 × 25 cm, wysyłane do Jane Recipient pod adres 6841 Rue Saint-Denis, Montreal (H2S 2S3), w ramach zamówienia internetowego `WEB-10045`.

- **Kasa, która oferuje każdą opcję wysyłki.** Sklep wycenia paczkę jeden raz i pokazuje lokalną dostawę tego samego dnia obok każdej usługi etykiet przewoźników dostępnej na koncie, każdą z jej ceną, a następnie tworzy zamówienie z opcją wybraną przez klienta.
- **Automatyczny druk etykiet.** Po utworzeniu zamówienia sklep pobiera plik PDF etykiety i wysyła go do drukarki na stanowisku pakowania, niezależnie od tego, czy paczkę dostarcza firma, czy przewoźnik.
- **Strona zamówienia ze śledzeniem na bieżąco.** Strona zamówienia klienta pokazuje status i oś czasu zdarzeń przesyłki, a po dostarczeniu bukietu także potwierdzenie dostawy.
- **Nocna partia z systemu ERP.** Hurtowe zamówienia z danego dnia są wyceniane i tworzone w jednym zadaniu w kolejce, obejmującym do 500 wierszy, a każdy wynik jest przypisywany do pozycji zamówienia na podstawie `reference`.

## 2. Zakres tego przewodnika

Jest to przewodnik krok po kroku po API Uniorder. Przegląd tego, co oferuje Uniorder i dlaczego, znajduje się w przewodniku **Uniorder: jedno API dla każdej przesyłki**; ten przewodnik podaje żądania, odpowiedzi i sprawdzenia dla każdego wywołania.

Uniorder jest zalecanym jedynym punktem wejścia dla nowych integracji, które wysyłają paczki dostawą lokalną lub z etykietą przewoźnika: zastępuje oddzielne wywołania API dostawy lokalnej i API etykiet przewoźników jednym kształtem żądania. Wcześniejsze endpointy opisane w przewodnikach **Odbiór i dostawa (własna flota)** oraz **Etykiety przewoźnika** pozostają dostępne i niezmienione. Uniorder nie dotyczy usług wysyłkowych rezerwowanych przez konto klienta końcowego ani zamówień magazynowania i wydania; do nich służą przewodniki **Usługi wysyłkowe** oraz **Magazynowanie i wydanie**.

## 3. Przed rozpoczęciem

- **Konto.** Należy użyć konta firmowego (klienta) lub konta pracownika firmy z uprawnieniem do API. Konto klienta końcowego firmy również może wywoływać Uniorder i zawsze jest wyceniane i rozliczane we własnym imieniu. Utworzenie zamówienia dostawy wymaga uprawnienia do składania zamówień.
- **Klienci końcowi.** Konto klienta lub pracownika może wyceniać i składać zamówienia dla jednego ze swoich klientów końcowych, podając w wycenie `customer_id` lub `customer_code`; `rate_id` przenosi wtedy tego klienta końcowego, a cena odpowiada jego planowi cenowemu.
- **Usługi etykiet.** Aby otrzymywać stawki `label_service`, konto (lub wskazany klient końcowy) musi mieć skonfigurowane co najmniej jedno konto przewoźnika etykiet.
- **Dane testowe.** Dla stawek `self_delivery` należy użyć adresu w obszarze dostaw firmy oraz testowych referencji, takich jak `WEB-10045`, które można później anulować.
- **Tokeny.** Token dostępu należy pobierać na własnym serwerze i tam go przechowywać. Nie wolno go wysyłać do przeglądarki ani aplikacji mobilnej.
- **Symbole zastępcze.** Należy zamienić `YOUR_HOST` na host API danego środowiska, a `ACCESS_TOKEN` na token z kroku 4.

## 4. Uwierzytelnienie

Każde wywołanie Uniorder jest wykonywane w imieniu konta. Należy zalogować się raz z własnego serwera, zapisać zwrócony token i wysyłać go w każdym żądaniu.

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

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

```json
{
  "result": true,
  "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9..."
}
```

- `access_token`: należy umieścić go w nagłówku każdego kolejnego żądania:

```
Authorization: Bearer ACCESS_TOKEN
```

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

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

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

## 5. Wycena wszystkich usług

Wycena wymienia wszystkie sposoby wysłania paczki, każdy z ceną i `rate_id`. Kasa pokazuje stawki jako opcje; nic nie jest tworzone ani rezerwowane.

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

Nadawca i odbiorca to pełne adresy; opcjonalne są tylko `from_address_2` i `to_address_2`. Każda paczka wymaga `weight`, `length`, `width` i `height`. Aby dodać usługi etykiet przewoźników, należy ustawić `quote_labels` na `true`; wtedy wymagane są nazwy i telefony obu stron. Konto klienta lub pracownika może wycenić przesyłkę dla jednego ze swoich klientów końcowych przez `customer_id` lub `customer_code`. Okno dostawy (`time_window_start`, `time_window_end`, format `YYYY-MM-DD HH:MM:SS`) jest uwzględniane, gdy cena od niego zależy.

```bash
curl -X POST https://YOUR_HOST/api/v1/uniorder/rate \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "D",
    "from_name": "Fleurs du Plateau",
    "from_telephone": "5145550100",
    "from_address": "4500 Rue Saint-Denis",
    "from_city": "Montreal",
    "from_province": "QC",
    "from_country": "CA",
    "from_postcode": "H2J2L3",
    "to_name": "Jane Recipient",
    "to_telephone": "5145550199",
    "to_address": "6841 Rue Saint-Denis",
    "to_address_2": "Apt 2",
    "to_city": "Montreal",
    "to_province": "QC",
    "to_country": "CA",
    "to_postcode": "H2S2S3",
    "time_window_start": "2026-10-02 13:00:00",
    "time_window_end": "2026-10-02 17:00:00",
    "quote_labels": true,
    "packages": [{
      "weight": 1.2,
      "weight_unit": 2,
      "length": 40,
      "width": 25,
      "height": 25,
      "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,
  "rates": [
    {
      "rate_id": "eyJpdiI6Ik1rT2Z...",
      "type": "self_delivery",
      "channel_id": null,
      "shipping_price": "14.60",
      "currency": "CAD",
      "price_details": { "shipping_fee": 12.92, "sub_total": "12.92" },
      "warning": null
    },
    {
      "rate_id": "eyJpdiI6IlpxR0...",
      "type": "label_service",
      "shipping_price": "18.40",
      "currency": "CAD",
      "shipping_method_id": 72,
      "shipping_method_name": "UPS",
      "carrier_name": "ups",
      "service_code": "ups_standard",
      "service_name": "UPS STANDARD",
      "transit_days": 3
    }
  ],
  "errors": []
}
```

- `type` `self_delivery`: dostawa realizowana przez firmę. Najwyżej jedna na wycenę.
- `type` `label_service`: jedna na każdą usługę każdego konta etykiet. Klientowi należy pokazać `service_name`, `shipping_price` i `transit_days`.
- `errors` wymienia to, czego nie udało się wycenić, wraz z `type`. Adres poza obszarem dostaw to błąd typu `self_delivery` z kodem `OUT_OF_DELIVERY_AREA`; należy wtedy pokazać tylko usługi etykiet.
- `rate_id` jest ważny przez 30 minut i tylko dla konta, które zażądało wyceny. Należy go przechowywać razem z sesją kasy.
- `result` ma wartość `true`, gdy znaleziono co najmniej jedną stawkę.

**GraphQL:** `uniorderRate` ([Podręcznik GraphQL](/api/graphql/documentation#/orders/uniorderRate)). Odpowiedź jest skalarem JSON, więc operacja nie ma selection set.

```graphql
mutation QuoteBouquet($packages: [Json]!) {
  uniorderRate(
    type: "D"
    from_name: "Fleurs du Plateau"
    from_telephone: "5145550100"
    from_address: "4500 Rue Saint-Denis"
    from_city: "Montreal"
    from_province: "QC"
    from_country: "CA"
    from_postcode: "H2J2L3"
    to_name: "Jane Recipient"
    to_telephone: "5145550199"
    to_address: "6841 Rue Saint-Denis"
    to_city: "Montreal"
    to_province: "QC"
    to_country: "CA"
    to_postcode: "H2S2S3"
    quote_labels: true
    packages: $packages
  )
}
```

Zmienne:

```json
{ "packages": [{ "weight": 1.2, "weight_unit": 2, "length": 40, "width": 25, "height": 25, "dimension_unit": 2 }] }
```

**Weryfikacja:** `rates` zawiera stawkę `self_delivery` dla adresu w obszarze oraz, przy `quote_labels`, jedną stawkę `label_service` na każdą usługę przewoźnika. Nic nie jest tworzone.

## 6. Utworzenie zamówienia po wybranej stawce

Gdy klient zapłaci, sklep tworzy zamówienie z `rate_id` wybranej opcji i tą samą przesyłką. O usłudze decyduje `rate_id`; nic innego w żądaniu jej nie wybiera.

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

Przy każdym tworzeniu należy wysyłać nagłówek `Idempotency-Key`, unikalny dla każdego zamówienia. Ponowienie z tym samym kluczem i tą samą treścią zwraca pierwszą odpowiedź z `replayed` `true` i nie tworzy drugiego zamówienia.

```bash
curl -X POST https://YOUR_HOST/api/v1/uniorder \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: checkout-10045" \
  -d '{
    "rate_id": "eyJpdiI6Ik1rT2Z...",
    "ref": "WEB-10045",
    "type": "D",
    "from_name": "Fleurs du Plateau",
    "from_telephone": "5145550100",
    "from_address": "4500 Rue Saint-Denis",
    "from_city": "Montreal",
    "from_province": "QC",
    "from_country": "CA",
    "from_postcode": "H2J2L3",
    "to_name": "Jane Recipient",
    "to_telephone": "5145550199",
    "to_email": "jane@example.com",
    "to_address": "6841 Rue Saint-Denis",
    "to_address_2": "Apt 2",
    "to_city": "Montreal",
    "to_province": "QC",
    "to_country": "CA",
    "to_postcode": "H2S2S3",
    "time_window_start": "2026-10-02 13:00:00",
    "time_window_end": "2026-10-02 17:00:00",
    "delivery_instruction": "Ring the bell at the side door.",
    "packages": [{
      "ref": "WEB-10045-1",
      "weight": 1.2,
      "weight_unit": 2,
      "length": 40,
      "width": 25,
      "height": 25,
      "dimension_unit": 2
    }]
  }'
```

Stawka `self_delivery` tworzy zamówienie dostawy. Dla `type` `D` punktem jest odbiorca; należy ustawić `need_pick_up` na `1`, aby paczka została odebrana u nadawcy. Dla `type` `P` punktem jest nadawca.

```json
{
  "result": true,
  "type": "self_delivery",
  "id": 123456,
  "ref": "WEB-10045",
  "tracking_numbers": ["SR26092800001"],
  "shipping_price": "14.60",
  "quoted_price": "14.60",
  "currency": "CAD"
}
```

Stawka `label_service` tworzy zamówienie etykiety i kupuje etykietę w wycenionej usłudze przewoźnika. `type` musi mieć wartość `D`, a `from_name`, `from_telephone`, `to_name` i `to_telephone` są wymagane. Gdyby klient wybrał UPS STANDARD, odpowiedź wyglądałaby tak:

```json
{
  "result": true,
  "type": "label_service",
  "id": 123457,
  "ref": "WEB-10045",
  "tracking_numbers": ["SR26092800002"],
  "shipping_price": "18.40",
  "quoted_price": "18.40",
  "currency": "CAD",
  "label": {
    "carrier_name": "ups",
    "service_code": "ups_standard",
    "main_tracking_number": "1Z999AA10123456784",
    "label_status": "ready",
    "shipping_label": "JVBERi0xLjQK..."
  }
}
```

- `id`: należy je zapisać razem z zamówieniem internetowym; używa go każde kolejne wywołanie.
- `tracking_numbers`: własne numery śledzenia przesyłki, jeden na paczkę.
- `shipping_price`: naliczona cena. Cena zamówienia jest ustalana przy jego utworzeniu; `quoted_price` to cena z wyceny. Obie mogą się różnić.
- `label.main_tracking_number` i `label.shipping_label` (tylko zamówienie etykiety): numer śledzenia przewoźnika i plik PDF etykiety w base64.
- `result` `false` z kodem `LABEL_PURCHASE_FAILED` (tylko zamówienie etykiety): zamówienie istnieje, ale nie ma etykiety. Należy zachować `id` i przejść do kroku 11.

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

```graphql
mutation CreateBouquetOrder($packages: [Json]!) {
  uniorderCreate(
    rate_id: "eyJpdiI6Ik1rT2Z..."
    ref: "WEB-10045"
    type: "D"
    from_name: "Fleurs du Plateau"
    from_telephone: "5145550100"
    from_address: "4500 Rue Saint-Denis"
    from_city: "Montreal"
    from_province: "QC"
    from_country: "CA"
    from_postcode: "H2J2L3"
    to_name: "Jane Recipient"
    to_telephone: "5145550199"
    to_address: "6841 Rue Saint-Denis"
    to_city: "Montreal"
    to_province: "QC"
    to_country: "CA"
    to_postcode: "H2S2S3"
    packages: $packages
  )
}
```

Zmienne:

```json
{ "packages": [{ "ref": "WEB-10045-1", "weight": 1.2, "weight_unit": 2, "length": 40, "width": 25, "height": 25, "dimension_unit": 2 }] }
```

**Weryfikacja:** `result` ma wartość `true`, a `id` jest ustawione. `rate_id`, który wygasł lub należy do innego konta, zwraca `400` z kodem `RATE_ID_INVALID` i nic nie jest tworzone.

## 7. Druk etykiety

Stanowisko pakowania drukuje etykietę, gdy tylko zamówienie istnieje. To samo wywołanie zwraca własną etykietę firmy dla zamówienia dostawy oraz kupioną etykietę przewoźnika dla zamówienia etykiety.

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

```bash
curl "https://YOUR_HOST/api/v1/uniorder/123456/label?hide_sender_address=0" \
  -H "Authorization: Bearer ACCESS_TOKEN"
```

```json
{
  "result": true,
  "type": "self_delivery",
  "id": 123456,
  "format": "pdf",
  "tracking_numbers": ["SR26092800001"],
  "pdf_data": "JVBERi0xLjQK..."
}
```

- `pdf_data`: plik PDF etykiety w base64. Należy go zdekodować i wysłać plik do drukarki.
- `hide_sender_address`, `hide_receiver_address` (`1`, aby ukryć): dotyczą własnej etykiety firmy dla zamówienia dostawy.
- `label_status` (zamówienie etykiety): `ready`, gdy plik zostaje zwrócony. Gdy przewoźnik jeszcze nie wygenerował pliku, odpowiedź to `200` z `result` `false` i `label_status` `pending`; etykietę należy pobrać ponownie później.
- To wywołanie nigdy nie kupuje etykiety: etykieta, która nie została kupiona, zwraca `409` z kodem `LABEL_PURCHASE_FAILED`. Należy ją kupić w kroku 11.

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

```graphql
query {
  uniorderLabel(order_id: 123456, hide_sender_address: 0)
}
```

**Weryfikacja:** `result` ma wartość `true`, a zdekodowane `pdf_data` otwiera się jako PDF z numerem śledzenia zamówienia.

## 8. Odczyt zamówienia

Sklep odczytuje zamówienie, aby pokazać jego status, adresy i paczki na stronie zamówienia lub na ekranie obsługi klienta.

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

```bash
curl https://YOUR_HOST/api/v1/uniorder/123456 \
  -H "Authorization: Bearer ACCESS_TOKEN"
```

```json
{
  "result": true,
  "type": "self_delivery",
  "id": 123456,
  "ref": "WEB-10045",
  "order_type": "D",
  "status": "pending",
  "created_at": "2026-10-02 09:14:05",
  "time_window_start": "2026-10-02 13:00:00",
  "time_window_end": "2026-10-02 17:00:00",
  "from": { "name": "Fleurs du Plateau", "address": "4500 Rue Saint-Denis", "city": "Montreal", "postcode": "H2J2L3" },
  "to": { "name": "Jane Recipient", "address": "6841 Rue Saint-Denis", "address_2": "Apt 2", "city": "Montreal", "postcode": "H2S2S3" },
  "packages": [
    { "id": 998877, "ref": "WEB-10045-1", "tracking_number": "SR26092800001", "weight": 1.2 }
  ],
  "shipping_price": "14.60",
  "currency": "CAD"
}
```

- `type`: `self_delivery` lub `label_service`; pozostałe pola mają ten sam kształt w obu przypadkach.
- `status`: `pending`, `in_transit`, `out_for_pickup`, `out_for_delivery`, `ready_for_self_pickup`, `delivered`, `exception` lub `cancelled` dla zamówienia dostawy oraz `label_pending`, `label_purchased` lub `cancelled` dla zamówienia etykiety.
- `label` (tylko zamówienie etykiety): przewoźnik, usługa, `carrier_tracking_numbers` i `label_status` (`not_purchased`, `pending`, `ready` lub `failed`).

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

```graphql
query {
  uniorder(order_id: 123456)
}
```

**Weryfikacja:** zamówienie zwraca swój `status` i `packages`, a `ref` odpowiada zamówieniu internetowemu.

## 9. Śledzenie zamówienia

Strona zamówienia pokazuje oś czasu przesyłki. Należy ją odczytywać, gdy klient otwiera stronę, lub aktualizować na podstawie webhooków.

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

```bash
curl https://YOUR_HOST/api/v1/uniorder/123456/tracking \
  -H "Authorization: Bearer ACCESS_TOKEN"
```

```json
{
  "result": true,
  "type": "self_delivery",
  "id": 123456,
  "status": "delivered",
  "tracking_numbers": ["SR26092800001"],
  "events": [
    { "code": "delivered", "description": "Delivered", "location": "Montreal", "time": "2026-10-02 15:42:10", "time_zone": "America/Toronto", "source": "shipper" }
  ],
  "proofs": [
    { "type": "photo", "url": "https://YOUR_HOST/storage/pod/123456.jpg", "uploaded_at": "2026-10-02 15:42:08" }
  ]
}
```

- `events`: oś czasu, od najnowszych, każde zdarzenie z `code`, `description`, `location` i czasem.
- `proofs`: pliki potwierdzenia dostawy. Należy je pokazać, gdy `status` ma wartość `delivered`.
- `carrier` (tylko zamówienie etykiety): nazwa przewoźnika, numer śledzenia i link do śledzenia (`tracking_url`).

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

```graphql
query {
  uniorderTracking(order_id: 123456)
}
```

**Weryfikacja:** wywołanie śledzenia zwraca `result` `true`, `status` zamówienia i jego `events`.

## 10. Anulowanie zamówienia

Gdy klient anuluje zamówienie internetowe, sklep anuluje przesyłkę tym samym wywołaniem dla zamówienia dostawy i zamówienia etykiety. Etykieta jest najpierw unieważniana u jej przewoźnika.

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

```bash
curl -X POST https://YOUR_HOST/api/v1/uniorder/123456/cancel \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: cancel-10045" \
  -d '{}'
```

```json
{
  "result": true,
  "type": "self_delivery",
  "id": 123456,
  "already_cancelled": false,
  "message": "The order has been cancelled."
}
```

- `already_cancelled`: `true`, gdy zamówienie zostało anulowane przed tym wywołaniem. Należy to traktować jako powodzenie.
- Gdy zamówienie nie zostaje anulowane, odpowiedź to `409`, a zamówienie pozostaje bez zmian: `ORDER_STATUS_NOT_CANCELLABLE` (za późno na anulowanie), `ORDER_CANCEL_REFUSED` (obecnie nie można anulować) lub `LABEL_CANCEL_FAILED` (przewoźnik nie unieważnił etykiety). Zamówienie internetowe należy pozostawić otwarte, a przesyłkę obsłużyć ręcznie.

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

```graphql
mutation {
  uniorderCancel(order_id: 123456)
}
```

**Weryfikacja:** `result` ma wartość `true`. Ponowne anulowanie tego samego zamówienia zwraca `already_cancelled` `true`.

## 11. Późniejszy zakup etykiety (tylko po LABEL_PURCHASE_FAILED)

Ten krok dotyczy wyłącznie zamówienia etykiety, którego utworzenie zwróciło `LABEL_PURCHASE_FAILED`. Odpowiedź miała status `200` z `result` `false`, kodem `LABEL_PURCHASE_FAILED` i `id` zamówienia: zamówienie zostało zachowane bez etykiety. Nie należy wysyłać zamówienia ponownie; należy kupić etykietę dla tego zamówienia.

**REST:** `POST /api/v1/uniorder/{orderId}/label` — [Podręcznik REST](/api/documentation#/paths/v1-uniorder-orderId--label/post)

Utworzenie, w którym nie udało się kupić etykiety, zwróciło:

```json
{
  "result": false,
  "code": "LABEL_PURCHASE_FAILED",
  "type": "label_service",
  "id": 123458,
  "ref": "WEB-10046",
  "tracking_numbers": ["SR26092800003"],
  "quoted_price": "18.40",
  "message": "The quoted service is not offered for this shipment."
}
```

Zakup etykiety dla zamówienia `123458`:

```bash
curl -X POST https://YOUR_HOST/api/v1/uniorder/123458/label \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: label-10046" \
  -d '{}'
```

Etykieta jest kupowana w usłudze wybranej przy tworzeniu zamówienia. Aby kupić ją w innej usłudze tego samego konta, należy wysłać w treści żądania nowy `rate_id` typu `label_service` z kroku 5 (`{"rate_id": "eyJpdiI6IlpxR0..."}`). Etykieta już kupiona jest zwracana i nie jest kupowana ponownie.

```json
{
  "result": true,
  "type": "label_service",
  "id": 123458,
  "ref": "WEB-10046",
  "shipping_price": "18.40",
  "label": {
    "carrier_name": "ups",
    "service_code": "ups_standard",
    "main_tracking_number": "1Z999AA10123456791",
    "label_status": "ready",
    "shipping_label": "JVBERi0xLjQK..."
  }
}
```

- `label.shipping_label`: plik PDF etykiety w base64; należy go wydrukować jak w kroku 7.
- `result` `false` ponownie z `LABEL_PURCHASE_FAILED`: przewoźnik nadal odmówił. Należy spróbować ponownie później lub kupić etykietę w innej usłudze z nowym `rate_id`.

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

```graphql
mutation {
  uniorderPurchaseLabel(order_id: 123458)
}
```

**Weryfikacja:** `result` ma wartość `true`, a `label.shipping_label` zawiera PDF, lub `label.label_status` ma wartość `pending`, dopóki przewoźnik generuje plik.

## 12. Partie

Partie wyceniają lub tworzą wiele przesyłek w jednym wywołaniu, na przykład hurtowe zamówienia z systemu ERP. Każdy wiersz przechodzi przez pojedyncze wywołanie i zwraca to, co zwróciłoby to wywołanie; wiersz zakończony błędem nie zatrzymuje pozostałych wierszy.

**REST:** `POST /api/v1/uniorder/rate/batch` — [Podręcznik REST](/api/documentation#/paths/v1-uniorder-rate-batch/post) · `POST /api/v1/uniorder/batch` — [Podręcznik REST](/api/documentation#/paths/v1-uniorder-batch/post)

Do 20 wierszy na wywołanie, z wynikami w tej samej odpowiedzi: `shipments` dla partii wyceny, `orders` dla partii tworzenia. Każdy wiersz ma te same pola co pojedyncze wywołanie oraz opcjonalne `reference`, które jest zwracane razem z jego wynikiem.

```bash
curl -X POST https://YOUR_HOST/api/v1/uniorder/batch \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: erp-2026-10-01" \
  -d '{
    "orders": [
      {
        "reference": "ERP-7781",
        "rate_id": "eyJpdiI6Ik1rT2Z...",
        "ref": "ERP-7781",
        "from_name": "Fleurs du Plateau",
        "from_telephone": "5145550100",
        "from_address": "4500 Rue Saint-Denis",
        "from_city": "Montreal",
        "from_province": "QC",
        "from_country": "CA",
        "from_postcode": "H2J2L3",
        "to_name": "Jane Recipient",
        "to_telephone": "5145550199",
        "to_address": "6841 Rue Saint-Denis",
        "to_city": "Montreal",
        "to_province": "QC",
        "to_country": "CA",
        "to_postcode": "H2S2S3",
        "packages": [{ "weight": 1.2, "weight_unit": 2, "length": 40, "width": 25, "height": 25, "dimension_unit": 2 }]
      }
    ]
  }'
```

```json
{
  "result": true,
  "count": 1,
  "results": [
    { "index": 0, "reference": "ERP-7781", "status": 200, "body": { "result": true, "type": "self_delivery", "id": 123460 } }
  ]
}
```

- `results`: jeden na wiersz, z `index` wiersza, jego `reference` oraz `status` i `body`, które zwróciłoby pojedyncze wywołanie. Każdy wynik należy przypisać do jego pozycji zamówienia na podstawie `reference`.

**REST:** `POST /api/v1/uniorder/rate/batch-async` — [Podręcznik REST](/api/documentation#/paths/v1-uniorder-rate-batch-async/post) · `POST /api/v1/uniorder/batch-async` — [Podręcznik REST](/api/documentation#/paths/v1-uniorder-batch-async/post) · `GET /api/v1/uniorder/jobs/{jobId}` — [Podręcznik REST](/api/documentation#/paths/v1-uniorder-jobs-jobId/get)

Do 500 wierszy, umieszczonych w kolejce jako jedno zadanie. Wywołanie zwraca `job_id`; zadanie należy odczytywać, aż `status` przyjmie wartość `done`, a następnie odczytać `results`. Ta sama partia wysłana ponownie, gdy pierwsza jest jeszcze w kolejce, zwraca pierwsze zadanie z `duplicate` `true`.

```bash
curl https://YOUR_HOST/api/v1/uniorder/jobs/8813 \
  -H "Authorization: Bearer ACCESS_TOKEN"
```

```json
{
  "result": true,
  "job_id": 8813,
  "kind": "create",
  "status": "done",
  "count": 1,
  "results": [
    { "index": 0, "reference": "ERP-7781", "status": 200, "body": { "result": true, "type": "self_delivery", "id": 123460 } }
  ]
}
```

- `status`: `queued`, `done` lub `failed` z `message`, gdy zadania nie udało się przetworzyć.
- Zadanie jest wykonywane raz i nie jest ponawiane. `rate_id`, który wygaśnie przed przetworzeniem jego wiersza, zwraca dla tego wiersza `RATE_ID_INVALID`; zadanie tworzenia należy wysłać wkrótce po zakończeniu zadania wyceny.

**GraphQL:** `uniorderRateBatch` ([Podręcznik GraphQL](/api/graphql/documentation#/orders/uniorderRateBatch)) · `uniorderCreateBatch` ([Podręcznik GraphQL](/api/graphql/documentation#/orders/uniorderCreateBatch)) · `uniorderRateBatchAsync` ([Podręcznik GraphQL](/api/graphql/documentation#/orders/uniorderRateBatchAsync)) · `uniorderCreateBatchAsync` ([Podręcznik GraphQL](/api/graphql/documentation#/orders/uniorderCreateBatchAsync)) · `uniorderJob` ([Podręcznik GraphQL](/api/graphql/documentation#/orders/uniorderJob))

```graphql
query {
  uniorderJob(job_id: 8813)
}
```

**Weryfikacja:** partia zwraca jeden wynik na wiersz; zadanie asynchroniczne osiąga `status` `done`.

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

| Sytuacja | Status HTTP | Kod | Co robi integracja |
|---|---|---|---|
| Brakuje wymaganego pola lub ma ono nieprawidłowy format | 400 | `VALIDATION_FAILED` | Poprawić pole wskazane w `message` i wysłać żądanie ponownie. |
| Odbiorca jest poza obszarem dostaw (wycena) | 200 | `OUT_OF_DELIVERY_AREA` w `errors` | Zaoferować tylko stawki `label_service`. |
| `rate_id` wygasł, ma nieprawidłowy format lub należy do innego konta | 400 | `RATE_ID_INVALID` | Zażądać nowej wyceny i utworzyć zamówienie z jej `rate_id`. Nic nie zostało utworzone. |
| Zamówienie etykiety zostało utworzone, ale jego etykieta nie została kupiona | 200 (`result` `false`) | `LABEL_PURCHASE_FAILED` | Zachować `id`; kupić etykietę przez `POST /api/v1/uniorder/{orderId}/label`. Nigdy nie tworzyć zamówienia ponownie. |
| Etykieta jest pobierana, zanim została kupiona | 409 | `LABEL_PURCHASE_FAILED` | Kupić etykietę przez `POST /api/v1/uniorder/{orderId}/label`. |
| Realizacja zamówienia jest zbyt zaawansowana, aby je anulować | 409 | `ORDER_STATUS_NOT_CANCELLABLE` | Pozostawić zamówienie bez zmian; obsłużyć zwrot oddzielnie. |
| Zamówienia nie można teraz anulować | 409 | `ORDER_CANCEL_REFUSED` | Pozostawić zamówienie bez zmian; spróbować ponownie później lub skontaktować się z firmą. |
| Przewoźnik nie unieważnił etykiety | 409 | `LABEL_CANCEL_FAILED` | Zamówienie pozostaje bez zmian; ponowić anulowanie później. |
| Zamówienie lub zadanie nie istnieje albo należy do innego konta | 404 | `ORDER_NOT_FOUND` | Sprawdzić `id` zapisane z zamówieniem internetowym. |
| `Idempotency-Key` jest użyty ponownie z inną treścią | 409 | `IDEMPOTENCY_CONFLICT` | Użyć nowego klucza dla innego żądania. |
| Brak tokenu lub token wygasł albo konto nie może składać zamówień | 401 | — | Zalogować się ponownie; sprawdzić uprawnienia konta. |

## Lista testów

Należy użyć testowego `ref`, takiego jak `WEB-10045`:

- [ ] Wycena zwraca stawkę `self_delivery` dla adresu w obszarze.
- [ ] Przy `quote_labels` wycena zwraca stawki `label_service`, każdą z `rate_id`.
- [ ] Zamówienie z `rate_id` typu `self_delivery` zwraca `id` i `tracking_numbers`.
- [ ] Zamówienie z `rate_id` typu `label_service` zwraca etykietę wycenionej usługi.
- [ ] Ten sam `Idempotency-Key` nie tworzy drugiego zamówienia.
- [ ] `rate_id` starszy niż 30 minut zwraca `RATE_ID_INVALID`.
- [ ] Etykieta każdego zamówienia dekoduje się do pliku PDF gotowego do druku.
- [ ] Zamówienie, jego etykietę i jego śledzenie można odczytać za pomocą `id` z tworzenia.
- [ ] Anulowanie zamówienia testowego zwraca `result: true`; ponowne anulowanie zwraca `already_cancelled: true`.
- [ ] Po `LABEL_PURCHASE_FAILED` `POST /api/v1/uniorder/{orderId}/label` kupuje etykietę dla tego samego zamówienia.
- [ ] Partia dwóch wierszy zwraca dwa wyniki z ich `reference`.
