# Tárolás és kiszállítás

A tárolási és kiszállítási API lehetővé teszi, hogy egy raktározó vállalkozás ügyfélfiókja árut adjon be tárolásra, kifizesse a tárolási időszakot, majd később a tárolt csomagokat a saját vevőinek kiszállíttassa. Olyan kereskedőknek és platformoknak készült, amelyek készletüket harmadik fél raktárában (3PL) tartják, és saját rendszereikből kívánják automatizálni a tárolás foglalását, a készlet nyilvántartását és a kimenő küldeményeket. Minden hívás az ügyfélfiók nevében fut, soha nem a raktározó vállalkozás nevében.

## 1. Mit építhet

Az útmutató példái egyetlen forgatókönyvet követnek. A **Northwind Outdoor**, téli felszereléseket forgalmazó szezonális online kereskedő, 2026. november 1. és 2027. március 31. között a 3PL-szolgáltatója **Toronto Hub** raktárában (`7` raktár) tárolja téli készletét. Amikor egy vevő egy karton bélelt kabátot rendel, a Northwind ezt a kartont a készletből szállíttatja ki az ottawai vevőnek.

- **Szezonális tárolásfoglalás.** A kereskedő háttérrendszere minden beérkező kartonra árajánlatot kér és tárolási időszakot foglal még azelőtt, hogy az áru elhagyná a beszállítót, és a tárolási díjat a fiókegyenlegéből fizeti ki.
- **Aktuális készletnézet.** A kereskedő webáruháza vagy ERP-rendszere azokat a csomagokat listázza, amelyeket a raktár ténylegesen átvett, és amelyek még kiszállíthatók, így csak valós készlet kerül teljesítésre felkínálásra.
- **Rendelésteljesítés készletből.** Amikor egy vevő rendelést ad le, a kereskedő rendszere beárazza a kimenő küldeményt, kiszállítási kérést hoz létre a tárolt csomagokra, kifizeti azt, és rögzíti a követési számot a vevő számára.
- **Állapotkövetés és javítás.** A kereskedő rendszere lekérdezi az egyes tárolási rendelések és kiszállítások állapotát, a nyilvános követéssel követi a küldeményt, és lemondja azt a kiszállítást, amelyre már nincs szükség, amíg ez még megengedett.

## 2. Mit tartalmaz ez az útmutató

Ezt az útmutatót akkor használja, ha az áru már a vállalkozás raktárában van, vagy oda kerül, és a küldemény ebből a készletből indul. A folyamat: belépés → a tárolási konfiguráció lekérdezése → tárolási árajánlat → a tárolási rendelés létrehozása → fizetés → a raktáron lévő csomagok listázása → a szolgáltatások listázása és a kiszállítás becslése → a kiszállítás létrehozása → fizetés → lekérdezés és követés → webhookok → lemondás.

Más esetekre más útmutatók valók:

- **Uniorder: egy API minden küldeményhez** — az ajánlott egységes belépési pont (`/api/v1/uniorder/...`) a helyi kiszállítást vagy fuvarozói címkéket foglaló új integrációkhoz. Az Uniorder **nem** terjed ki a tárolásra és a kiszállításra; tárolási rendelések és kiszállítások kizárólag az ebben az útmutatóban leírt ügyfélvégpontokon hozhatók létre.
- **Szállítási szolgáltatások** — az ügyfél nem tárolt árut szállíttat a vállalkozás szállítási szolgáltatásaival.
- **Fuvarozói címkék** — a vállalkozás közvetlenül vásárol fuvarozói címkéket a saját csomagjaihoz.
- **Felvétel és kézbesítés (saját flotta)** — a vállalkozás saját flottájával foglal felvételeket és kézbesítéseket.

## 3. Mielőtt elkezdené

- **Fióktípus.** A raktározó vállalkozás **ügyfélfiókja** (a raktárat üzemeltető vállalkozás a szolgáltató). Vállalkozói (kliens) fiók tokenje nem működik a `/api/v1/customer/...` végpontokon.
- **Jogosultságok.** Az ügyfélfióknak API-hozzáféréssel kell rendelkeznie. A tárolási végpontokhoz tárolási jogosultság is szükséges; a kiszállítási végpontokhoz a vállalkozásnak engedélyeznie kell a kiszállítást (vagy a konszolidációt) ennél az ügyfélnél, ellenkező esetben `403` a válasz.
- **Egyenleg.** A tárolási és kiszállítási díjak az ügyfél fiókegyenlegéből kerülnek levonásra. Teszteléshez kérje meg a vállalkozást, hogy írjon jóvá összeget a tesztügyfél egyenlegén.
- **Tesztadatok.** Egy raktár-`id`, legalább egy csomagolás-`id`, ha egyedi csomagok nem engedélyezettek, és legalább egy, az adott raktárból elérhető aktív szállítási szolgáltatás. A kiszállítás csak azután működik, hogy a raktár **átvette** a tárolt csomagokat; teszteléskor kérje meg a raktári személyzetet a teszt tárolási rendelés átvételére.
- **Tokenkezelés.** A belépést a szerveréről végezze, a tokent a szerveren tárolja, és soha ne helyezze böngésző- vagy mobilkódba.
- **Helyettesítők.** Cserélje a `YOUR_HOST` értéket a platform gazdagépnevére, az `ACCESS_TOKEN` értéket pedig a 4. lépésben kapott tokenre.

## 4. Belépés ügyfélként

Minden későbbi hívás ügyfél bearer tokennel van hitelesítve. Az integráció egyszer lép be, a tokent a szerveroldalon tárolja, és az `expires_at` előtt megújítja.

**REST:** `POST /api/v1/user/customer/login` — [REST kézikönyv](/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" \
  -d '{"email":"ops@northwind-outdoor.example","password":"your_password"}'
```

```json
{
  "result": true,
  "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOi...",
  "token_type": "Bearer",
  "expires_at": "2027-09-28 10:15:00",
  "expires_timestamp": 1822040100,
  "name": "Northwind Outdoor"
}
```

- `access_token` — minden kérésben küldje el az alábbi fejlécben.
- `expires_at` / `expires_timestamp` — ezen időpont előtt lépjen be újra.

```
Authorization: Bearer ACCESS_TOKEN
```

A GraphQL ugyanazt a fejlécet használja a `POST /api/graphql` híváson.

**Ellenőrzés:** a belépés `access_token` értéket ad. Az e token nélküli későbbi kérések `401` választ adnak.

## 5. A tárolási konfiguráció lekérdezése

A konfigurációs csomag felsorolja az ügyfél által használható raktárakat, a csomagolási katalógust, a mértékegységeket és a pótdíjakat. Az integráció munkamenetenként egyszer kérdezi le, hogy kiválassza a raktárat, és érvényes csomagsorokat állítson össze.

**REST:** `GET /api/v1/customer/storage-orders/config` — [REST kézikönyv](/api/documentation#/paths/v1-customer-storage-orders-config/get)

```bash
curl https://YOUR_HOST/api/v1/customer/storage-orders/config \
  -H "Authorization: Bearer ACCESS_TOKEN"
```

```json
{
  "result": true,
  "data": {
    "warehouses": [
      { "id": 7, "name": "Toronto Hub", "address": "10 Main St", "city": "Toronto", "province": "ON", "postcode": "M5V 2T6" }
    ],
    "packagings": [
      { "id": 1, "name": "Large Carton", "type": "Box", "length": 60, "width": 40, "height": 40, "dimension_unit": 2 }
    ],
    "dimension_units": { "1": { "name": "dimension_mm" }, "2": { "name": "dimension_cm" }, "3": { "name": "dimension_m" }, "4": { "name": "dimension_inch" } },
    "weight_units": { "1": { "name": "weight_g" }, "2": { "name": "weight_kg" }, "3": { "name": "weight_oz" }, "4": { "name": "weight_lb" } },
    "allow_custom_package": true,
    "surcharges": [],
    "form_bindings": []
  }
}
```

- `warehouses[].id` — a `warehouse_id` minden későbbi híváshoz.
- `allow_custom_package` — ha `false`, minden tárolási tételnek tartalmaznia kell egy `packaging_id` értéket a `packagings[]` listából; ha `true`, a tételek csak méretekkel is megadhatók.
- `dimension_units` / `weight_units` — a csomagsorokban használt egész számos kódok (`2` = cm, `2` = kg).
- `form_bindings` — a vállalkozás által a tárolási rendeléshez megkövetelt űrlapok; a válaszokat a 7. lépésben `form_data` mezőként küldje el.

**GraphQL:** `customerStorageOrderConfig` ([GraphQL kézikönyv](/api/graphql/documentation#/customer/customerStorageOrderConfig))

```graphql
query {
  customerStorageOrderConfig
}
```

**Ellenőrzés:** rögzített egy raktár-`id` értéket, és ha a katalógus nem üres, egy csomagolás-`id` értéket.

## 6. Árajánlat a tárolási időszakra

Az árajánlat a tervezett csomagok tárolási időszakát árazza be, mielőtt bármi le lenne foglalva. Az integráció megjeleníti vagy ellenőrzi ezt az árat, majd ugyanazokkal a bemenetekkel hozza létre a rendelést.

**REST:** `POST /api/v1/customer/storage-orders/calculate-price` — [REST kézikönyv](/api/documentation#/paths/v1-customer-storage-orders-calculate-price/post)

```bash
curl -X POST https://YOUR_HOST/api/v1/customer/storage-orders/calculate-price \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "warehouse_id": 7,
    "start_date": "2026-11-01",
    "end_date": "2027-03-31",
    "items": [{
      "qty": 2,
      "length": 60,
      "width": 40,
      "height": 40,
      "dimension_unit": 2,
      "weight": 14,
      "weight_unit": 2
    }]
  }'
```

```json
{
  "success": true,
  "price": {
    "total_price": "186.45",
    "currency": "CAD",
    "shipping_price": 186.45
  }
}
```

- `success` — `true`, ha az ár kiszámítása megtörtént.
- `price.total_price` / `price.currency` — az időszak adót tartalmazó tárolási ára.
- `promotion` — csak akkor szerepel, ha promóció érvényes.

**Ellenőrzés:** a `success` vagy a `result` értéke igaz, és van ár. Hiányzó `warehouse_id` / dátumok esetén a válasz `400`.

## 7. A tárolási rendelés létrehozása

A tárolási rendelés bejelenti a beérkező csomagokat a raktárnak, és rögzíti a tárolási időszakot. Az integráció eltárolja a visszaadott azonosítót; erre a fizetéshez, a rendelés lekérdezéséhez és lemondásához van szükség.

**REST:** `POST /api/v1/customer/storage-orders` — [REST kézikönyv](/api/documentation#/paths/v1-customer-storage-orders/post)

```bash
curl -X POST https://YOUR_HOST/api/v1/customer/storage-orders \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: northwind-winter-2026-po-4471" \
  -d '{
    "warehouse_id": 7,
    "start_date": "2026-11-01",
    "end_date": "2027-03-31",
    "notes": "Winter 2026 stock, PO 4471",
    "items": [{
      "description": "Insulated jackets, carton of 12",
      "qty": 2,
      "length": 60,
      "width": 40,
      "height": 40,
      "dimension_unit": 2,
      "weight": 14,
      "weight_unit": 2,
      "value": 1800
    }]
  }'
```

```json
{
  "result": true,
  "message": "Storage order created",
  "data": { "id": 1024, "status": "pending payment" }
}
```

- `data.id` — a tárolási rendelés azonosítója. Tárolja a beszerzési rendelésével együtt.
- `data.status` — `pending payment`, amíg a rendelés nincs kifizetve.
- `Idempotency-Key` — a saját stabil azonosítójából képezze. Ugyanazzal a kulccsal és törzzsel megismételt kérés az első választ adja vissza (`replayed: true`); ugyanaz a kulcs eltérő törzzsel `409 IDEMPOTENCY_CONFLICT` elutasítást kap.
- Kötelező mezők: `warehouse_id`, `start_date`, `end_date` (a `start_date` utáni), valamint `items[]` a következőkkel: `qty`, `length`, `width`, `height`, `dimension_unit`. Adja meg az `items[].packaging_id` értéket, ha az `allow_custom_package` értéke `false`.

**Ellenőrzés:** a válasz tartalmazza a `data.id` értéket. Tárolja el ezt a tárolási rendelés-azonosítót.

## 8. A tárolás kifizetése

A fizetés megerősíti a tárolási rendelést. Az integráció előbb lekérdezheti az esedékes összeget, majd az ügyfél egyenlegéből fizet.

**REST:** `GET /api/v1/customer/storage-orders/{id}/payment-info` — [REST kézikönyv](/api/documentation#/paths/v1-customer-storage-orders-id--payment-info/get) (opcionális)

```bash
curl https://YOUR_HOST/api/v1/customer/storage-orders/1024/payment-info \
  -H "Authorization: Bearer ACCESS_TOKEN"
```

```json
{
  "success": true,
  "result": true,
  "data": {
    "order_id": 1024,
    "currency": "CAD",
    "total_price": "186.45",
    "paid_amount": "0.00",
    "remaining_balance": "186.45",
    "user_balance": "500.00",
    "has_sufficient_balance": true,
    "minimum_payment": "186.45"
  }
}
```

**REST:** `POST /api/v1/customer/storage-orders/{id}/pay` — [REST kézikönyv](/api/documentation#/paths/v1-customer-storage-orders-id--pay/post)

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

```json
{
  "success": true,
  "result": true,
  "message": "Payment of $186.45 processed successfully. Your storage order is now confirmed.",
  "new_balance": 313.55,
  "paid_amount": 186.45,
  "charge_amount": 186.45,
  "is_fully_paid": true
}
```

- `payment_type` — `full_balance` (alapértelmezett, a fennmaradó összeget fizeti), `minimum_payment` (a vállalkozás által megkövetelt minimumot fizeti), vagy `custom` a `custom_amount` értékkel együtt.
- `is_fully_paid` — `true`, ha nem maradt fizetendő összeg.
- Elégtelen egyenleg esetén a válasz `400` a `customer_balance` értékkel; töltse fel az egyenleget, majd próbálja újra.

Kérdezze le a rendelést az állapota megerősítéséhez, később pedig annak megállapításához, mely csomagokat vette át a raktár.

**REST:** `GET /api/v1/customer/storage-orders/{id}` — [REST kézikönyv](/api/documentation#/paths/v1-customer-storage-orders-id/get)

```bash
curl https://YOUR_HOST/api/v1/customer/storage-orders/1024 \
  -H "Authorization: Bearer ACCESS_TOKEN"
```

```json
{
  "result": true,
  "data": {
    "id": 1024,
    "status": "confirmed",
    "can_cancel": true,
    "store_from": "2026-11-01",
    "store_to": "2027-03-31",
    "warehouse_id": 7,
    "packages": [
      { "id": 5001, "description": "Insulated jackets, carton of 12", "quantity": 1, "received": false },
      { "id": 5002, "description": "Insulated jackets, carton of 12", "quantity": 1, "received": false }
    ],
    "total_price": 186.45,
    "currency": "CAD"
  }
}
```

- `status` — fizetés után `confirmed`; később, az áru beérkezésével `partial received` / `storage in progress`.
- `packages[].received` — `true`, amint a raktár átvette az adott csomagot.
- `can_cancel` — lemondható-e még a tárolási rendelés.

**GraphQL:** `customerStorageOrderShow` ([GraphQL kézikönyv](/api/graphql/documentation#/customer/customerStorageOrderShow)); az összes tárolási rendelés listája `customerStorageOrders` ([GraphQL kézikönyv](/api/graphql/documentation#/customer/customerStorageOrders)).

```graphql
query {
  customerStorageOrderShow(id: 1024) {
    result
    data {
      id
      status
      can_cancel
      packages { id description received }
    }
  }
}
```

**Ellenőrzés:** a tárolási rendelés ki van fizetve / meg van erősítve. A `customer_balance` értéket tartalmazó `400` azt jelenti, hogy fel kell tölteni az egyenleget, majd újra kell próbálni.

Az alábbi kiszállítás csak akkor működik, ha a csomagokat **átvették** a raktárban. Teszteléskor várja meg, amíg a személyzet (vagy egy tesztátvétel) átvettnek jelöli őket, majd folytassa.

## 9. A még raktáron lévő tételek listázása

Ez a lista az a készlet, amelyet az integráció kiszállíthat. Csak olyan csomagokat tartalmaz, amelyeket a raktár átvett, és amelyek nincsenek már más kiszállításhoz zárolva.

**REST:** `GET /api/v1/customer/shipout-orders/available-items` — [REST kézikönyv](/api/documentation#/paths/v1-customer-shipout-orders-available-items/get)

```bash
curl "https://YOUR_HOST/api/v1/customer/shipout-orders/available-items?warehouse_id=7" \
  -H "Authorization: Bearer ACCESS_TOKEN"
```

```json
{
  "result": true,
  "data": {
    "warehouses": [{ "id": 7, "name": "Toronto Hub", "available_count": 2 }],
    "storage_orders": [{
      "id": 1024,
      "warehouse_id": 7,
      "packages": [
        { "id": 5001, "description": "Insulated jackets, carton of 12", "weight": 14, "weight_unit": 2, "length": 60, "width": 40, "height": 40, "dimension_unit": 2, "value": 1800 },
        { "id": 5002, "description": "Insulated jackets, carton of 12", "weight": 14, "weight_unit": 2, "length": 60, "width": 40, "height": 40, "dimension_unit": 2, "value": 1800 }
      ]
    }]
  }
}
```

- `storage_orders[].packages[].id` — a 10. lépésben kiszállítandó `storage_package_ids`.
- `warehouses[].available_count` — az elérhető csomagok száma raktáranként.

**GraphQL:** `customerShipoutAvailableItems` ([GraphQL kézikönyv](/api/graphql/documentation#/storage-shipout/customerShipoutAvailableItems))

```graphql
query {
  customerShipoutAvailableItems(warehouse_id: 7) {
    result
    data {
      warehouses { id name available_count }
      storage_orders { id warehouse_id packages { id description weight length width height } }
    }
  }
}
```

**Ellenőrzés:** rögzített egy vagy több `storage_package_ids` értéket (például `5001`). Az üres lista azt jelenti, hogy még semmi nincs átvéve — ne hozzon létre kiszállítást. A `403` azt jelenti, hogy a kiszállítás le van tiltva ennél az ügyfélnél.

## 10. A kiszállítás becslése és létrehozása

A kiszállítást a vállalkozás egyik szállítási szolgáltatása árazza be. Az integráció listázza a raktárból elérhető szolgáltatásokat, megbecsüli az árat a vevő célállomására, majd létrehozza a kiszállítást a kiválasztott csomagokra.

**REST:** `GET /api/v1/customer/shipout-orders/services?warehouse_id=7` — [REST kézikönyv](/api/documentation#/paths/v1-customer-shipout-orders-services/get)

```bash
curl "https://YOUR_HOST/api/v1/customer/shipout-orders/services?warehouse_id=7" \
  -H "Authorization: Bearer ACCESS_TOKEN"
```

```json
{
  "result": true,
  "data": {
    "services": [
      { "id": 5, "service_code": "intl_express", "name": { "en": "Express" }, "pricing_method": 1, "pricing_method_name": "Shipping Price Plan", "support_multi_package": true }
    ]
  }
}
```

Rögzítsen egy `service_code` értéket.

**REST:** `POST /api/v1/customer/shipout-orders/services/{serviceCode}/estimate` — [REST kézikönyv](/api/documentation#/paths/v1-customer-shipout-orders-services-serviceCode--estimate/post)

```bash
curl -X POST https://YOUR_HOST/api/v1/customer/shipout-orders/services/intl_express/estimate \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "warehouse_id": 7,
    "delivery_postcode": "K2P1L4",
    "delivery_country": "CA",
    "packages": [{
      "weight": 14,
      "length": 60,
      "width": 40,
      "height": 40,
      "weight_unit": 2,
      "dimension_unit": 2
    }]
  }'
```

```json
{
  "result": true,
  "data": {
    "shipping_fee": 38.5,
    "fuel_surcharge": 4.2,
    "handling_fee": 0,
    "sub_total": 42.7,
    "tax": 5.55,
    "total": 48.25,
    "currency": "CAD",
    "has_items_needing_quote": false,
    "refused": false
  }
}
```

- `total` / `currency` — a becsült ár erre a célállomásra.
- `has_items_needing_quote` — `true`, ha a szolgáltatást kézzel árazzák; a raktár a kiszállítás létrehozása után állapítja meg az árat, és a fizetés erre vár.
- `refused` / `refusal_message` — a szolgáltatás nem fogadja el ezt a küldeményt, mert nem tudja beárazni.

**REST:** `POST /api/v1/customer/shipout-orders/services/{serviceCode}/orders` — [REST kézikönyv](/api/documentation#/paths/v1-customer-shipout-orders-services-serviceCode--orders/post)

```bash
curl -X POST https://YOUR_HOST/api/v1/customer/shipout-orders/services/intl_express/orders \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: northwind-order-NW-20931" \
  -d '{
    "warehouse_id": 7,
    "storage_package_ids": [5001],
    "delivery_name": "Maya Chen",
    "delivery_telephone": "6135550142",
    "delivery_email": "maya.chen@example.com",
    "delivery_address_1": "150 Elgin St",
    "delivery_city": "Ottawa",
    "delivery_province": "ON",
    "delivery_country": "CA",
    "delivery_postcode": "K2P1L4",
    "note": "Web order NW-20931"
  }'
```

```json
{
  "result": true,
  "data": {
    "id": 8001,
    "status": 0,
    "is_storage_shipout": true,
    "total_price": "48.25",
    "price_breakdown": { "total": 48.25, "has_items_needing_quote": false },
    "has_items_needing_quote": false,
    "storage_package_ids": [5001]
  }
}
```

- `data.id` — a kiszállítás azonosítója. Tárolja a vevő rendelésével együtt.
- `data.status` — `0` = függőben (fizetésre vár), `1` = megerősítve, `2` = úton, `3` = feladva, `4` = lemondva, `5` = sikertelen.
- `storage_package_ids` — ezek a csomagok most ehhez a kiszállításhoz vannak zárolva, és már nem jelennek meg a 9. lépésben.
- Kötelező mezők: `warehouse_id`, `storage_package_ids`, `delivery_name`, `delivery_telephone`, `delivery_address_1`, `delivery_city`, `delivery_province`, `delivery_country`, `delivery_postcode`. Minden csomagnak ugyanabból a raktárból kell származnia.

**GraphQL:** `customerCreateShipoutOrder` ([GraphQL kézikönyv](/api/graphql/documentation#/customer/customerCreateShipoutOrder))

```graphql
mutation {
  customerCreateShipoutOrder(
    service_code: "intl_express"
    warehouse_id: 7
    storage_package_ids: [5001]
    delivery_name: "Maya Chen"
    delivery_telephone: "6135550142"
    delivery_email: "maya.chen@example.com"
    delivery_address_1: "150 Elgin St"
    delivery_city: "Ottawa"
    delivery_province: "ON"
    delivery_country: "CA"
    delivery_postcode: "K2P1L4"
    note: "Web order NW-20931"
  ) {
    result
    message
    data { id status total_price has_items_needing_quote storage_package_ids }
  }
}
```

**Ellenőrzés:** a válasz tartalmaz egy kiszállítás-`id` értéket. A kiválasztott tárolási csomagok ehhez a kéréshez vannak zárolva.

## 11. A kiszállítás kifizetése

A raktár a kiszállítást a kifizetése után dolgozza fel. Az integráció az ügyfél fiókjából fizeti ki a fennmaradó összeget; a teljes összeg kifizetéséhez hagyja el az `amount` mezőt.

**REST:** `POST /api/v1/customer/shipout-orders/{id}/pay` — [REST kézikönyv](/api/documentation#/paths/v1-customer-shipout-orders-id--pay/post)

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

```json
{
  "result": true,
  "data": {
    "transaction_id": 9001,
    "amount": "48.25",
    "order_status": 1,
    "paid_amount": 48.25,
    "remaining_balance": 0
  }
}
```

- `amount` (kérés, opcionális) — részösszeg; alapértelmezés szerint a teljes fennmaradó összeg.
- `order_status` — teljes kifizetés után `1` (megerősítve).
- `remaining_balance` — teljes kifizetés esetén `0`.

**GraphQL:** `customerPayShipout` ([GraphQL kézikönyv](/api/graphql/documentation#/storage-shipout/customerPayShipout))

```graphql
mutation {
  customerPayShipout(id: 8001) {
    result
    message
    data { transaction_id amount order_status paid_amount remaining_balance }
  }
}
```

**Ellenőrzés:** a fizetés összeget rögzít (vagy `402` / `422` választ ad egyértelmű indokkal). A `402` azt jelenti, hogy az egyenleg nem elegendő; a `422` azt, hogy a rendelés még nem fizethető ki (például még kézi árajánlatra vár), vagy az összeg érvénytelen.

## 12. A kiszállítás lekérdezése és követése

Az integráció lekérdezi a kiszállítást az állapota követéséhez, majd miután a raktár feladta, a követési szám alapján követi a küldeményt.

**REST:** `GET /api/v1/customer/shipout-orders/{id}` — [REST kézikönyv](/api/documentation#/paths/v1-customer-shipout-orders-id/get)

```bash
curl https://YOUR_HOST/api/v1/customer/shipout-orders/8001 \
  -H "Authorization: Bearer ACCESS_TOKEN"
```

```json
{
  "result": true,
  "data": {
    "id": 8001,
    "status": 3,
    "status_label": "Shipped",
    "warehouse": { "id": 7, "name": "Toronto Hub" },
    "shipping_service": { "id": 5, "service_code": "intl_express" },
    "total_price": "48.25",
    "paid_amount": 48.25,
    "remaining_balance": 0,
    "can_be_paid": false,
    "can_be_cancelled": false,
    "storage_packages": [{ "id": 5001, "storage_order_id": 1024, "description": "Insulated jackets, carton of 12" }]
  }
}
```

- `status` / `status_label` — a kiszállítás aktuális állapota.
- `can_be_paid` / `can_be_cancelled` — jelenleg engedélyezett-e a 11. vagy a 14. lépés.

Ha van követési szám:

**REST:** `GET /api/v1/tracking/{trackingNumber}` — [REST kézikönyv](/api/documentation#/paths/v1-tracking-trackingNumber/get)

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

```json
{
  "result": true,
  "deliveried": false,
  "data": [
    { "tracking_event_status_id": 3, "description": "Package picked up", "location_city": "Toronto", "updated_at": "2026-12-02 14:30:00" }
  ]
}
```

- `data[]` — a követési események időrendben.
- `deliveried` — `true`, amint a küldeményt kézbesítették.

**GraphQL:** `trackingPublic` ([GraphQL kézikönyv](/api/graphql/documentation#/tracking/trackingPublic))

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

**Ellenőrzés:** a kiszállítás lekérdezése a várt `status` értéket adja. A nyilvános követés megtalálja a küldeményt, amint van követési szám.

## 13. Feliratkozás webhookokra

A webhookok a követési és állapotváltozásokat a szerverére küldik, így nincs szükség ismételt lekérdezésre. Az ügyfélfiók maga állítja be a webhook URL-jeit és az aláírási titkot; a beállítások az ügyfélfiókon tárolódnak, nem a vállalkozáson.

**REST:** `PUT /api/v1/webhook-settings` — [REST kézikönyv](/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" \
  -d '{
    "tracking_event_webhook_url": "https://hooks.northwind-outdoor.example/tracking",
    "order_status_change_webhook_url": "https://hooks.northwind-outdoor.example/status",
    "webhook_sign_secret": "nw-webhook-secret-2026-0123456789"
  }'
```

```json
{
  "result": true,
  "changed_keys": ["tracking_event_webhook_url", "order_status_change_webhook_url", "webhook_sign_secret"],
  "recipient_type": "customer",
  "settings": {
    "webhook_sign_secret": "************6789",
    "tracking_event_webhook_url": "https://hooks.northwind-outdoor.example/tracking",
    "order_status_change_webhook_url": "https://hooks.northwind-outdoor.example/status"
  }
}
```

- Csak a beküldött kulcsok változnak; ismeretlen kulcs vagy érvénytelen URL esetén a válasz `400`.
- `recipient_type` — a `customer` érték megerősíti, hogy a beállítások az ügyfélfiókhoz tartoznak.
- `webhook_sign_secret` — 16–255 karakter; tárolja a szerverén az aláírások ellenőrzéséhez.

**GraphQL:** `webhookSettingsUpdate` ([GraphQL kézikönyv](/api/graphql/documentation#/webhooks/webhookSettingsUpdate)).

Ellenőrizze a **v2** aláírást: `HMAC_SHA256(timestamp + "." + raw_body, secret)`, összevetve az `X-Webhook-Signature-V2` fejléccel. Az ismétlődéseket az `X-Webhook-Event-Id` alapján szűrje ki. **3 másodpercen belül 2xx** választ adjon.

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

**Ellenőrzés:** a frissítés a beküldött kulcsokkal adja vissza a `changed_keys` értéket, és az URL-jére érkező tesztesemény megfelel a fenti aláírás-ellenőrzésnek.

## 14. Kiszállítás vagy tárolási rendelés lemondása

A lemondás felszabadítja a lefoglaltakat. A kiszállítás lemondása visszahelyezi a csomagjait a készletbe; a tárolási rendelés lemondása leállítja azt a foglalást, amelynek áruját még nem vették át.

**REST:** `POST /api/v1/customer/shipout-orders/{id}/cancel` — [REST kézikönyv](/api/documentation#/paths/v1-customer-shipout-orders-id--cancel/post)

```bash
curl -X POST https://YOUR_HOST/api/v1/customer/shipout-orders/8001/cancel \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"reason": "Buyer cancelled web order NW-20931"}'
```

```json
{ "result": true, "message": "Shipout cancelled." }
```

- `reason` (opcionális) — a lemondással együtt rögzítésre kerül.
- A kiszállítás csak függőben (`0`) vagy megerősített (`1`) állapotban mondható le.

**GraphQL:** `customerCancelShipout` ([GraphQL kézikönyv](/api/graphql/documentation#/storage-shipout/customerCancelShipout))

```graphql
mutation {
  customerCancelShipout(id: 8001, reason: "Buyer cancelled web order NW-20931") {
    result
    message
  }
}
```

Ez feloldja a tárolási csomagok zárolását. Magát a tárolást a `POST /api/v1/customer/storage-orders/{id}/cancel` hívással lehet lemondani, amíg ez még megengedett (`pending payment`, `confirmed`, `waiting for pickup` vagy `awaiting dropoff` állapotban; [REST kézikönyv](/api/documentation#/paths/v1-customer-storage-orders-id--cancel/post)).

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

```json
{ "result": true, "message": "Storage order cancelled", "data": { "id": 1024, "status": "cancelled" } }
```

- A tárolási rendelésre már kifizetett összeg jóváírásra kerül az ügyfél egyenlegén.
- Bármely más állapotú tárolási rendelés esetén a válasz `403`.

**Ellenőrzés:** a `422` azt jelenti, hogy ez az állapot nem mondható le. A kiszállítás sikeres lemondása után a 9. lépés ismét listázza a csomagokat.

## 15. Hibakezelés

| Helyzet | HTTP-állapot | Kód | Teendő az integrációban |
|---|---|---|---|
| Hiányzó vagy lejárt token, vagy nem megfelelő fióktípus | 401 | — | Lépjen be újra ügyfélként (4. lépés). |
| Tárolási árajánlat `warehouse_id` vagy dátumok nélkül | 400 | — | Küldje el a `warehouse_id`, `start_date` és `end_date` értéket. |
| A tárolási rendelés érvényesítése sikertelen (hiányzó tételméretek, az `end_date` nem a `start_date` utáni, hiányzó `packaging_id`) | 422 | — | Olvassa ki az `errors` értéket, javítsa a mezőket, és küldje el újra. |
| Tárolási fizetés elégtelen egyenleggel, vagy a rendelés már teljesen ki van fizetve | 400 | — | Töltse fel az egyenleget (a válasz tartalmazza a `customer_balance` értéket), vagy álljon meg, ha már ki van fizetve. |
| Tárolási fizetés a megengedett tartományon kívüli `custom_amount` értékkel | 422 | — | A minimum és a fennmaradó összeg közötti összeget fizessen. |
| A tárolási rendelés a jelenlegi állapotában nem mondható le | 403 | — | Kérje meg a raktárat a rendelés kezelésére; ne próbálja újra. |
| A kiszállítás le van tiltva ennél az ügyfélnél | 403 | — | Kérje meg a vállalkozást, hogy engedélyezze a kiszállítást az ügyfélfiók számára. |
| Ismeretlen szolgáltatáskód, vagy a kiszállítás / tárolási rendelés nem található | 404 | — | Kérdezze le újra a szolgáltatáslistát, vagy ellenőrizze a tárolt azonosítót. |
| A csomag nem elérhető, a csomagok különböző raktárakból származnak, vagy a szolgáltatás nem érhető el a raktárból | 422 | — | Kérdezze le újra a 9. lépést, és egyetlen raktárból válasszon elérhető csomagokat. |
| A szállítási szolgáltatás nem tudja beárazni a küldeményt, és elutasítja | 422 | `unpriced_refused` | Válasszon másik szolgáltatást vagy célállomást; semmi nem jött létre. |
| Kiszállítás fizetése elégtelen egyenleggel | 402 | — | Töltse fel az egyenleget, majd ismételje meg a 11. lépést. |
| A kiszállítás még nem fizethető ki (kézi árajánlatra vár), vagy az összeg érvénytelen | 422 | — | Várja meg az árat, kérdezze le újra a kiszállítást, majd fizessen. |
| A kiszállítás a jelenlegi állapotában nem mondható le | 422 | — | A küldemény már folyamatban van; ne próbálja újra. |
| Ugyanaz az `Idempotency-Key` eltérő törzzsel elküldve | 409 | `IDEMPOTENCY_CONFLICT` | Eltérő kéréshez használjon új kulcsot. |
| Az ugyanazzal az `Idempotency-Key` értékkel küldött eredeti kérés feldolgozása még folyamatban van | 409 | `IDEMPOTENCY_IN_PROGRESS` | Várjon `Retry-After` másodpercig, és küldje el újra ugyanazt a kérést. |

## Tesztlista

- [ ] A tárolási konfiguráció raktár-`id` értéket ad.
- [ ] A tárolási árajánlat árat ad, a tárolás létrehozása pedig `data.id` értéket ad.
- [ ] A tárolás kifizetése sikeres, **vagy** meggyőződött arról, hogy a tárcát fel kell tölteni.
- [ ] Az elérhető tételek listája az átvett csomagokat mutatja (`storage_package_ids`).
- [ ] A kiszállítás becslése árat vagy `has_items_needing_quote` értéket ad, a kiszállítás létrehozása pedig `id` értéket ad, és zárolja a csomagokat.
- [ ] A kiszállítás kifizetése sikeres (vagy a `402` / `422` válasz értelmezett).
- [ ] A nyilvános követés megtalálja a küldeményt, amint van követési szám.
- [ ] A kiszállítás lemondása felszabadítja a csomagokat, **vagy** ez az állapot nem mondható le.
- [ ] Egy létrehozás ugyanazzal az `Idempotency-Key` értékkel és törzzsel történő megismétlése `replayed: true` értéket ad, és nem jön létre második rendelés.
- [ ] A webhook-beállítások `recipient_type: customer` értéket adnak, és egy beérkezett esemény megfelel a v2 aláírás-ellenőrzésnek.
