# Preuzimanje i dostava

Preuzimanje i poslednja milja sopstvenom flotom koriste iste API-je. Kreiranje porudžbine preuzimanja ili dostave → štampa lokalne nalepnice → praćenje → pretplata na obaveštenja o događajima → otkazivanje test porudžbine.

`type` bira stanicu: `D` dostava, `P` preuzimanje. Izvršite korake redom. Ponuda je opciona i nije obavezna pre kreiranja porudžbine.

Zamenite `YOUR_HOST` i `ACCESS_TOKEN` vrednostima iz vašeg okruženja.

## 1. Prijava

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

Vraćeni `access_token` stavite u zaglavlje zahteva:

```
Authorization: Bearer ACCESS_TOKEN
```

GraphQL koristi isto zaglavlje na `POST /api/graphql`.

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

**Verifikacija:** prijava vraća `access_token`. Naredni zahtevi bez ovog tokena vraćaju `401`.

## 2. Ponuda (opciono)

Ovaj korak je opcion. Vraća samo ponudu; porudžbina se ne kreira. Kreiranje ne zahteva prethodnu ponudu. Postavite `type` na `D` (dostava) ili `P` (preuzimanje).

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

**Verifikacija:** ako pokrenete ovaj poziv, `result` je true i `shipping_price` je broj. Ponovite sa `type` `P` da biste dobili ponudu za preuzimanje. Prazna cena znači da poštanski broj nije u aktivnoj zoni. Kreiranje ne zavisi od ovog koraka.

## 3. Kreiranje porudžbine

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

Zahtev mora da sadrži `Idempotency-Key` kako ponovni pokušaj ne bi kreirao drugu porudžbinu.

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

### Preuzimanje (`type` `P`)

Isti endpoint. Adresa je stanica preuzimanja.

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

| Polje | Značenje |
|---|---|
| `type` | `D` dostava, ili `P` preuzimanje |
| `need_pick_up` | `0` — paket je već u skladištu. `1` — vozač mora da preuzme paket |
| `ref` | Spoljna referenca za pretragu i usklađivanje |
| `name` / adresa | Dostava: primalac. Preuzimanje: stanica preuzimanja |
| `auto_deduplication` | `1` odbija drugi paket sa istim `ref` paketa |

**GraphQL:** `clientOrderCreate` ([GraphQL priručnik](/api/graphql/documentation#/client/clientOrderCreate)) (JSON skalar).

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

`orders_status_id` `2` je Nova. Sačuvajte `id`, `tracking_number` i `ref`.

**Verifikacija:** pošaljite **isto** telo sa istim `Idempotency-Key` ponovo. Morate dobiti isti `id` i ne smete kreirati drugu porudžbinu.

Porudžbina se **ne** kreira kada:

| `code` | Postupanje |
|---|---|
| `INSUFFICIENT_BALANCE` | `insufficient_balance` sadrži required / available / shortfall. Dopunite, pa pokušajte ponovo. |
| `OUT_OF_DELIVERY_AREA` | Adresa je van servisne zone i kompanija briše te porudžbine. Pošaljite adresu unutar servisne zone. |
| `IDEMPOTENCY_CONFLICT` | Isti idempotentni ključ je ponovo upotrebljen sa drugačijim telom zahteva. Koristite novi ključ. |

Zadržana porudžbina van zone i dalje može da vrati `result: true` sa `shipping_price: null` i `warning`. Pročitajte to polje.

## 4. Pregled porudžbine

```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` — sve porudžbine naloga, najnovije prve, svaka sa svojim paketima i stavkama. Pošaljite `page` i `per_page` zajedno za paginaciju (`per_page` najviše 1000); bez njih dobijate najnovijih 1000 porudžbina i oznaku `truncated`. [REST priručnik](/api/documentation#/paths/v1-orders-list/get)

**GraphQL:** `ordersList` ([GraphQL priručnik](/api/graphql/documentation#/orders/ordersList))

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

**GraphQL:** `orders` ([GraphQL priručnik](/api/graphql/documentation#/orders/orders))

**Verifikacija:** porudžbina pripada autentifikovanom nalogu. `ref` se poklapa sa vrednošću koju ste kreirali. `tracking_number` se poklapa sa korakom 3.

## 5. Štampa lokalne nalepnice

**REST:** `POST /api/v1/shipping/getShippingLabel` — [REST priručnik](/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` ili `REF`. `base64: 0` (podrazumevano) šalje PDF tok. Uz `base64: 1` celo telo odgovora je JSON niska na najvišem nivou koja sadrži PDF u base64 obliku, a ne objekat sa poljem `pdf_data`. Pozovite `POST /api/v2/shipping/getShippingLabel` ako želite etiketu unutar uobičajenog JSON objekta.

**GraphQL:** `shippingGetShippingLabel` ([GraphQL priručnik](/api/graphql/documentation#/shipping/shippingGetShippingLabel)). `shippingGetShippingLabelV2` ([GraphQL priručnik](/api/graphql/documentation#/shipping/shippingGetShippingLabelV2)) uvek vraća JSON (`pdf_data`).

**Verifikacija:** PDF se otvara. Nalepnica dostave pokazuje primaoca; nalepnica preuzimanja pokazuje adresu preuzimanja. Skriveno adresno polje na nalepnici je prazno.

## 6. Praćenje

Javni endpoint za praćenje; pristupni token nije potreban.

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

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

Isti URL prihvata vaš `ref` kada je sačuvan kao spoljni broj.

**GraphQL** (tipizirano — treba 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 }
  }
}
```

Granajte po `tracking_event_status_id`, ne po `description` (taj string prati `Accept-Language`):

| `tracking_event_status_id` | Strana | Značenje |
|---|---|---|
| `100` | obe | Porudžbina primljena |
| `300` / `301` | dostava | U objektu |
| `450` | dostava | Na dostavi |
| `500` | dostava | Dostavljeno |
| `501` | dostava | Dostava nije uspela, potreban je novi plan |
| `460` | preuzimanje | Na preuzimanju |
| `510` | preuzimanje | Preuzeto |
| `512` | preuzimanje | Preuzimanje nije uspelo, pokušajte kasnije |
| `513` | preuzimanje | Problem pri preuzimanju |

`data` je od najnovijeg. Prvi red tretirajte kao trenutni. `deliveried: true` posle `500`.

Na `500` ili `510`, `proofs[]` može da nosi `type` `1` (potpis) ili `2` (foto), plus `file_id` i `signed_url`. Foto otpremljena posle tog događaja nije u tom payloadu — pretplatite se na `pod.files_updated` u koraku 7.

**Verifikacija:** odmah posle kreiranja najnoviji događaj je `100` i `deliveried` je false. Nepoznat broj je `result: false` / 404 — prikažite stanje nije pronađeno; ne izmišljajte događaje praćenja.

## 7. Konfiguracija obaveštenja o događajima

Podesite URL-ove povratnog poziva koje ovaj tok zahteva:

| Podešavanje | Događaj | Kada |
|---|---|---|
| `order_create_webhook_url` | `order.created` | Sačuvati `id` i `tracking_number` |
| `order_status_change_webhook_url` | `order.status_change` | Status koji vidi kupac |
| `tracking_event_webhook_url` | `tracking.event` | Vremenska linija preuzimanja ili dostave |
| `pod_files_webhook_url` | `pod.files_updated` | Foto/potpis posle preuzimanja ili dostave |
| `order_cancel_failed_webhook_url` | `order.cancel_failed` | Otkazivanje koje ste poslali je odbijeno |

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

**GraphQL:** `webhookSettingsUpdate` ([GraphQL priručnik](/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
  }'
```

Proverite **v2** nad sirovim telom: `HMAC_SHA256(timestamp + "." + raw_body, secret)` protiv `X-Webhook-Signature-V2`. Deduplikujte po `X-Webhook-Event-Id`. Odgovorite **2xx za manje od 3 sekunde**.

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

**Verifikacija:** kreirajte jednu test porudžbinu i vidite `order.created` sa istim `id` / `tracking_number`. Nevažeći potpis primalac mora da odbije sa `401`. Druga isporuka istog `X-Webhook-Event-Id` ne sme da se obradi dva puta.

## 8. Otkazivanje test porudžbine

**REST:** `POST /api/v1/orders/cancel` — [REST priručnik](/api/documentation#/paths/v1-orders-cancel/post) — tačno jedno od `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"}'
```

Već otkazano: `200` sa `already_cancelled: true`. **GraphQL:** `ordersCancel` ([GraphQL priručnik](/api/graphql/documentation#/orders/ordersCancel)).

**Verifikacija:** javno praćenje pošiljku više ne tretira kao aktivnu. Ako je otkazivanje odbijeno (`409`, npr. `ORDER_ALREADY_IN_DELIVERY`), pali se `order.cancel_failed`.

## 9. Paketno kreiranje (opciono)

- `POST /api/v1/client/batchOrderCreate` — [REST priručnik](/api/documentation#/paths/v1-client-batchOrderCreate/post) — čeka dok se ne obradi svaki red.
- `POST /api/v1/client/batchOrderCreateAsync` — [REST priručnik](/api/documentation#/paths/v1-client-batchOrderCreateAsync/post) — odmah vraća identifikator posla; pollujte `GET /api/v1/client/async/{id}` — [REST priručnik](/api/documentation#/paths/v1-client-async-id/get) ili uzmite `order.create_async`.

Ista polja kao korak 3, kao niz porudžbina. Svaki red može biti `type` `D` ili `P`. **GraphQL:** `clientBatchOrderCreate` ([GraphQL priručnik](/api/graphql/documentation#/client/clientBatchOrderCreate)) / `clientBatchOrderCreateAsync` ([GraphQL priručnik](/api/graphql/documentation#/client/clientBatchOrderCreateAsync)).

**Verifikacija:** svaki red ima svoj `result`. Asinhroni endpoint koristite kada šaljete više od približno 100 redova.

## Lista provera

Koristite test `ref` kao `DEV-LOCAL-001` / `DEV-PICKUP-001`:

- [ ] (Opciono) Ponuda vraća cenu za poštanski broj u zoni sa `type` `D`.
- [ ] (Opciono) Ponuda vraća cenu za poštanski broj u zoni sa `type` `P`.
- [ ] Kreiranje dostave vraća `id` + `tracking_number`; isti `Idempotency-Key` ne kreira drugu porudžbinu.
- [ ] Kreiranje preuzimanja vraća `id` + `tracking_number`; `need_pick_up` je `1`.
- [ ] Lista / detalj pokazuje porudžbinu na ovom nalogu.
- [ ] PDF lokalne nalepnice se otvara i pokazuje primaoca ili adresu preuzimanja.
- [ ] Javno praćenje vraća vremensku liniju bez tokena; najnoviji događaj je `100`.
- [ ] Stiže `order.created`; v2 potpis se proverava.
- [ ] Otkazivanje vraća `result: true` (ili `already_cancelled: true` pri ponovnom pokušaju).
