# Wybór usługi z oferty

Większość integracji z góry wie, której usługi przewoźnika chce, i przekazuje
stały `service_id`. Gdy tak nie jest — gdy chcesz pokazać klientowi (albo
własnemu silnikowi reguł) zbiór usług, które faktycznie mogą przewieźć **tę**
paczkę, i jedną z nich wybrać — najpierw wyceń przesyłkę we wszystkich
usługach, a potem wybieraj według pełnego zestawu ograniczeń, **nie samej
ceny**.

Najtańsza wycena to często zły wybór: usługa punkt–punkt potrzebuje
identyfikatora automatu, międzynarodowa — danych celnych, część usług wymaga
zamówionego odbioru kurierskiego. Usługa, której wymagań nie spełnisz,
wywali się przy `order_send`, nie przy wycenie.

Ten przewodnik obejmuje **początek** przepływu. Wszystko po utworzeniu
zamówienia (etykieta, śledzenie, anulowanie) jest identyczne jak w
[Cyklu życia zamówienia](/pl/guides/order-lifecycle) — ta strona zastępuje
tylko sposób wyboru usługi.

`order_valuation` (bez `service_id`) → `service_structure` (mapowanie wymagań) →
uzupełnienie payloadu (odbiór / punkt / cło) → `order_send` →
etykieta / śledzenie / … (zobacz [Cykl życia zamówienia](/pl/guides/order-lifecycle))

Przykłady używają [SDK PHP](/pl/sdk); każdy krok podaje nazwę metody API,
więc przepływ mapuje się 1:1 na surowe
[żądania z kopertą](/pl/legacy/hmac-signing) z dowolnego języka.

## 1. Wyceń we wszystkich usługach — `order_valuation` bez usługi

Zbuduj zamówienie na **neutralnym szkielecie** — odbiór `SELF`, bez punktu,
bez cła — i **pomiń usługę**. `order_valuation` zwróci wtedy `price_table` z
każdą usługą, która może przewieźć tę paczkę.

```php
use AlsendoOne\SDK\DTO\Address;
use AlsendoOne\SDK\DTO\Request\OrderRequest;
use AlsendoOne\SDK\DTO\Request\PickupRequest;
use AlsendoOne\SDK\DTO\Request\ShipmentRequest;

$shipment = new ShipmentRequest('PACZKA', 2.5, 40, 30, 20); // kg, cm

$order = OrderRequest::create()
    ->setSenderAddress(new Address(/* ... */))
    ->setReceiverAddress(new Address(/* ... */))
    ->addShipment($shipment)
    ->setPickup(new PickupRequest('SELF'));   // neutralnie — bez setService()

$valuation = $client->getValuation($order);

foreach ($valuation->getPriceTable() as $price) {
    // Ceny wracają w groszach (1 PLN = 100 gr)
    printf("%d: %.2f PLN brutto\n", $price->getServiceId(), $price->getPriceGross() / 100);
}
```

Bez SDK — to samo wywołanie, bez `service_id` w payloadzie:

```bash
--data-urlencode 'request={"order":{"address":{"sender":{...},"receiver":{...}},"pickup":{"type":"SELF"},"shipment":[{"dimension1":40,"dimension2":30,"dimension3":20,"weight":2.5,"is_nstd":0}]}}'
# POST order_valuation/  → response.price_table, jeden wpis na service_id, kwoty w groszach
```

:::tip[Wyceniaj szkielet, nie finalne zamówienie]

Dodanie pól specyficznych dla usługi (punkt, odbiór kurierski, cło) może
zmienić albo ograniczyć dostępną ofertę. Najpierw wyceń neutralne zamówienie,
uzupełniaj dopiero **po** wyborze.

:::

## 2. Zmapuj ofertę na `service_structure` — sprawdź wymagania

`price_table` mówi, **kto** może przewieźć paczkę i za ile.
`service_structure` mówi, **czego każda usługa wymaga**. Połącz je po
`service_id`. Słownik czytaj z cache (odświeżaj mniej więcej raz dziennie),
zamiast wołać go przy każdej wycenie — zobacz
[Dobre praktyki](/pl/guides/best-practices#nie-hardkoduj-przewoźników-i-usług).

```php
$structure = $client->getServiceStructure();

$byId = [];
foreach ($structure->getServices() as $svc) {
    $byId[(int) $svc['service_id']] = $svc;
}

$offer = [];
foreach ($valuation->getPriceTable() as $price) {
    $svc = $byId[$price->getServiceId()] ?? null;
    if ($svc === null) {
        continue; // wycenione, ale nieobecne w aktualnym słowniku — pomiń
    }

    $offer[] = [
        'service_id'     => $price->getServiceId(),
        'name'           => $svc['name'],
        'supplier'       => $svc['supplier'],
        'price_gross'    => $price->getPriceGross(),   // grosze
        'domestic'       => (int) $svc['domestic'],        // 1 krajowa / 0 międzynarodowa
        'pickup_courier' => (int) $svc['pickup_courier'],  // 0 niedostępny / 1 dostępny / 2 wymagany
        'door_to_door'   => (int) $svc['door_to_door'],
        'door_to_point'  => (int) $svc['door_to_point'],
        'point_to_point' => (int) $svc['point_to_point'],
        'point_to_door'  => (int) $svc['point_to_door'],
    ];
}
```

Co każda flaga oznacza dla payloadu, który zbudujesz w kroku 3:

| Warunek usługi (z `service_structure`) | Co musisz dodać przed `order_send` |
|---|---|
| `pickup_courier = 2` (kurier **wymagany**) | `pickup.type = COURIER` + `date` / `hours_from` / `hours_to` — okno wybierz z `pickup_hours` |
| `pickup_courier = 0` (kurier niedostępny) | tylko `SELF` / nadanie własne — bez odbioru kurierskiego |
| trasa przez punkt (`door_to_point` / `point_to_point` / `point_to_door` = 1) | `foreign_address_id` z [`points/{type}/`](/pl/reference/v2-legacy) po właściwej stronie (nadawca i/lub odbiorca) |
| `domestic = 0` (międzynarodowa) | cło: `customs_data` per pozycja przesyłki (+ blok `customs` na poziomie zamówienia, gdy usługa go wymaga) |
| pobranie / usługi dodatkowe | sprawdź `options` w słowniku oraz `option_cod` / `option_send` / `option_deliver` na wybranym punkcie |

:::caution[Najtaniej ≠ najlepiej]

Wybieraj według własnej polityki — SLA, trasa i to, czy faktycznie spełnisz
dodatkowe wymagania — nie według najniższego `price_gross`. Cały sens tego
przepływu to zawężenie oferty do usług *wykonalnych*.

:::

## 3. Uzupełnij payload dla wybranej usługi

Po wyborze ustaw usługę i dodaj **tylko** te pola, których ta usługa
potrzebuje.

```php
use AlsendoOne\SDK\DTO\Request\CustomsDataRequest;
use AlsendoOne\SDK\DTO\Request\PickupRequest;
use AlsendoOne\SDK\Type\Service;

$serviceId = /* Twój wybór z $offer */;
$svc       = $byId[$serviceId];
$service   = Service::from($serviceId);

$order->setService($service);

// Kurier wymagany → zamów odbiór w dostępnym oknie
if ((int) $svc['pickup_courier'] === 2) {
    $days = $client->getPickupHours($senderPostalCode, $service)->getHours();
    $day  = reset($days);
    $order->setPickup(new PickupRequest('COURIER', $day->getDate(), '09:00', '15:00'));
}

// Trasa przez punkt → dołącz punkt przez foreign_address_id u odbiorcy
// (id punktu weź z points/<type>/). Address jest niemutowalny — zbuduj nowy.
if ((int) $svc['point_to_point'] === 1 || (int) $svc['door_to_point'] === 1) {
    $order->setReceiverAddress(new Address(
        name: 'Jan Kowalski',
        phone: '+48600000000',
        email: 'jan@example.com',
        postalCode: '00-001',
        city: 'Warszawa',
        countryCode: 'PL',
        foreignAddressId: $pointId,          // np. id automatu InPost
    ));
}

// Międzynarodowa → zadeklaruj cło per pozycja
if ((int) $svc['domestic'] === 0) {
    $shipment->addCustomsData(new CustomsDataRequest(
        'Cotton T-shirt', 3, 1550, 1, '6109100010', 'PL' // opis, ilość, cena jedn. (gr), waga, kod HS, pochodzenie
    ));
}
```

Blok `customs` na poziomie zamówienia (`content_type`, `EORI_number`,
`documents_form`, `documents`) nie ma jeszcze typowanego buildera. Jeśli
usługa go wymaga, zamień zamówienie przez `toArray()`, dodaj blok i przekaż
tablicę do `sendOrder()` — przyjmuje obie formy.

:::caution[Pułapka nazewnicza odbioru]

Przy tworzeniu zamówienia pola są w liczbie mnogiej — `hours_from` /
`hours_to`. Osobne endpointy `pickup` / `batch_pickup` używają liczby
pojedynczej `hour_from` / `hour_to`. Zobacz [Typy odbioru](/pl/guides/pickups).

:::

Uzupełnienie zamówienia może zmienić jego cenę — jeśli pokazałeś klientowi
pierwszą wycenę, wyceń finalny payload ponownie przed wysłaniem (zobacz
[Cykl życia zamówienia → krok 2b](/pl/guides/order-lifecycle#2b-opcjonalnie-wyceń-ponownie-finalny-payload--order_valuation)).

## 4. Utwórz zamówienie — `order_send`

```php
$created = $client->sendOrder($order);

$created->getId();            // id zamówienia Apaczka
$created->getWaybillNumber(); // numer śledzenia przewoźnika
$created->getStatus();        // np. NEW
```

:::caution

`order_send` **nie jest idempotentne** — ponowione żądanie po timeoucie może
utworzyć zduplikowane zamówienie. Przed ponowieniem uzgodnij stan przez
`getOrders()`, nigdy nie ponawiaj zapisu na ślepo. Zobacz
[dobre praktyki](/pl/guides/best-practices#ostrożnie-z-ponawianiem-żądań).

:::

## 5. Dalej jak zwykle

Od tego miejsca przepływ jest identyczny jak w
[Cyklu życia zamówienia](/pl/guides/order-lifecycle): pobierz etykietę,
opcjonalnie kod nadania / potwierdzenie zbiorcze, potem śledź przesyłkę
([Śledzenie](/pl/guides/tracking)). Anulowanie i wgląd w zamówienia bez
zmian.

---

**Zobacz też:** [Cykl życia zamówienia](/pl/guides/order-lifecycle) ·
[Typy odbioru](/pl/guides/pickups) · [Śledzenie](/pl/guides/tracking) ·
[Dobre praktyki](/pl/guides/best-practices) ·
[Podpisywanie żądań (HMAC)](/pl/legacy/hmac-signing)
