feat(shipments): add ShipmentProviderInterface and ShipmentProviderRegistry
- Introduced ShipmentProviderInterface to define the contract for shipment providers. - Implemented ShipmentProviderRegistry to manage and retrieve shipment providers. - Added a new tool for probing Apaczka order_send payload variants, enhancing debugging capabilities.
This commit is contained in:
1
.vscode/ftp-kr.diff.tmp_fix_invoice_order21.2.php
vendored
Normal file
1
.vscode/ftp-kr.diff.tmp_fix_invoice_order21.2.php
vendored
Normal file
@@ -0,0 +1 @@
|
||||
c:\visual studio code\projekty\orderPRO\tools\tmp_fix_invoice_order21.php
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
## Status
|
||||
- Projekt po resecie do trybu `users-only`.
|
||||
- UI korzysta z globalnego standardu naglowkow sekcji (`h2/h3/h4.section-title`) definiowanego centralnie w `resources/scss/app.scss` i buildowanego do `public/assets/css/app.css`.
|
||||
|
||||
## Moduly aktywne
|
||||
- `App\Modules\Auth`
|
||||
@@ -44,6 +45,7 @@
|
||||
- `GET /settings/integrations/allegro/oauth/callback`
|
||||
- `GET /settings/integrations/apaczka`
|
||||
- `POST /settings/integrations/apaczka/save`
|
||||
- `POST /settings/integrations/apaczka/test`
|
||||
- `GET /settings/integrations/inpost`
|
||||
- `POST /settings/integrations/inpost/save`
|
||||
- `GET /settings/integrations/shoppro`
|
||||
@@ -92,6 +94,8 @@
|
||||
- `App\Modules\Users\UserRepository`
|
||||
- `App\Modules\Settings\ApaczkaIntegrationController`
|
||||
- `App\Modules\Settings\ApaczkaIntegrationRepository`
|
||||
- `App\Modules\Settings\ApaczkaApiClient`
|
||||
- `App\Modules\Settings\CarrierDeliveryMethodMappingRepository`
|
||||
- `App\Modules\Settings\InpostIntegrationController`
|
||||
- `App\Modules\Settings\InpostIntegrationRepository`
|
||||
- `App\Modules\Settings\IntegrationsHubController`
|
||||
@@ -106,6 +110,9 @@
|
||||
- `App\Modules\Settings\AllegroOrdersSyncService`
|
||||
- `App\Modules\Settings\AllegroOrderSyncStateRepository`
|
||||
- `App\Modules\Settings\AllegroStatusSyncService`
|
||||
- `App\Modules\Shipments\ShipmentProviderInterface`
|
||||
- `App\Modules\Shipments\ShipmentProviderRegistry`
|
||||
- `App\Modules\Shipments\ApaczkaShipmentService`
|
||||
|
||||
## Przeplyw Zamowienia > Lista zamowien
|
||||
- `GET /orders/list`:
|
||||
@@ -220,6 +227,29 @@
|
||||
- `shoppro_order_status_sync` -> `ShopproStatusSyncHandler::handle(...)` (synchronizacja statusow shopPRO wg kierunku ustawionego per instancja).
|
||||
- `shoppro_payment_status_sync` -> `ShopproPaymentStatusSyncHandler::handle(...)` (odswiezanie statusu platnosci zamowien shopPRO na podstawie flagi `paid`).
|
||||
|
||||
## Przeplyw tworzenia przesylki
|
||||
- `GET /orders/{id}/shipment/prepare`:
|
||||
- `ShipmentController::prepare(Request): Response`,
|
||||
- laduje uslugi dostawy providerow z `ShipmentProviderRegistry` (aktualnie: `allegro_wza`, `apaczka`),
|
||||
- pobiera automatyczne mapowanie formy dostawy przez `CarrierDeliveryMethodMappingRepository` (`source_system` + `source_integration_id` + `order_delivery_method`),
|
||||
- dla dostaw punktowych (`parcel_external_id`/`parcel_name`) prefillem `receiver_name` sa dane klienta (a nie nazwa punktu/metody dostawy),
|
||||
- gdy mapowanie nie zostanie znalezione, buduje komunikat diagnostyczny (brak mapowan dla instancji lub brak mapowania konkretnej metody) i przekazuje go do widoku.
|
||||
- `POST /orders/{id}/shipment/create`:
|
||||
- `ShipmentController::create(Request): Response`,
|
||||
- wybiera providera dynamicznie po `provider_code` i deleguje do `ShipmentProviderInterface::createShipment(...)`,
|
||||
- dla `apaczka` waliduje wymagane punkty odbioru/nadania wg definicji uslugi (`service_structure`) i przy bledzie wyceny zwraca rozszerzona diagnostyke parametrow,
|
||||
- `apaczka` uzupelnia i wysyla `contact_person` dla nadawcy (z `Ustawienia > Dane firmy`) i odbiorcy (fallback z danych zamowienia),
|
||||
- `apaczka` ustawia jawnie `pickup.type` (`SELF`/`COURIER`) na podstawie uslugi i obecnosci `sender_point_id`; dla `COURIER` dopelnia tez `pickup.date`, `pickup.hours_from`, `pickup.hours_to`,
|
||||
- dla uslug punktowych `apaczka` payload adresu zawiera aliasy identyfikatora punktu (`point`, `foreign_address_id`, `point_id`) dla nadania i odbioru,
|
||||
- `ApaczkaShipmentService::buildReceiverAddress(...)` sklada dane odbiorcy z fallbackami (formularz -> delivery -> punkt odbioru z `parcel_name` -> customer), a dla przesylek punktowych dodatkowo probuje uzupelnic adres punktu przez API `points`; przy dalszych brakach dopelnia minimum techniczne, aby nie blokowac tworzenia.
|
||||
- `GET /orders/{id}/shipment/{packageId}/status`:
|
||||
- `ShipmentController::checkStatus(Request): Response`,
|
||||
- wybiera providera po `shipment_packages.provider` i deleguje `checkCreationStatus(...)`.
|
||||
- `POST /orders/{id}/shipment/{packageId}/label`:
|
||||
- `ShipmentController::label(Request): Response`,
|
||||
- wybiera providera po `shipment_packages.provider` i deleguje `downloadLabel(...)`,
|
||||
- dla Apaczka bledy typu `Label is not available for this order` oznaczaja paczke jako `error`, aby nie ponawiac nieskutecznych prob pobrania.
|
||||
|
||||
## Przeplyw Ustawienia > Integracje > Allegro
|
||||
- `GET /settings/integrations/allegro`:
|
||||
- `AllegroIntegrationController::index(Request): Response`
|
||||
@@ -394,6 +424,8 @@
|
||||
- pobiera liste zamowien i (opcjonalnie) szczegoly zamowienia z API shopPRO,
|
||||
- mapuje kwoty z fallbackami (`summary`, `paid`, `transport_cost`) oraz ceny pozycji (`price_brutto`),
|
||||
- uzupelnia `order_items.media_url` przez pobranie `products/get` po `product_id`, gdy zamowienie nie zawiera obrazu.
|
||||
- wykrywa zadanie faktury takze po obecnosci danych firmowych (`firm_*`) i ustawia `orders.is_invoice`,
|
||||
- mapuje adres faktury do `order_addresses.address_type=invoice` (firma/NIP/adres) na podstawie pol `invoice`/`billing*`/`firm_*`,
|
||||
- mapuje punkty odbioru (`inpost_paczkomat` / `orlen_point`) do adresu `delivery` (`parcel_external_id`, `parcel_name`, ulica/kod/miasto),
|
||||
- uzupelnia `delivery` o telefon/e-mail klienta i etykiete metody dostawy z kosztem (`transport_cost`).
|
||||
- `ShopproStatusSyncService`:
|
||||
|
||||
@@ -45,6 +45,32 @@
|
||||
- tabela przechowuje mapowanie formy shopPRO na usluge Allegro WZA/InPost (`allegro_delivery_method_id`, `allegro_credentials_id`, `allegro_carrier_id`, `allegro_service_name`, `carrier`).
|
||||
- 2026-03-08: Poprawiono mapowanie importu zamowien shopPRO (kwoty i miniatury pozycji) - bez zmian schematu bazy.
|
||||
- 2026-03-08: Poprawiono mapowanie danych wysylki shopPRO (paczkomat/punkt odbioru + kontakt klienta + koszt transportu) - bez zmian schematu bazy.
|
||||
- 2026-03-08: Dodano migracje `20260308_000044_create_carrier_delivery_method_mappings_table.sql`:
|
||||
- nowa tabela `carrier_delivery_method_mappings` (wspolne mapowanie form dostawy dla roznych zrodel zamowien i providerow wysylek),
|
||||
- migracja backfilluje dane z `allegro_delivery_method_mappings` i `shoppro_delivery_method_mappings`.
|
||||
- 2026-03-08: Dodano migracje `20260308_000045_extend_apaczka_credentials.sql`:
|
||||
- rozszerzenie `apaczka_integration_settings` o `app_id` i `app_secret_encrypted`,
|
||||
- migracja przenosi legacy sekret z `api_key_encrypted` do `app_secret_encrypted`.
|
||||
- 2026-03-08: Poprawiono mapowanie danych faktury w imporcie shopPRO - bez zmian schematu:
|
||||
- `orders.is_invoice` jest wykrywany takze po polach firmowych (`firm_*`),
|
||||
- adres faktury jest zapisywany do istniejacej tabeli `order_addresses` (`address_type=invoice`, `company_name`, `company_tax_number`).
|
||||
- 2026-03-08: Poprawiono diagnostyke tworzenia przesylek Apaczka - bez zmian schematu:
|
||||
- payload tworzenia przesylki przekazuje punkty odbioru/nadania (`receiver.point`, `sender.point`) z danych formularza,
|
||||
- komunikaty bledow tworzenia zawieraja rozszerzona diagnostyke parametrow wyceny.
|
||||
- 2026-03-08: Poprawiono obsluge bledow etykiet Apaczka - bez zmian schematu:
|
||||
- przy odpowiedzi API `Label is not available for this order` rekord `shipment_packages` jest oznaczany statusem `error` i zachowuje tresc bledu.
|
||||
- 2026-03-08: Poprawiono fallback danych odbiorcy dla przesylek punktowych Apaczka (Orlen/InPost) - bez zmian schematu:
|
||||
- brakujace dane adresowe odbiorcy sa uzupelniane na etapie budowania payloadu z danych zamowienia (`delivery`, `customer`) oraz metadanych punktu (`parcel_name`).
|
||||
- 2026-03-08: Rozszerzono fallback adresu odbiorcy dla przesylek punktowych Apaczka - bez zmian schematu:
|
||||
- serwis probuje uzupelnic adres punktu przez API `points` po `receiver_point_id`,
|
||||
- przy dalszych brakach danych dla przesylki punktowej dopelniane jest minimum techniczne wymagane przez lokalna walidacje payloadu.
|
||||
- 2026-03-08: Poprawiono przekazywanie punktow w payloadzie Apaczka (nadanie/odbior) - bez zmian schematu:
|
||||
- payload wysyla aliasy identyfikatora punktu (`point`, `foreign_address_id`, `point_id`) dla zgodnosci z wariantami API.
|
||||
- 2026-03-08: Uzupelniono payload odbioru kurierem Apaczka (`pickup.date`, `pickup.hours_from`, `pickup.hours_to`) - bez zmian schematu.
|
||||
- 2026-03-08: Dodano migracje `20260308_000046_extend_company_settings_contact_person.sql`:
|
||||
- rozszerzenie `company_settings` o `sender_contact_person` (osoba kontaktowa nadawcy),
|
||||
- wykorzystywane w payloadzie Apaczka jako `address.sender.contact_person`.
|
||||
- 2026-03-08: Ujednolicono styl naglowkow sekcji UI (`section-title`) - bez zmian schematu bazy.
|
||||
|
||||
## Tabele
|
||||
|
||||
@@ -218,9 +244,29 @@
|
||||
- Kolumny:
|
||||
- `id` (PK, tinyint unsigned),
|
||||
- `integration_id` (int unsigned, UNIQUE, FK -> `integrations.id`),
|
||||
- `app_id` (varchar 128, nullable),
|
||||
- `app_secret_encrypted` (text, nullable),
|
||||
- `api_key_encrypted` (text, nullable),
|
||||
- `created_at`, `updated_at`.
|
||||
|
||||
### `carrier_delivery_method_mappings`
|
||||
- Wspolne mapowanie form dostawy zamowien na providerow wysylek (model docelowy pod wielu kurierow: `allegro_wza`, `apaczka`, kolejne).
|
||||
- Kolumny:
|
||||
- `id` (PK, int unsigned, AI),
|
||||
- `source_system` (varchar 32; np. `allegro`, `shoppro`),
|
||||
- `source_integration_id` (int unsigned; `0` dla mapowan globalnych, np. Allegro),
|
||||
- `order_delivery_method` (varchar 200),
|
||||
- `provider` (varchar 50; np. `allegro_wza`, `apaczka`),
|
||||
- `provider_service_id` (varchar 128),
|
||||
- `provider_account_id` (varchar 128, nullable),
|
||||
- `provider_carrier_id` (varchar 128, nullable),
|
||||
- `provider_service_name` (varchar 255, nullable),
|
||||
- `created_at`, `updated_at`.
|
||||
- Indeksy:
|
||||
- `carrier_dm_mapping_unique` (UNIQUE: `source_system`, `source_integration_id`, `order_delivery_method`),
|
||||
- `carrier_dm_mapping_provider_idx` (`provider`),
|
||||
- `carrier_dm_mapping_source_idx` (`source_system`, `source_integration_id`).
|
||||
|
||||
### `inpost_integration_settings`
|
||||
- Tabela ustawien specyficznych InPost ShipX (`id = 1`); token API utrzymywany bazowo w `integrations.api_key_encrypted`.
|
||||
- Kolumny:
|
||||
@@ -238,6 +284,29 @@
|
||||
- `weekend_delivery`, `auto_insurance_value`, `multi_parcel` (tinyint 0/1),
|
||||
- `created_at`, `updated_at`.
|
||||
|
||||
### `company_settings`
|
||||
- Ustawienia firmy/nadawcy wykorzystywane m.in. przy tworzeniu przesylek.
|
||||
- Kolumny:
|
||||
- `id` (PK, tinyint unsigned),
|
||||
- `company_name` (varchar 200, nullable),
|
||||
- `person_name` (varchar 200, nullable),
|
||||
- `sender_contact_person` (varchar 200, nullable),
|
||||
- `street` (varchar 200, nullable),
|
||||
- `city` (varchar 128, nullable),
|
||||
- `postal_code` (varchar 16, nullable),
|
||||
- `country_code` (char 2, default `PL`),
|
||||
- `phone` (varchar 64, nullable),
|
||||
- `email` (varchar 128, nullable),
|
||||
- `tax_number` (varchar 64, nullable),
|
||||
- `bank_account` (varchar 64, nullable),
|
||||
- `bank_owner_name` (varchar 200, nullable),
|
||||
- `default_package_length_cm` (decimal 8,1),
|
||||
- `default_package_width_cm` (decimal 8,1),
|
||||
- `default_package_height_cm` (decimal 8,1),
|
||||
- `default_package_weight_kg` (decimal 8,3),
|
||||
- `default_label_format` (varchar 8),
|
||||
- `created_at`, `updated_at`.
|
||||
|
||||
## Zasady aktualizacji
|
||||
- Po kazdej migracji dopisz:
|
||||
- nowe/zmienione tabele i kolumny,
|
||||
|
||||
@@ -1,6 +1,101 @@
|
||||
# Tech Changelog
|
||||
|
||||
## 2026-03-08
|
||||
- Poprawiono date podjazdu kuriera w payloadzie Apaczka:
|
||||
- `pickup.date` dla trybu `COURIER` jest normalizowane tak, aby nie wypadal w niedziele (niedziela -> poniedzialek),
|
||||
- `pickup.hours_to` jest ograniczane do `16:00` (wg limitu API), a okno godzinowe jest korygowane do poprawnej relacji `hours_from < hours_to`,
|
||||
- eliminuje blad API `Field PickupLocation->MaxPickupDate cannot be set to Sunday`.
|
||||
- Dodano pole firmy `sender_contact_person` (osoba kontaktowa nadawcy):
|
||||
- nowa migracja `20260308_000046_extend_company_settings_contact_person.sql`,
|
||||
- UI `Ustawienia > Dane firmy` ma nowe pole `Osoba kontaktowa nadawcy`,
|
||||
- `ApaczkaShipmentService` wysyla `contact_person` dla nadawcy z ustawien firmy.
|
||||
- Uzupelniono kontakt odbiorcy w payloadzie Apaczka:
|
||||
- `ApaczkaShipmentService` wysyla `receiver.contact_person` (fallback na dane odbiorcy/klienta z zamowienia),
|
||||
- eliminuje blad API: `Osoba kontaktowa nadawcy/odbiorcy: Pole jest wymagane`.
|
||||
- Dla tworzenia przesylek Apaczka dodano jawne pole `pickup.type` w payloadzie `order_send`:
|
||||
- gdy podano `sender_point_id` ustawiane jest `SELF`,
|
||||
- gdy brak punktu nadania i usluga dopuszcza odbior kurierem ustawiane jest `COURIER`.
|
||||
- Poprawiono obsluge punktow nadania/odbioru w payloadzie Apaczka:
|
||||
- adres `sender` i `receiver` wysyla teraz komplet aliasow punktu (`point`, `foreign_address_id`, `point_id`),
|
||||
- rozszerzono diagnostyke dla bledu API `Niepoprawny sposob nadania przesylki` o wskazowke dot. `sender_point_id` / doboru uslugi.
|
||||
- Wzmocniono fallback adresu odbiorcy dla przesylek punktowych Apaczka (Orlen/InPost):
|
||||
- `ApaczkaShipmentService` probuje teraz dodatkowo uzupelnic adres punktu (`street`, `postal_code`, `city`) przez API `points` na podstawie `receiver_point_id`,
|
||||
- jezeli zamowienie punktowe dalej nie ma kompletu danych adresowych, serwis uzupelnia techniczne minimum (`line1`, `city`, `postal_code`) zamiast blokowac tworzenie przesylki bledem walidacji lokalnej.
|
||||
- Ujednolicono i wzmocniono naglowki sekcji UI (`h2/h3/h4.section-title`) w widokach:
|
||||
- naglowki sekcyjne maja teraz wyrazniejszy styl (akcent lewy, delikatne tlo, obramowanie),
|
||||
- zmiana jest globalna (SCSS), obejmuje m.in. `Zamowienia > Przygotuj przesylke` oraz pozostale ekrany korzystajace z `section-title`.
|
||||
- Poprawiono fallback danych odbiorcy dla przesylek punktowych Apaczka (w tym Orlen Paczka):
|
||||
- `ApaczkaShipmentService::buildReceiverAddress(...)` uzupelnia brakujace pola z wielu zrodel w kolejnosci:
|
||||
- dane z formularza,
|
||||
- adres `delivery` z zamowienia,
|
||||
- dane punktu z `parcel_name` (`ulica`, `kod`, `miasto`),
|
||||
- dane klienta (`name`, `phone`, `email`, `country`),
|
||||
- adres ulicy sklada teraz `street_name + street_number`,
|
||||
- naprawiono fallback `receiver_point_id` (uzywa `delivery.parcel_external_id`, usunieto bledne odwolanie do niezdefiniowanej zmiennej).
|
||||
- Poprawiono obsluge usunietych/nieosiagalnych etykiet Apaczka:
|
||||
- gdy API zwraca `Label is not available for this order`, rekord paczki jest oznaczany jako `error` i zapisywany jest `error_message`,
|
||||
- dzieki temu przycisk etykiety nie jest ponownie pokazywany dla tego rekordu.
|
||||
- Ujednolicono flow etykiet w `Zamowienia > Przygotuj przesylke` (dla wszystkich providerow):
|
||||
- po statusie `created` etykieta jest generowana automatycznie przez cykliczne sprawdzanie statusu (backend probuje `downloadLabel`),
|
||||
- UI nie pokazuje juz przycisku `Generuj etykiete`; do czasu gotowosci widoczny jest stan `Generowanie etykiety...`,
|
||||
- przycisk `Pobierz` pojawia sie dopiero po zapisaniu pliku etykiety (`label_path`).
|
||||
- Uzgodniono payload `order_send` Apaczka z dokumentacja API v2:
|
||||
- `ApaczkaShipmentService` wysyla teraz strukture `address.sender/receiver` + `shipment[]` (`dimension1/2/3`, `weight` w kg),
|
||||
- `cod` jest przekazywany jako obiekt (`amount`, `currency`), a wartosc ubezpieczenia przez `shipment_value` + `shipment_currency`,
|
||||
- punkty sa przekazywane jako `foreign_address_id` (z aliasami diagnostycznymi),
|
||||
- naprawilo to blad wyceny dla zamowienia testowego `#21` (utworzenie przesylki zakonczone sukcesem).
|
||||
- Dodano techniczny skrypt diagnostyczny `tools/apaczka_probe_order.php`:
|
||||
- wykonuje automatyczne proby `order_send` dla wskazanego zamowienia (bez klikania w UI),
|
||||
- testuje kombinacje uslug i wariantow pol punktu, raportujac sukces/blad per proba.
|
||||
- Rozszerzono payload punktow w tworzeniu przesylki Apaczka:
|
||||
- oprocz `point` przekazywane sa teraz aliasy `foreign_address_id` i `point_id` (dla odbiorcy i nadawcy),
|
||||
- cel: kompatybilnosc z wariantami API wymagajacymi innej nazwy pola punktu.
|
||||
- Korekta walidacji punktu odbioru Apaczka:
|
||||
- usunieto twarde blokowanie tworzenia przesylki na podstawie prefiksu punktu (`POP-`),
|
||||
- informacja o prefiksie punktu pozostaje tylko jako sugestia diagnostyczna.
|
||||
- Poprawiono prefill danych odbiorcy na ekranie `Zamowienia > Przygotuj przesylke`:
|
||||
- `ShipmentController` dla dostaw do punktu (`parcel_external_id`/`parcel_name`) ustawia `receiver_name` na dane klienta (`address_type=customer`),
|
||||
- eliminuje przypadek podstawiania nazwy punktu/metody dostawy w polu `Imie i nazwisko`.
|
||||
- Poprawiono diagnostyke bledow tworzenia przesylki Apaczka:
|
||||
- `ApaczkaShipmentService` przekazuje teraz `receiver_point_id` do payloadu `receiver.point` (oraz `sender_point_id` do `sender.point`),
|
||||
- dodano walidacje wymagan uslugi na podstawie `service_structure` (np. wymagany punkt odbioru/nadania),
|
||||
- dla bledu API `Brak wyceny dla podanych parametrów zamówienia` komunikat zawiera rozszerzona diagnostyke (service_id/nazwa/supplier, punkt odbioru/nadania, gabaryt/waga) i hint o niedopasowaniu uslugi do typu punktu.
|
||||
- Poprawiono import danych faktury z shopPRO:
|
||||
- `ShopproOrdersSyncService` wykrywa fakture nie tylko po `is_invoice`/`invoice.required`, ale takze po danych firmowych (`firm_name`/`firm_nip`),
|
||||
- `ShopproOrdersSyncService::mapAddresses(...)` zapisuje adres `invoice` (firma, NIP, adres) na podstawie pol `invoice`/`billing*`/`firm_*`,
|
||||
- widok `orders/show` wyswietla teraz `company_name` i `company_tax_number` dla adresu faktury.
|
||||
- Fix prewyboru uslugi Apaczka na ekranie `Zamowienia > Przygotuj przesylke`:
|
||||
- widok `resources/views/shipments/prepare.php` odczytuje ID uslugi Apaczka z fallbackiem `service_id -> id`,
|
||||
- naprawia przypadek, gdy przewoznik (`Apaczka`) byl wybierany poprawnie, ale usluga dostawy pozostawala pusta mimo mapowania.
|
||||
- Poprawiono diagnostyke mapowania form dostawy na ekranie `Zamowienia > Przygotuj przesylke`:
|
||||
- `ShipmentController` zwraca teraz komunikat diagnostyczny, gdy brak mapowania metody dostawy,
|
||||
- komunikat rozroznia brak mapowan dla instancji `shopPRO` (`source_integration_id`) od braku mapowania konkretnej metody,
|
||||
- widok `resources/views/shipments/prepare.php` wyswietla ten komunikat bezposrednio pod informacja o metodzie z zamowienia.
|
||||
- Poprawiono UX wyszukiwania w selectach mapowania form dostawy (zakladki `Formy dostawy`):
|
||||
- `resources/views/settings/allegro.php` przeszlo z przebudowy opcji `<select>` na tryb bezpieczny (`focus first match` bez modyfikacji listy opcji),
|
||||
- eliminuje przypadki „znikajacych” opcji i problem z wyborem uslugi po zmianie przewoznika na `Apaczka`,
|
||||
- ujednolicono zachowanie z `resources/views/settings/shoppro.php`.
|
||||
- Poprawiono inicjalizacje stanu mapowania przewoznika (`allegro.php`, `shoppro.php`):
|
||||
- dla niezamapowanych form domyslny przewoznik to teraz pusty wybor (zamiast `allegro`),
|
||||
- panel uslugi (`Allegro/InPost/Apaczka/pusty`) jest synchronizowany na starcie z realna wartoscia selecta przewoznika (`showPanel(carrierSelect.value)`),
|
||||
- usunieto pozostaly po refaktorze odwolanie do nieistniejacej zmiennej `$currentAllegroId` (zastapione `$currentMethodId`).
|
||||
- Dodano architekture provider-agnostic dla wysylek:
|
||||
- nowy kontrakt `ShipmentProviderInterface`,
|
||||
- nowy rejestr `ShipmentProviderRegistry`,
|
||||
- `ShipmentController` wybiera providera dynamicznie (`allegro_wza` / `apaczka`) przy tworzeniu przesylki, sprawdzaniu statusu i pobieraniu etykiety.
|
||||
- Dodano integracje API Apaczka v2:
|
||||
- nowy klient `App\Modules\Settings\ApaczkaApiClient` (`service_structure`, `order_send`, `order_details`, `waybill`, `points`),
|
||||
- nowy serwis wysylkowy `App\Modules\Shipments\ApaczkaShipmentService`,
|
||||
- rozszerzone `Ustawienia > Integracje > Apaczka` o pola `App ID`, `App Secret` oraz test polaczenia (`POST /settings/integrations/apaczka/test`).
|
||||
- poprawiono format podpisu/requestu zgodnie z API v2 (`app_id:route:request_json:expires`, endpointy URL zamiast pola `method`), co naprawia blad testu polaczenia (`brak JSON`).
|
||||
- Dodano wspolny model mapowania form dostawy dla wielu providerow:
|
||||
- nowa tabela `carrier_delivery_method_mappings` (migracja `20260308_000044_create_carrier_delivery_method_mappings_table.sql`),
|
||||
- nowy repo `CarrierDeliveryMethodMappingRepository`,
|
||||
- backfill danych z tabel legacy (`allegro_delivery_method_mappings`, `shoppro_delivery_method_mappings`),
|
||||
- zakladki `Formy dostawy` w Allegro i shopPRO obsluguja mapowanie na `allegro_wza` i `apaczka`.
|
||||
- Dodano migracje `20260308_000045_extend_apaczka_credentials.sql`:
|
||||
- rozszerzenie `apaczka_integration_settings` o `app_id` i `app_secret_encrypted`,
|
||||
- migracja danych legacy sekretu z `api_key_encrypted`.
|
||||
- Dodano zakladke `Formy dostawy` dla integracji `shopPRO` (analogicznie do Allegro):
|
||||
- nowy endpoint `POST /settings/integrations/shoppro/delivery/save`,
|
||||
- mapowanie per instancja: forma dostawy shopPRO -> usluga dostawy Allegro/InPost WZA,
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
CREATE TABLE IF NOT EXISTS carrier_delivery_method_mappings (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
source_system VARCHAR(32) NOT NULL,
|
||||
source_integration_id INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
order_delivery_method VARCHAR(200) NOT NULL,
|
||||
provider VARCHAR(50) NOT NULL,
|
||||
provider_service_id VARCHAR(128) NOT NULL,
|
||||
provider_account_id VARCHAR(128) NULL,
|
||||
provider_carrier_id VARCHAR(128) NULL,
|
||||
provider_service_name VARCHAR(255) NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY carrier_dm_mapping_unique (source_system, source_integration_id, order_delivery_method),
|
||||
KEY carrier_dm_mapping_provider_idx (provider),
|
||||
KEY carrier_dm_mapping_source_idx (source_system, source_integration_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
INSERT INTO carrier_delivery_method_mappings (
|
||||
source_system,
|
||||
source_integration_id,
|
||||
order_delivery_method,
|
||||
provider,
|
||||
provider_service_id,
|
||||
provider_account_id,
|
||||
provider_carrier_id,
|
||||
provider_service_name,
|
||||
created_at,
|
||||
updated_at
|
||||
)
|
||||
SELECT
|
||||
'allegro' AS source_system,
|
||||
0 AS source_integration_id,
|
||||
m.order_delivery_method,
|
||||
'allegro_wza' AS provider,
|
||||
m.allegro_delivery_method_id AS provider_service_id,
|
||||
NULLIF(m.allegro_credentials_id, '') AS provider_account_id,
|
||||
NULLIF(m.allegro_carrier_id, '') AS provider_carrier_id,
|
||||
NULLIF(m.allegro_service_name, '') AS provider_service_name,
|
||||
NOW(),
|
||||
NOW()
|
||||
FROM allegro_delivery_method_mappings m
|
||||
LEFT JOIN carrier_delivery_method_mappings n
|
||||
ON n.source_system = 'allegro'
|
||||
AND n.source_integration_id = 0
|
||||
AND n.order_delivery_method = m.order_delivery_method
|
||||
WHERE n.id IS NULL
|
||||
AND m.order_delivery_method IS NOT NULL
|
||||
AND m.order_delivery_method <> ''
|
||||
AND m.allegro_delivery_method_id IS NOT NULL
|
||||
AND m.allegro_delivery_method_id <> '';
|
||||
|
||||
INSERT INTO carrier_delivery_method_mappings (
|
||||
source_system,
|
||||
source_integration_id,
|
||||
order_delivery_method,
|
||||
provider,
|
||||
provider_service_id,
|
||||
provider_account_id,
|
||||
provider_carrier_id,
|
||||
provider_service_name,
|
||||
created_at,
|
||||
updated_at
|
||||
)
|
||||
SELECT
|
||||
'shoppro' AS source_system,
|
||||
m.integration_id AS source_integration_id,
|
||||
m.order_delivery_method,
|
||||
'allegro_wza' AS provider,
|
||||
m.allegro_delivery_method_id AS provider_service_id,
|
||||
NULLIF(m.allegro_credentials_id, '') AS provider_account_id,
|
||||
NULLIF(m.allegro_carrier_id, '') AS provider_carrier_id,
|
||||
NULLIF(m.allegro_service_name, '') AS provider_service_name,
|
||||
NOW(),
|
||||
NOW()
|
||||
FROM shoppro_delivery_method_mappings m
|
||||
LEFT JOIN carrier_delivery_method_mappings n
|
||||
ON n.source_system = 'shoppro'
|
||||
AND n.source_integration_id = m.integration_id
|
||||
AND n.order_delivery_method = m.order_delivery_method
|
||||
WHERE n.id IS NULL
|
||||
AND m.order_delivery_method IS NOT NULL
|
||||
AND m.order_delivery_method <> ''
|
||||
AND m.allegro_delivery_method_id IS NOT NULL
|
||||
AND m.allegro_delivery_method_id <> '';
|
||||
@@ -0,0 +1,39 @@
|
||||
SET @sql = (
|
||||
SELECT IF(
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'apaczka_integration_settings'
|
||||
AND column_name = 'app_id'
|
||||
),
|
||||
'SELECT 1',
|
||||
'ALTER TABLE apaczka_integration_settings ADD COLUMN app_id VARCHAR(128) NULL AFTER integration_id'
|
||||
)
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @sql = (
|
||||
SELECT IF(
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'apaczka_integration_settings'
|
||||
AND column_name = 'app_secret_encrypted'
|
||||
),
|
||||
'SELECT 1',
|
||||
'ALTER TABLE apaczka_integration_settings ADD COLUMN app_secret_encrypted TEXT NULL AFTER app_id'
|
||||
)
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
UPDATE apaczka_integration_settings
|
||||
SET app_secret_encrypted = api_key_encrypted
|
||||
WHERE (app_secret_encrypted IS NULL OR app_secret_encrypted = '')
|
||||
AND api_key_encrypted IS NOT NULL
|
||||
AND api_key_encrypted <> '';
|
||||
@@ -0,0 +1,3 @@
|
||||
ALTER TABLE company_settings
|
||||
ADD COLUMN sender_contact_person VARCHAR(200) NULL AFTER person_name;
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -618,21 +618,32 @@ return [
|
||||
'title' => 'Konfiguracja API',
|
||||
],
|
||||
'fields' => [
|
||||
'api_key' => 'Klucz API',
|
||||
'app_id' => 'App ID',
|
||||
'app_secret' => 'App Secret',
|
||||
],
|
||||
'api_key' => [
|
||||
'saved' => 'Klucz API jest zapisany. Pozostaw pole puste, aby nie zmieniac.',
|
||||
'missing' => 'Brak zapisanego klucza API.',
|
||||
'app_secret' => [
|
||||
'saved' => 'App Secret jest zapisany. Pozostaw pole puste, aby nie zmieniac.',
|
||||
'missing' => 'Brak zapisanego App Secret.',
|
||||
],
|
||||
'status' => [
|
||||
'app_id' => 'App ID',
|
||||
'app_secret' => 'App Secret',
|
||||
'saved' => 'zapisany',
|
||||
'missing' => 'brak',
|
||||
'updated_at' => 'Ostatnia aktualizacja',
|
||||
],
|
||||
'actions' => [
|
||||
'save' => 'Zapisz ustawienia Apaczka',
|
||||
'test' => 'Test polaczenia',
|
||||
],
|
||||
'validation' => [
|
||||
'api_key_required' => 'Podaj klucz API Apaczka.',
|
||||
'app_id_required' => 'Podaj App ID Apaczka.',
|
||||
],
|
||||
'flash' => [
|
||||
'saved' => 'Ustawienia Apaczka zostaly zapisane.',
|
||||
'save_failed' => 'Nie udalo sie zapisac ustawien Apaczka.',
|
||||
'test_success' => 'Polaczenie z Apaczka dziala. Dostepne uslugi: :count.',
|
||||
'test_failed' => 'Nie udalo sie polaczyc z API Apaczka.',
|
||||
],
|
||||
],
|
||||
'inpost' => [
|
||||
@@ -902,7 +913,7 @@ return [
|
||||
'fields' => [
|
||||
'order_method' => 'Forma dostawy shopPRO',
|
||||
'carrier' => 'Przewoznik',
|
||||
'allegro_service' => 'Usluga dostawy Allegro',
|
||||
'allegro_service' => 'Usluga dostawy',
|
||||
'no_mapping' => 'brak mapowania',
|
||||
'search_placeholder' => 'Szukaj uslugi Allegro...',
|
||||
'select_carrier_first' => 'Najpierw wybierz przewoznika.',
|
||||
@@ -1089,6 +1100,7 @@ return [
|
||||
'fields' => [
|
||||
'company_name' => 'Nazwa firmy',
|
||||
'person_name' => 'Imie i nazwisko',
|
||||
'sender_contact_person' => 'Osoba kontaktowa nadawcy',
|
||||
'street' => 'Ulica',
|
||||
'postal_code' => 'Kod pocztowy',
|
||||
'city' => 'Miasto',
|
||||
|
||||
@@ -303,6 +303,21 @@ details[open] > .sidebar__group-toggle .sidebar__toggle-arrow {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
h2.section-title,
|
||||
h3.section-title,
|
||||
h4.section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-weight: 400;
|
||||
padding: 5px 10px;
|
||||
border-left: 3px solid var(--c-primary);
|
||||
border-radius: 7px;
|
||||
background: linear-gradient(180deg, #f4f8ff 0%, #edf3ff 100%);
|
||||
color: #1e3a8a;
|
||||
box-shadow: inset 0 0 0 1px #dbe7fb;
|
||||
}
|
||||
|
||||
.mt-0 {
|
||||
margin-top: 0;
|
||||
}
|
||||
@@ -2043,6 +2058,21 @@ details[open] > .sidebar__group-toggle .sidebar__toggle-arrow {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.dm-carrier-select {
|
||||
min-width: 140px;
|
||||
}
|
||||
|
||||
.dm-service-wrap {
|
||||
min-width: 200px;
|
||||
|
||||
.dm-inpost-panel,
|
||||
.dm-apaczka-panel {
|
||||
.form-control {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.integration-settings-group {
|
||||
grid-column: 1 / -1;
|
||||
border: 1px solid var(--c-border);
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
var POPUP_GAP = 12;
|
||||
|
||||
document.addEventListener('mouseenter', function (e) {
|
||||
if (!e.target || !e.target.closest) return;
|
||||
var wrap = e.target.closest('.orders-image-hover-wrap');
|
||||
if (!wrap) return;
|
||||
var popup = wrap.querySelector('.orders-image-hover-popup');
|
||||
|
||||
@@ -216,6 +216,16 @@ foreach ($addressesList as $address) {
|
||||
<?php endif; ?>
|
||||
<?php else: ?>
|
||||
<div><?= $e((string) ($addr['name'] ?? '')) ?></div>
|
||||
<?php
|
||||
$invoiceCompanyName = $addrType === 'invoice' ? trim((string) ($addr['company_name'] ?? '')) : '';
|
||||
$invoiceName = $addrType === 'invoice' ? trim((string) ($addr['name'] ?? '')) : '';
|
||||
?>
|
||||
<?php if ($addrType === 'invoice' && $invoiceCompanyName !== '' && $invoiceCompanyName !== $invoiceName): ?>
|
||||
<div><?= $e((string) $addr['company_name']) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if ($addrType === 'invoice' && !empty($addr['company_tax_number'])): ?>
|
||||
<div>NIP: <?= $e((string) $addr['company_tax_number']) ?></div>
|
||||
<?php endif; ?>
|
||||
<div><?= $e((string) (($addr['street_name'] ?? '') . ' ' . ($addr['street_number'] ?? ''))) ?></div>
|
||||
<div><?= $e((string) (($addr['zip_code'] ?? '') . ' ' . ($addr['city'] ?? ''))) ?></div>
|
||||
<div><?= $e((string) ($addr['country'] ?? '')) ?></div>
|
||||
|
||||
@@ -292,6 +292,7 @@ $orderproStatuses = is_array($orderproStatuses ?? null) ? $orderproStatuses : []
|
||||
$dmMappings = is_array($deliveryMappings ?? null) ? $deliveryMappings : [];
|
||||
$dmOrderMethods = is_array($orderDeliveryMethods ?? null) ? $orderDeliveryMethods : [];
|
||||
$dmAllegroServices = is_array($allegroDeliveryServices ?? null) ? $allegroDeliveryServices : [];
|
||||
$dmApaczkaServices = is_array($apaczkaDeliveryServices ?? null) ? $apaczkaDeliveryServices : [];
|
||||
$dmInpostServices = is_array($inpostDeliveryServices ?? null) ? $inpostDeliveryServices : [];
|
||||
$dmServicesError = (string) ($allegroDeliveryServicesError ?? '');
|
||||
$dmMappingsByMethod = [];
|
||||
@@ -326,9 +327,18 @@ $orderproStatuses = is_array($orderproStatuses ?? null) ? $orderproStatuses : []
|
||||
<?php foreach ($dmOrderMethods as $rowIdx => $orderMethod): ?>
|
||||
<?php
|
||||
$currentMapping = $dmMappingsByMethod[$orderMethod] ?? null;
|
||||
$currentCarrier = $currentMapping !== null ? trim((string) ($currentMapping['carrier'] ?? 'allegro')) : '';
|
||||
$currentAllegroId = $currentMapping !== null ? trim((string) ($currentMapping['allegro_delivery_method_id'] ?? '')) : '';
|
||||
$currentServiceName = $currentMapping !== null ? trim((string) ($currentMapping['allegro_service_name'] ?? '')) : '';
|
||||
$currentProvider = $currentMapping !== null ? trim((string) ($currentMapping['provider'] ?? 'allegro_wza')) : '';
|
||||
$currentMethodId = $currentMapping !== null ? trim((string) ($currentMapping['provider_service_id'] ?? '')) : '';
|
||||
$currentServiceName = $currentMapping !== null ? trim((string) ($currentMapping['provider_service_name'] ?? '')) : '';
|
||||
$currentProviderCarrierId = $currentMapping !== null ? trim((string) ($currentMapping['provider_carrier_id'] ?? '')) : '';
|
||||
$currentCarrier = '';
|
||||
if ($currentProvider === 'apaczka') {
|
||||
$currentCarrier = 'apaczka';
|
||||
} elseif (stripos($currentProviderCarrierId, 'inpost') !== false) {
|
||||
$currentCarrier = 'inpost';
|
||||
} elseif ($currentMethodId !== '') {
|
||||
$currentCarrier = 'allegro';
|
||||
}
|
||||
?>
|
||||
<tr data-dm-row="<?= $rowIdx ?>">
|
||||
<td>
|
||||
@@ -338,20 +348,22 @@ $orderproStatuses = is_array($orderproStatuses ?? null) ? $orderproStatuses : []
|
||||
<td>
|
||||
<select class="form-control dm-carrier-select" name="carrier[]" data-row="<?= $rowIdx ?>">
|
||||
<option value="">-- <?= $e($t('settings.allegro.delivery.fields.no_mapping')) ?> --</option>
|
||||
<option value="allegro"<?= $currentCarrier === 'allegro' && $currentAllegroId !== '' ? ' selected' : '' ?>>Allegro</option>
|
||||
<option value="allegro"<?= $currentCarrier === 'allegro' && $currentMethodId !== '' ? ' selected' : '' ?>>Allegro</option>
|
||||
<option value="inpost"<?= $currentCarrier === 'inpost' ? ' selected' : '' ?>>InPost</option>
|
||||
<option value="apaczka"<?= $currentCarrier === 'apaczka' ? ' selected' : '' ?>>Apaczka</option>
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<div class="dm-service-wrap" data-row="<?= $rowIdx ?>">
|
||||
<input type="hidden" name="allegro_delivery_method_id[]" class="dm-hidden-method-id" value="<?= $e($currentAllegroId) ?>">
|
||||
<input type="hidden" name="allegro_credentials_id[]" class="dm-hidden-credentials-id" value="<?= $e(trim((string) ($currentMapping['allegro_credentials_id'] ?? ''))) ?>">
|
||||
<input type="hidden" name="allegro_carrier_id[]" class="dm-hidden-carrier-id" value="<?= $e(trim((string) ($currentMapping['allegro_carrier_id'] ?? ''))) ?>">
|
||||
<input type="hidden" name="allegro_delivery_method_id[]" class="dm-hidden-method-id" value="<?= $e($currentProvider !== 'apaczka' ? $currentMethodId : '') ?>">
|
||||
<input type="hidden" name="apaczka_delivery_method_id[]" class="dm-hidden-apaczka-method-id" value="<?= $e($currentProvider === 'apaczka' ? $currentMethodId : '') ?>">
|
||||
<input type="hidden" name="allegro_credentials_id[]" class="dm-hidden-credentials-id" value="<?= $e(trim((string) ($currentMapping['provider_account_id'] ?? ''))) ?>">
|
||||
<input type="hidden" name="allegro_carrier_id[]" class="dm-hidden-carrier-id" value="<?= $e($currentProviderCarrierId) ?>">
|
||||
<input type="hidden" name="allegro_service_name[]" class="dm-hidden-service-name" value="<?= $e($currentServiceName) ?>">
|
||||
|
||||
<?php // Allegro searchable select ?>
|
||||
<div class="dm-allegro-panel dm-searchable-select" data-current-id="<?= $e($currentCarrier === 'allegro' ? $currentAllegroId : '') ?>" data-current-name="<?= $e($currentCarrier === 'allegro' ? $currentServiceName : '') ?>" style="<?= $currentCarrier !== 'allegro' || $currentAllegroId === '' && $currentCarrier === '' ? 'display:none' : '' ?>">
|
||||
<input type="text" class="form-control dm-search-input" placeholder="<?= $e($t('settings.allegro.delivery.fields.search_placeholder')) ?>" value="<?= $e($currentCarrier === 'allegro' ? $currentServiceName : '') ?>" autocomplete="off">
|
||||
<div class="dm-allegro-panel dm-searchable-select" data-current-id="<?= $e(($currentCarrier === 'allegro' || $currentCarrier === 'inpost') ? $currentMethodId : '') ?>" data-current-name="<?= $e(($currentCarrier === 'allegro' || $currentCarrier === 'inpost') ? $currentServiceName : '') ?>" style="<?= ($currentCarrier !== 'allegro' && $currentCarrier !== 'inpost') ? 'display:none' : '' ?>">
|
||||
<input type="text" class="form-control dm-search-input" placeholder="<?= $e($t('settings.allegro.delivery.fields.search_placeholder')) ?>" value="<?= $e(($currentCarrier === 'allegro' || $currentCarrier === 'inpost') ? $currentServiceName : '') ?>" autocomplete="off">
|
||||
<div class="searchable-select__dropdown dm-dropdown">
|
||||
<div class="searchable-select__option dm-option-clear" data-value="" data-label="" data-credentials-id="" data-carrier-id="">
|
||||
<em class="muted">-- <?= $e($t('settings.allegro.delivery.fields.no_mapping')) ?> --</em>
|
||||
@@ -376,6 +388,29 @@ $orderproStatuses = is_array($orderproStatuses ?? null) ? $orderproStatuses : []
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dm-apaczka-panel" style="<?= $currentCarrier !== 'apaczka' ? 'display:none' : '' ?>">
|
||||
<?php if ($dmApaczkaServices === []): ?>
|
||||
<div class="muted">Brak uslug Apaczka (sprawdz konfiguracje App ID/App Secret).</div>
|
||||
<?php else: ?>
|
||||
<select class="form-control dm-apaczka-select">
|
||||
<option value="">-- <?= $e($t('settings.allegro.delivery.fields.no_mapping')) ?> --</option>
|
||||
<?php foreach ($dmApaczkaServices as $aSvc): ?>
|
||||
<?php
|
||||
$aSvcId = trim((string) ($aSvc['id'] ?? ''));
|
||||
$aSvcName = trim((string) ($aSvc['name'] ?? ''));
|
||||
$aSvcCarrier = trim((string) ($aSvc['carrier_code'] ?? ''));
|
||||
?>
|
||||
<option
|
||||
value="<?= $e($aSvcId) ?>"
|
||||
data-carrier-id="<?= $e($aSvcCarrier) ?>"
|
||||
<?= $currentCarrier === 'apaczka' && $currentMethodId === $aSvcId ? 'selected' : '' ?>>
|
||||
<?= $e($aSvcName !== '' ? $aSvcName : ('ID ' . $aSvcId)) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php // InPost simple select ?>
|
||||
<div class="dm-inpost-panel" style="<?= $currentCarrier !== 'inpost' ? 'display:none' : '' ?>">
|
||||
<?php if ($dmInpostServices === []): ?>
|
||||
@@ -395,7 +430,7 @@ $orderproStatuses = is_array($orderproStatuses ?? null) ? $orderproStatuses : []
|
||||
value="<?= $e($inSvcMethodId) ?>"
|
||||
data-credentials-id="<?= $e($inSvcCredentialsId) ?>"
|
||||
data-carrier-id="<?= $e($inSvcCarrierId) ?>"
|
||||
<?= $currentCarrier === 'inpost' && $currentAllegroId === $inSvcMethodId ? 'selected' : '' ?>>
|
||||
<?= $currentCarrier === 'inpost' && $currentMethodId === $inSvcMethodId ? 'selected' : '' ?>>
|
||||
<?= $e($inSvcName) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
@@ -404,7 +439,7 @@ $orderproStatuses = is_array($orderproStatuses ?? null) ? $orderproStatuses : []
|
||||
</div>
|
||||
|
||||
<?php // Empty state ?>
|
||||
<div class="dm-empty-panel muted" style="<?= $currentCarrier !== '' ? 'display:none' : ($currentAllegroId !== '' ? 'display:none' : '') ?>">
|
||||
<div class="dm-empty-panel muted" style="<?= $currentCarrier !== '' ? 'display:none' : ($currentMethodId !== '' ? 'display:none' : '') ?>">
|
||||
<?= $e($t('settings.allegro.delivery.fields.select_carrier_first')) ?>
|
||||
</div>
|
||||
</div>
|
||||
@@ -474,6 +509,60 @@ $orderproStatuses = is_array($orderproStatuses ?? null) ? $orderproStatuses : []
|
||||
})();
|
||||
|
||||
(function () {
|
||||
function attachSelectFilter(selectEl) {
|
||||
if (!selectEl || selectEl.dataset.enhanced === '1') {
|
||||
return;
|
||||
}
|
||||
selectEl.dataset.enhanced = '1';
|
||||
|
||||
var parent = selectEl.parentNode;
|
||||
if (!parent) {
|
||||
return;
|
||||
}
|
||||
|
||||
var filterInput = document.createElement('input');
|
||||
filterInput.type = 'text';
|
||||
filterInput.className = 'form-control';
|
||||
filterInput.placeholder = 'Szukaj...';
|
||||
filterInput.autocomplete = 'off';
|
||||
filterInput.style.marginBottom = '8px';
|
||||
parent.insertBefore(filterInput, selectEl);
|
||||
|
||||
function focusFirstMatch(term) {
|
||||
var query = term.toLowerCase().trim();
|
||||
if (query === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
var matchedIndex = -1;
|
||||
Array.prototype.forEach.call(selectEl.options, function (opt, idx) {
|
||||
if (matchedIndex >= 0) {
|
||||
return;
|
||||
}
|
||||
if (opt.textContent.toLowerCase().indexOf(query) !== -1) {
|
||||
matchedIndex = idx;
|
||||
}
|
||||
});
|
||||
|
||||
if (matchedIndex >= 0 && selectEl.selectedIndex !== matchedIndex) {
|
||||
selectEl.selectedIndex = matchedIndex;
|
||||
selectEl.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
}
|
||||
}
|
||||
|
||||
filterInput.addEventListener('input', function () {
|
||||
focusFirstMatch(filterInput.value);
|
||||
});
|
||||
|
||||
selectEl._syncTrigger = function () {
|
||||
// Keep select untouched; filter only suggests matching option.
|
||||
};
|
||||
}
|
||||
|
||||
document.querySelectorAll('.dm-inpost-select, .dm-apaczka-select').forEach(function (selectEl) {
|
||||
attachSelectFilter(selectEl);
|
||||
});
|
||||
|
||||
// Carrier switching logic
|
||||
document.querySelectorAll('.dm-carrier-select').forEach(function (carrierSelect) {
|
||||
var rowIdx = carrierSelect.getAttribute('data-row');
|
||||
@@ -482,8 +571,10 @@ $orderproStatuses = is_array($orderproStatuses ?? null) ? $orderproStatuses : []
|
||||
|
||||
var allegroPanel = serviceWrap.querySelector('.dm-allegro-panel');
|
||||
var inpostPanel = serviceWrap.querySelector('.dm-inpost-panel');
|
||||
var apaczkaPanel = serviceWrap.querySelector('.dm-apaczka-panel');
|
||||
var emptyPanel = serviceWrap.querySelector('.dm-empty-panel');
|
||||
var hiddenMethodId = serviceWrap.querySelector('.dm-hidden-method-id');
|
||||
var hiddenApaczkaMethodId = serviceWrap.querySelector('.dm-hidden-apaczka-method-id');
|
||||
var hiddenCredentialsId = serviceWrap.querySelector('.dm-hidden-credentials-id');
|
||||
var hiddenCarrierId = serviceWrap.querySelector('.dm-hidden-carrier-id');
|
||||
var hiddenServiceName = serviceWrap.querySelector('.dm-hidden-service-name');
|
||||
@@ -491,23 +582,42 @@ $orderproStatuses = is_array($orderproStatuses ?? null) ? $orderproStatuses : []
|
||||
function showPanel(carrier) {
|
||||
if (allegroPanel) allegroPanel.style.display = carrier === 'allegro' ? '' : 'none';
|
||||
if (inpostPanel) inpostPanel.style.display = carrier === 'inpost' ? '' : 'none';
|
||||
if (apaczkaPanel) apaczkaPanel.style.display = carrier === 'apaczka' ? '' : 'none';
|
||||
if (emptyPanel) emptyPanel.style.display = carrier === '' ? '' : 'none';
|
||||
}
|
||||
|
||||
showPanel(carrierSelect.value);
|
||||
|
||||
carrierSelect.addEventListener('change', function () {
|
||||
var carrier = carrierSelect.value;
|
||||
showPanel(carrier);
|
||||
// Clear hidden values when switching carrier
|
||||
if (hiddenMethodId) hiddenMethodId.value = '';
|
||||
if (hiddenApaczkaMethodId) hiddenApaczkaMethodId.value = '';
|
||||
if (hiddenCredentialsId) hiddenCredentialsId.value = '';
|
||||
if (hiddenCarrierId) hiddenCarrierId.value = '';
|
||||
if (hiddenServiceName) hiddenServiceName.value = '';
|
||||
// Reset Allegro search input
|
||||
var allegroInput = allegroPanel ? allegroPanel.querySelector('.dm-search-input') : null;
|
||||
if (allegroInput) allegroInput.value = '';
|
||||
if (allegroInput) {
|
||||
allegroInput.value = '';
|
||||
allegroInput.blur();
|
||||
}
|
||||
var allegroDropdown = allegroPanel ? allegroPanel.querySelector('.dm-dropdown') : null;
|
||||
if (allegroDropdown) {
|
||||
allegroDropdown.classList.remove('is-open');
|
||||
}
|
||||
// Reset InPost select
|
||||
var inpostSelect = inpostPanel ? inpostPanel.querySelector('.dm-inpost-select') : null;
|
||||
if (inpostSelect) inpostSelect.value = '';
|
||||
if (inpostSelect) {
|
||||
inpostSelect.value = '';
|
||||
if (inpostSelect._syncTrigger) inpostSelect._syncTrigger();
|
||||
}
|
||||
var apaczkaSelect = apaczkaPanel ? apaczkaPanel.querySelector('.dm-apaczka-select') : null;
|
||||
if (apaczkaSelect) {
|
||||
apaczkaSelect.value = '';
|
||||
if (apaczkaSelect._syncTrigger) apaczkaSelect._syncTrigger();
|
||||
}
|
||||
});
|
||||
|
||||
// InPost select change -> update hidden fields
|
||||
@@ -516,10 +626,29 @@ $orderproStatuses = is_array($orderproStatuses ?? null) ? $orderproStatuses : []
|
||||
inpostSelect.addEventListener('change', function () {
|
||||
var opt = inpostSelect.options[inpostSelect.selectedIndex];
|
||||
if (hiddenMethodId) hiddenMethodId.value = inpostSelect.value;
|
||||
if (hiddenApaczkaMethodId) hiddenApaczkaMethodId.value = '';
|
||||
if (hiddenCredentialsId) hiddenCredentialsId.value = opt ? (opt.getAttribute('data-credentials-id') || '') : '';
|
||||
if (hiddenCarrierId) hiddenCarrierId.value = opt ? (opt.getAttribute('data-carrier-id') || '') : '';
|
||||
if (hiddenServiceName) hiddenServiceName.value = opt ? opt.textContent.trim() : '';
|
||||
});
|
||||
if (inpostSelect.value !== '') {
|
||||
inpostSelect.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
}
|
||||
}
|
||||
|
||||
var apaczkaSelect = apaczkaPanel ? apaczkaPanel.querySelector('.dm-apaczka-select') : null;
|
||||
if (apaczkaSelect) {
|
||||
apaczkaSelect.addEventListener('change', function () {
|
||||
var opt = apaczkaSelect.options[apaczkaSelect.selectedIndex];
|
||||
if (hiddenMethodId) hiddenMethodId.value = '';
|
||||
if (hiddenApaczkaMethodId) hiddenApaczkaMethodId.value = apaczkaSelect.value;
|
||||
if (hiddenCredentialsId) hiddenCredentialsId.value = '';
|
||||
if (hiddenCarrierId) hiddenCarrierId.value = opt ? (opt.getAttribute('data-carrier-id') || '') : '';
|
||||
if (hiddenServiceName) hiddenServiceName.value = opt ? opt.textContent.trim() : '';
|
||||
});
|
||||
if (apaczkaSelect.value !== '') {
|
||||
apaczkaSelect.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -531,6 +660,7 @@ $orderproStatuses = is_array($orderproStatuses ?? null) ? $orderproStatuses : []
|
||||
if (!searchInput || !dropdown || !serviceWrap) return;
|
||||
|
||||
var hiddenMethodId = serviceWrap.querySelector('.dm-hidden-method-id');
|
||||
var hiddenApaczkaMethodId = serviceWrap.querySelector('.dm-hidden-apaczka-method-id');
|
||||
var hiddenCredentialsId = serviceWrap.querySelector('.dm-hidden-credentials-id');
|
||||
var hiddenCarrierId = serviceWrap.querySelector('.dm-hidden-carrier-id');
|
||||
var hiddenServiceName = serviceWrap.querySelector('.dm-hidden-service-name');
|
||||
@@ -540,6 +670,7 @@ $orderproStatuses = is_array($orderproStatuses ?? null) ? $orderproStatuses : []
|
||||
|
||||
function selectOption(opt) {
|
||||
hiddenMethodId.value = opt.getAttribute('data-value') || '';
|
||||
if (hiddenApaczkaMethodId) hiddenApaczkaMethodId.value = '';
|
||||
hiddenCredentialsId.value = opt.getAttribute('data-credentials-id') || '';
|
||||
hiddenCarrierId.value = opt.getAttribute('data-carrier-id') || '';
|
||||
hiddenServiceName.value = opt.getAttribute('data-label') || '';
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
<?php
|
||||
$integration = is_array($settings ?? null) ? $settings : [];
|
||||
$hasApiKey = (bool) ($integration['has_api_key'] ?? false);
|
||||
$appId = trim((string) ($integration['app_id'] ?? ''));
|
||||
$hasAppSecret = (bool) ($integration['has_app_secret'] ?? false);
|
||||
$hasAppId = (bool) ($integration['has_app_id'] ?? false);
|
||||
$updatedAt = trim((string) ($integration['updated_at'] ?? ''));
|
||||
?>
|
||||
|
||||
<section class="card">
|
||||
@@ -18,17 +21,38 @@ $hasApiKey = (bool) ($integration['has_api_key'] ?? false);
|
||||
|
||||
<section class="card mt-16">
|
||||
<h3 class="section-title"><?= $e($t('settings.apaczka.config.title')) ?></h3>
|
||||
<div class="muted mt-12">
|
||||
<?= $e($t('settings.apaczka.status.app_id')) ?>:
|
||||
<strong><?= $e($hasAppId ? $t('settings.apaczka.status.saved') : $t('settings.apaczka.status.missing')) ?></strong>
|
||||
|
|
||||
<?= $e($t('settings.apaczka.status.app_secret')) ?>:
|
||||
<strong><?= $e($hasAppSecret ? $t('settings.apaczka.status.saved') : $t('settings.apaczka.status.missing')) ?></strong>
|
||||
<?php if ($updatedAt !== ''): ?>
|
||||
| <?= $e($t('settings.apaczka.status.updated_at')) ?>: <strong><?= $e($updatedAt) ?></strong>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<form class="statuses-form mt-16" action="/settings/integrations/apaczka/save" method="post" novalidate>
|
||||
<input type="hidden" name="_token" value="<?= $e($csrfToken ?? '') ?>">
|
||||
|
||||
<label class="form-field">
|
||||
<span class="field-label"><?= $e($t('settings.apaczka.fields.api_key')) ?></span>
|
||||
<input class="form-control" type="password" name="api_key" autocomplete="new-password">
|
||||
<span class="muted"><?= $e($hasApiKey ? $t('settings.apaczka.api_key.saved') : $t('settings.apaczka.api_key.missing')) ?></span>
|
||||
<span class="field-label"><?= $e($t('settings.apaczka.fields.app_id')) ?></span>
|
||||
<input class="form-control" type="text" name="app_id" maxlength="128" value="<?= $e($appId) ?>" required>
|
||||
</label>
|
||||
|
||||
<label class="form-field">
|
||||
<span class="field-label"><?= $e($t('settings.apaczka.fields.app_secret')) ?></span>
|
||||
<input class="form-control" type="password" name="app_secret" autocomplete="new-password">
|
||||
<span class="muted"><?= $e($hasAppSecret ? $t('settings.apaczka.app_secret.saved') : $t('settings.apaczka.app_secret.missing')) ?></span>
|
||||
</label>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn--primary"><?= $e($t('settings.apaczka.actions.save')) ?></button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form class="mt-12" action="/settings/integrations/apaczka/test" method="post">
|
||||
<input type="hidden" name="_token" value="<?= $e($csrfToken ?? '') ?>">
|
||||
<button type="submit" class="btn btn--secondary"><?= $e($t('settings.apaczka.actions.test')) ?></button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
@@ -31,6 +31,11 @@ $s = is_array($settings ?? null) ? $settings : [];
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label class="form-field mt-0">
|
||||
<span class="field-label"><?= $e($t('settings.company.fields.sender_contact_person')) ?></span>
|
||||
<input class="form-control" type="text" name="sender_contact_person" maxlength="200" value="<?= $e((string) ($s['sender_contact_person'] ?? '')) ?>">
|
||||
</label>
|
||||
|
||||
<label class="form-field">
|
||||
<span class="field-label"><?= $e($t('settings.company.fields.street')) ?></span>
|
||||
<input class="form-control" type="text" name="street" maxlength="200" value="<?= $e((string) ($s['street'] ?? '')) ?>">
|
||||
|
||||
@@ -14,6 +14,7 @@ $selectedPaymentSyncCodes = is_array($formValues['payment_sync_status_codes'] ??
|
||||
$dmMappings = is_array($deliveryMappings ?? null) ? $deliveryMappings : [];
|
||||
$dmOrderMethods = is_array($orderDeliveryMethods ?? null) ? $orderDeliveryMethods : [];
|
||||
$dmAllegroServices = is_array($allegroDeliveryServices ?? null) ? $allegroDeliveryServices : [];
|
||||
$dmApaczkaServices = is_array($apaczkaDeliveryServices ?? null) ? $apaczkaDeliveryServices : [];
|
||||
$dmInpostServices = is_array($inpostDeliveryServices ?? null) ? $inpostDeliveryServices : [];
|
||||
$dmServicesError = (string) ($allegroDeliveryServicesError ?? '');
|
||||
$dmMappingsByMethod = [];
|
||||
@@ -404,9 +405,18 @@ foreach ($dmMappings as $dm) {
|
||||
<?php
|
||||
$methodName = trim((string) $orderMethod);
|
||||
$currentMapping = $dmMappingsByMethod[$methodName] ?? null;
|
||||
$currentCarrier = $currentMapping !== null ? trim((string) ($currentMapping['carrier'] ?? 'allegro')) : '';
|
||||
$currentAllegroId = $currentMapping !== null ? trim((string) ($currentMapping['allegro_delivery_method_id'] ?? '')) : '';
|
||||
$currentServiceName = $currentMapping !== null ? trim((string) ($currentMapping['allegro_service_name'] ?? '')) : '';
|
||||
$currentProvider = $currentMapping !== null ? trim((string) ($currentMapping['provider'] ?? 'allegro_wza')) : '';
|
||||
$currentMethodId = $currentMapping !== null ? trim((string) ($currentMapping['provider_service_id'] ?? '')) : '';
|
||||
$currentServiceName = $currentMapping !== null ? trim((string) ($currentMapping['provider_service_name'] ?? '')) : '';
|
||||
$currentProviderCarrierId = $currentMapping !== null ? trim((string) ($currentMapping['provider_carrier_id'] ?? '')) : '';
|
||||
$currentCarrier = '';
|
||||
if ($currentProvider === 'apaczka') {
|
||||
$currentCarrier = 'apaczka';
|
||||
} elseif (stripos($currentProviderCarrierId, 'inpost') !== false) {
|
||||
$currentCarrier = 'inpost';
|
||||
} elseif ($currentMethodId !== '') {
|
||||
$currentCarrier = 'allegro';
|
||||
}
|
||||
?>
|
||||
<tr data-dm-row="<?= $rowIdx ?>">
|
||||
<td>
|
||||
@@ -416,18 +426,20 @@ foreach ($dmMappings as $dm) {
|
||||
<td>
|
||||
<select class="form-control dm-carrier-select" name="carrier[]" data-row="<?= $rowIdx ?>">
|
||||
<option value="">-- <?= $e($t('settings.integrations.delivery.fields.no_mapping')) ?> --</option>
|
||||
<option value="allegro"<?= $currentCarrier === 'allegro' && $currentAllegroId !== '' ? ' selected' : '' ?>>Allegro</option>
|
||||
<option value="allegro"<?= $currentCarrier === 'allegro' && $currentMethodId !== '' ? ' selected' : '' ?>>Allegro</option>
|
||||
<option value="inpost"<?= $currentCarrier === 'inpost' ? ' selected' : '' ?>>InPost</option>
|
||||
<option value="apaczka"<?= $currentCarrier === 'apaczka' ? ' selected' : '' ?>>Apaczka</option>
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<div class="dm-service-wrap" data-row="<?= $rowIdx ?>">
|
||||
<input type="hidden" name="allegro_delivery_method_id[]" class="dm-hidden-method-id" value="<?= $e($currentAllegroId) ?>">
|
||||
<input type="hidden" name="allegro_credentials_id[]" class="dm-hidden-credentials-id" value="<?= $e(trim((string) ($currentMapping['allegro_credentials_id'] ?? ''))) ?>">
|
||||
<input type="hidden" name="allegro_carrier_id[]" class="dm-hidden-carrier-id" value="<?= $e(trim((string) ($currentMapping['allegro_carrier_id'] ?? ''))) ?>">
|
||||
<input type="hidden" name="allegro_delivery_method_id[]" class="dm-hidden-method-id" value="<?= $e($currentProvider !== 'apaczka' ? $currentMethodId : '') ?>">
|
||||
<input type="hidden" name="apaczka_delivery_method_id[]" class="dm-hidden-apaczka-method-id" value="<?= $e($currentProvider === 'apaczka' ? $currentMethodId : '') ?>">
|
||||
<input type="hidden" name="allegro_credentials_id[]" class="dm-hidden-credentials-id" value="<?= $e(trim((string) ($currentMapping['provider_account_id'] ?? ''))) ?>">
|
||||
<input type="hidden" name="allegro_carrier_id[]" class="dm-hidden-carrier-id" value="<?= $e($currentProviderCarrierId) ?>">
|
||||
<input type="hidden" name="allegro_service_name[]" class="dm-hidden-service-name" value="<?= $e($currentServiceName) ?>">
|
||||
|
||||
<div class="dm-allegro-panel dm-searchable-select" data-current-id="<?= $e($currentCarrier === 'allegro' ? $currentAllegroId : '') ?>" data-current-name="<?= $e($currentCarrier === 'allegro' ? $currentServiceName : '') ?>" style="<?= $currentCarrier !== 'allegro' || $currentAllegroId === '' && $currentCarrier === '' ? 'display:none' : '' ?>">
|
||||
<div class="dm-allegro-panel dm-searchable-select" data-current-id="<?= $e($currentCarrier === 'allegro' ? $currentMethodId : '') ?>" data-current-name="<?= $e($currentCarrier === 'allegro' ? $currentServiceName : '') ?>" style="<?= $currentCarrier !== 'allegro' ? 'display:none' : '' ?>">
|
||||
<input type="text" class="form-control dm-search-input" placeholder="<?= $e($t('settings.integrations.delivery.fields.search_placeholder')) ?>" value="<?= $e($currentCarrier === 'allegro' ? $currentServiceName : '') ?>" autocomplete="off">
|
||||
<div class="searchable-select__dropdown dm-dropdown">
|
||||
<div class="searchable-select__option dm-option-clear" data-value="" data-label="" data-credentials-id="" data-carrier-id="">
|
||||
@@ -457,12 +469,46 @@ foreach ($dmMappings as $dm) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dm-inpost-panel" style="<?= $currentCarrier !== 'inpost' ? 'display:none' : '' ?>">
|
||||
<div class="dm-apaczka-panel dm-searchable-select" data-provider="apaczka" data-current-id="<?= $e($currentCarrier === 'apaczka' ? $currentMethodId : '') ?>" data-current-name="<?= $e($currentCarrier === 'apaczka' ? $currentServiceName : '') ?>" style="<?= $currentCarrier !== 'apaczka' ? 'display:none' : '' ?>">
|
||||
<?php if ($dmApaczkaServices === []): ?>
|
||||
<div class="muted">Brak uslug Apaczka (sprawdz konfiguracje App ID/App Secret).</div>
|
||||
<?php else: ?>
|
||||
<input type="text" class="form-control dm-search-input" placeholder="<?= $e($t('settings.integrations.delivery.fields.search_placeholder')) ?>" value="<?= $e($currentCarrier === 'apaczka' ? $currentServiceName : '') ?>" autocomplete="off">
|
||||
<div class="searchable-select__dropdown dm-dropdown">
|
||||
<div class="searchable-select__option dm-option-clear" data-value="" data-label="" data-credentials-id="" data-carrier-id="">
|
||||
<em class="muted">-- <?= $e($t('settings.integrations.delivery.fields.no_mapping')) ?> --</em>
|
||||
</div>
|
||||
<?php foreach ($dmApaczkaServices as $aSvc): ?>
|
||||
<?php
|
||||
if (!is_array($aSvc)) {
|
||||
continue;
|
||||
}
|
||||
$aSvcId = trim((string) ($aSvc['service_id'] ?? $aSvc['id'] ?? ''));
|
||||
$aSvcName = trim((string) ($aSvc['name'] ?? ''));
|
||||
$aSvcCarrier = trim((string) ($aSvc['supplier'] ?? $aSvc['carrier_code'] ?? ''));
|
||||
$aSvcLabel = $aSvcName !== '' ? $aSvcName : ('ID ' . $aSvcId);
|
||||
?>
|
||||
<div class="searchable-select__option"
|
||||
data-value="<?= $e($aSvcId) ?>"
|
||||
data-label="<?= $e($aSvcLabel) ?>"
|
||||
data-credentials-id=""
|
||||
data-carrier-id="<?= $e($aSvcCarrier) ?>">
|
||||
<?= $e($aSvcLabel) ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="dm-inpost-panel dm-searchable-select" data-provider="inpost" data-current-id="<?= $e($currentCarrier === 'inpost' ? $currentMethodId : '') ?>" data-current-name="<?= $e($currentCarrier === 'inpost' ? $currentServiceName : '') ?>" style="<?= $currentCarrier !== 'inpost' ? 'display:none' : '' ?>">
|
||||
<?php if ($dmInpostServices === []): ?>
|
||||
<div class="muted"><?= $e($t('settings.integrations.delivery.no_inpost_services')) ?></div>
|
||||
<?php else: ?>
|
||||
<select class="form-control dm-inpost-select">
|
||||
<option value="">-- <?= $e($t('settings.integrations.delivery.fields.no_mapping')) ?> --</option>
|
||||
<input type="text" class="form-control dm-search-input" placeholder="<?= $e($t('settings.integrations.delivery.fields.search_placeholder')) ?>" value="<?= $e($currentCarrier === 'inpost' ? $currentServiceName : '') ?>" autocomplete="off">
|
||||
<div class="searchable-select__dropdown dm-dropdown">
|
||||
<div class="searchable-select__option dm-option-clear" data-value="" data-label="" data-credentials-id="" data-carrier-id="">
|
||||
<em class="muted">-- <?= $e($t('settings.integrations.delivery.fields.no_mapping')) ?> --</em>
|
||||
</div>
|
||||
<?php foreach ($dmInpostServices as $inSvc): ?>
|
||||
<?php
|
||||
if (!is_array($inSvc)) {
|
||||
@@ -474,19 +520,19 @@ foreach ($dmMappings as $dm) {
|
||||
$inSvcCarrierId = trim((string) ($inSvc['carrierId'] ?? ''));
|
||||
$inSvcName = trim((string) ($inSvc['name'] ?? ''));
|
||||
?>
|
||||
<option
|
||||
value="<?= $e($inSvcMethodId) ?>"
|
||||
data-credentials-id="<?= $e($inSvcCredentialsId) ?>"
|
||||
data-carrier-id="<?= $e($inSvcCarrierId) ?>"
|
||||
<?= $currentCarrier === 'inpost' && $currentAllegroId === $inSvcMethodId ? 'selected' : '' ?>>
|
||||
<div class="searchable-select__option"
|
||||
data-value="<?= $e($inSvcMethodId) ?>"
|
||||
data-label="<?= $e($inSvcName) ?>"
|
||||
data-credentials-id="<?= $e($inSvcCredentialsId) ?>"
|
||||
data-carrier-id="<?= $e($inSvcCarrierId) ?>">
|
||||
<?= $e($inSvcName) ?>
|
||||
</option>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="dm-empty-panel muted" style="<?= $currentCarrier !== '' ? 'display:none' : ($currentAllegroId !== '' ? 'display:none' : '') ?>">
|
||||
<div class="dm-empty-panel muted" style="<?= $currentCarrier !== '' ? 'display:none' : ($currentMethodId !== '' ? 'display:none' : '') ?>">
|
||||
<?= $e($t('settings.integrations.delivery.fields.select_carrier_first')) ?>
|
||||
</div>
|
||||
</div>
|
||||
@@ -573,8 +619,10 @@ foreach ($dmMappings as $dm) {
|
||||
|
||||
var allegroPanel = serviceWrap.querySelector('.dm-allegro-panel');
|
||||
var inpostPanel = serviceWrap.querySelector('.dm-inpost-panel');
|
||||
var apaczkaPanel = serviceWrap.querySelector('.dm-apaczka-panel');
|
||||
var emptyPanel = serviceWrap.querySelector('.dm-empty-panel');
|
||||
var hiddenMethodId = serviceWrap.querySelector('.dm-hidden-method-id');
|
||||
var hiddenApaczkaMethodId = serviceWrap.querySelector('.dm-hidden-apaczka-method-id');
|
||||
var hiddenCredentialsId = serviceWrap.querySelector('.dm-hidden-credentials-id');
|
||||
var hiddenCarrierId = serviceWrap.querySelector('.dm-hidden-carrier-id');
|
||||
var hiddenServiceName = serviceWrap.querySelector('.dm-hidden-service-name');
|
||||
@@ -582,32 +630,38 @@ foreach ($dmMappings as $dm) {
|
||||
function showPanel(carrier) {
|
||||
if (allegroPanel) allegroPanel.style.display = carrier === 'allegro' ? '' : 'none';
|
||||
if (inpostPanel) inpostPanel.style.display = carrier === 'inpost' ? '' : 'none';
|
||||
if (apaczkaPanel) apaczkaPanel.style.display = carrier === 'apaczka' ? '' : 'none';
|
||||
if (emptyPanel) emptyPanel.style.display = carrier === '' ? '' : 'none';
|
||||
}
|
||||
|
||||
showPanel(carrierSelect.value);
|
||||
|
||||
carrierSelect.addEventListener('change', function () {
|
||||
var carrier = carrierSelect.value;
|
||||
showPanel(carrier);
|
||||
if (hiddenMethodId) hiddenMethodId.value = '';
|
||||
if (hiddenApaczkaMethodId) hiddenApaczkaMethodId.value = '';
|
||||
if (hiddenCredentialsId) hiddenCredentialsId.value = '';
|
||||
if (hiddenCarrierId) hiddenCarrierId.value = '';
|
||||
if (hiddenServiceName) hiddenServiceName.value = '';
|
||||
var allegroInput = allegroPanel ? allegroPanel.querySelector('.dm-search-input') : null;
|
||||
if (allegroInput) allegroInput.value = '';
|
||||
var inpostSelect = inpostPanel ? inpostPanel.querySelector('.dm-inpost-select') : null;
|
||||
if (inpostSelect) inpostSelect.value = '';
|
||||
if (allegroInput) {
|
||||
allegroInput.value = '';
|
||||
allegroInput.blur();
|
||||
}
|
||||
var allegroDropdown = allegroPanel ? allegroPanel.querySelector('.dm-dropdown') : null;
|
||||
if (allegroDropdown) {
|
||||
allegroDropdown.classList.remove('is-open');
|
||||
}
|
||||
[inpostPanel, apaczkaPanel].forEach(function (panel) {
|
||||
if (!panel) return;
|
||||
var inp = panel.querySelector('.dm-search-input');
|
||||
if (inp) { inp.value = ''; inp.blur(); }
|
||||
var dd = panel.querySelector('.dm-dropdown');
|
||||
if (dd) dd.classList.remove('is-open');
|
||||
});
|
||||
});
|
||||
|
||||
var inpostSelect = inpostPanel ? inpostPanel.querySelector('.dm-inpost-select') : null;
|
||||
if (inpostSelect) {
|
||||
inpostSelect.addEventListener('change', function () {
|
||||
var opt = inpostSelect.options[inpostSelect.selectedIndex];
|
||||
if (hiddenMethodId) hiddenMethodId.value = inpostSelect.value;
|
||||
if (hiddenCredentialsId) hiddenCredentialsId.value = opt ? (opt.getAttribute('data-credentials-id') || '') : '';
|
||||
if (hiddenCarrierId) hiddenCarrierId.value = opt ? (opt.getAttribute('data-carrier-id') || '') : '';
|
||||
if (hiddenServiceName) hiddenServiceName.value = opt ? opt.textContent.trim() : '';
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelectorAll('.dm-searchable-select').forEach(function (wrapper) {
|
||||
@@ -617,6 +671,7 @@ foreach ($dmMappings as $dm) {
|
||||
if (!searchInput || !dropdown || !serviceWrap) return;
|
||||
|
||||
var hiddenMethodId = serviceWrap.querySelector('.dm-hidden-method-id');
|
||||
var hiddenApaczkaMethodId = serviceWrap.querySelector('.dm-hidden-apaczka-method-id');
|
||||
var hiddenCredentialsId = serviceWrap.querySelector('.dm-hidden-credentials-id');
|
||||
var hiddenCarrierId = serviceWrap.querySelector('.dm-hidden-carrier-id');
|
||||
var hiddenServiceName = serviceWrap.querySelector('.dm-hidden-service-name');
|
||||
@@ -624,11 +679,22 @@ foreach ($dmMappings as $dm) {
|
||||
var options = dropdown.querySelectorAll('.searchable-select__option');
|
||||
wrapper.style.position = 'relative';
|
||||
|
||||
var provider = wrapper.getAttribute('data-provider') || '';
|
||||
var isApaczka = provider === 'apaczka';
|
||||
|
||||
function selectOption(opt) {
|
||||
hiddenMethodId.value = opt.getAttribute('data-value') || '';
|
||||
hiddenCredentialsId.value = opt.getAttribute('data-credentials-id') || '';
|
||||
hiddenCarrierId.value = opt.getAttribute('data-carrier-id') || '';
|
||||
hiddenServiceName.value = opt.getAttribute('data-label') || '';
|
||||
var val = opt.getAttribute('data-value') || '';
|
||||
if (isApaczka) {
|
||||
if (hiddenMethodId) hiddenMethodId.value = '';
|
||||
if (hiddenApaczkaMethodId) hiddenApaczkaMethodId.value = val;
|
||||
if (hiddenCredentialsId) hiddenCredentialsId.value = '';
|
||||
} else {
|
||||
if (hiddenMethodId) hiddenMethodId.value = val;
|
||||
if (hiddenApaczkaMethodId) hiddenApaczkaMethodId.value = '';
|
||||
if (hiddenCredentialsId) hiddenCredentialsId.value = opt.getAttribute('data-credentials-id') || '';
|
||||
}
|
||||
if (hiddenCarrierId) hiddenCarrierId.value = opt.getAttribute('data-carrier-id') || '';
|
||||
if (hiddenServiceName) hiddenServiceName.value = opt.getAttribute('data-label') || '';
|
||||
searchInput.value = opt.getAttribute('data-label') || '';
|
||||
dropdown.classList.remove('is-open');
|
||||
options.forEach(function (o) { o.classList.remove('is-selected'); });
|
||||
|
||||
@@ -5,20 +5,31 @@ $receiver = is_array($receiverAddr ?? null) ? $receiverAddr : [];
|
||||
$prefs = is_array($preferences ?? null) ? $preferences : [];
|
||||
$comp = is_array($company ?? null) ? $company : [];
|
||||
$services = is_array($deliveryServices ?? null) ? $deliveryServices : [];
|
||||
$apaczkaSvcList = is_array($apaczkaServices ?? null) ? $apaczkaServices : [];
|
||||
$packages = is_array($existingPackages ?? null) ? $existingPackages : [];
|
||||
$servicesError = (string) ($deliveryServicesError ?? '');
|
||||
$flashSuccessMsg = (string) ($flashSuccess ?? '');
|
||||
$flashErrorMsg = (string) ($flashError ?? '');
|
||||
$deliveryMappingDiagnostic = trim((string) ($deliveryMappingDiagnostic ?? ''));
|
||||
|
||||
$mapping = is_array($deliveryMapping ?? null) ? $deliveryMapping : [];
|
||||
$mappedMethodId = trim((string) ($mapping['allegro_delivery_method_id'] ?? ''));
|
||||
$mappedCredentialsId = trim((string) ($mapping['allegro_credentials_id'] ?? ''));
|
||||
$mappedCarrierId = trim((string) ($mapping['allegro_carrier_id'] ?? ''));
|
||||
$mappedCarrier = trim((string) ($mapping['carrier'] ?? ''));
|
||||
$mappedServiceName = trim((string) ($mapping['allegro_service_name'] ?? ''));
|
||||
$deliveryMethodId = $mappedCarrier === 'allegro' && $mappedMethodId !== ''
|
||||
? $mappedMethodId
|
||||
: ($mappedCarrier !== 'inpost' ? trim((string) ($prefs['delivery_method_id'] ?? ($orderRow['external_carrier_account_id'] ?? ''))) : '');
|
||||
$mappedMethodId = trim((string) ($mapping['provider_service_id'] ?? ''));
|
||||
$mappedCredentialsId = trim((string) ($mapping['provider_account_id'] ?? ''));
|
||||
$mappedCarrierId = trim((string) ($mapping['provider_carrier_id'] ?? ''));
|
||||
$mappedProvider = trim((string) ($mapping['provider'] ?? ''));
|
||||
$mappedServiceName = trim((string) ($mapping['provider_service_name'] ?? ''));
|
||||
$mappedCarrier = $mappedProvider === 'apaczka' ? 'apaczka' : 'allegro';
|
||||
if ($mappedCarrier !== 'apaczka' && stripos($mappedCarrierId, 'inpost') !== false) {
|
||||
$mappedCarrier = 'inpost';
|
||||
}
|
||||
$deliveryMethodId = '';
|
||||
if ($mappedCarrier === 'apaczka' && $mappedMethodId !== '') {
|
||||
$deliveryMethodId = $mappedMethodId;
|
||||
} elseif (($mappedCarrier === 'allegro' || $mappedCarrier === 'inpost') && $mappedMethodId !== '') {
|
||||
$deliveryMethodId = $mappedMethodId;
|
||||
} elseif ($mappedCarrier !== 'inpost') {
|
||||
$deliveryMethodId = trim((string) ($prefs['delivery_method_id'] ?? ($orderRow['external_carrier_account_id'] ?? '')));
|
||||
}
|
||||
$deliveryMethodName = trim((string) ($orderRow['external_carrier_id'] ?? ''));
|
||||
$inpostSvcList = is_array($inpostServices ?? null) ? $inpostServices : [];
|
||||
$preselectedCarrier = $mappedCarrier !== '' ? $mappedCarrier : ($mappedMethodId !== '' ? 'allegro' : '');
|
||||
@@ -90,23 +101,32 @@ $defaultCodAmount = $isCod ? number_format($totalWithTax, 2, '.', '') : '0';
|
||||
<td><?= $e($pkgCarrier !== '' ? $pkgCarrier : '-') ?></td>
|
||||
<td>
|
||||
<?php if ($pkgLabelPath !== ''): ?>
|
||||
<?php if ($pkgStatus === 'error'): ?>
|
||||
-
|
||||
<?php else: ?>
|
||||
<form method="post" action="/orders/<?= $e((string) ($orderId ?? 0)) ?>/shipment/<?= $e((string) $pkgId) ?>/label" style="display:inline">
|
||||
<input type="hidden" name="_csrf_token" value="<?= $e($csrfToken ?? '') ?>">
|
||||
<button type="submit" class="btn btn--sm btn--secondary">Pobierz</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
<?php elseif ($pkgShipmentId !== '' && $pkgStatus === 'created'): ?>
|
||||
<form method="post" action="/orders/<?= $e((string) ($orderId ?? 0)) ?>/shipment/<?= $e((string) $pkgId) ?>/label" style="display:inline">
|
||||
<input type="hidden" name="_csrf_token" value="<?= $e($csrfToken ?? '') ?>">
|
||||
<button type="submit" class="btn btn--sm btn--primary">Generuj etykiete</button>
|
||||
</form>
|
||||
<span class="muted">Generowanie etykiety...</span>
|
||||
<?php else: ?>
|
||||
-
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="text-nowrap"><?= $e((string) ($pkg['created_at'] ?? '')) ?></td>
|
||||
<td>
|
||||
<?php if ($pkgStatus === 'pending'): ?>
|
||||
<button type="button" class="btn btn--sm btn--secondary" data-check-status="<?= $e((string) $pkgId) ?>" data-order-id="<?= $e((string) ($orderId ?? 0)) ?>">Sprawdz status</button>
|
||||
<?php
|
||||
$shouldCheckStatus = $pkgStatus === 'pending' || ($pkgStatus === 'created' && $pkgLabelPath === '');
|
||||
?>
|
||||
<?php if ($shouldCheckStatus): ?>
|
||||
<button type="button"
|
||||
class="btn btn--sm btn--secondary"
|
||||
data-check-status="<?= $e((string) $pkgId) ?>"
|
||||
data-order-id="<?= $e((string) ($orderId ?? 0)) ?>"
|
||||
data-package-status="<?= $e($pkgStatus) ?>"
|
||||
data-auto-check="1">Sprawdz status</button>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -134,9 +154,13 @@ $defaultCodAmount = $isCod ? number_format($totalWithTax, 2, '.', '') : '0';
|
||||
<option value="">-- Wybierz --</option>
|
||||
<option value="allegro"<?= $preselectedCarrier === 'allegro' ? ' selected' : '' ?>>Allegro</option>
|
||||
<option value="inpost"<?= $preselectedCarrier === 'inpost' ? ' selected' : '' ?>>InPost</option>
|
||||
<option value="apaczka"<?= $preselectedCarrier === 'apaczka' ? ' selected' : '' ?>>Apaczka</option>
|
||||
</select>
|
||||
<?php if ($deliveryMethodName !== ''): ?>
|
||||
<div class="muted mt-4" style="font-size:12px">Metoda z zamowienia: <strong><?= $e($deliveryMethodName) ?></strong><?php if ($mappedServiceName !== ''): ?> → <?= $e($mappedCarrier === 'inpost' ? 'InPost' : 'Allegro') ?>: <?= $e($mappedServiceName) ?><?php endif; ?></div>
|
||||
<div class="muted mt-4" style="font-size:12px">Metoda z zamowienia: <strong><?= $e($deliveryMethodName) ?></strong><?php if ($mappedServiceName !== ''): ?> → <?= $e($mappedCarrier === 'inpost' ? 'InPost' : ($mappedCarrier === 'apaczka' ? 'Apaczka' : 'Allegro')) ?>: <?= $e($mappedServiceName) ?><?php endif; ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if ($deliveryMappingDiagnostic !== ''): ?>
|
||||
<div class="flash flash--error mt-8"><?= $e($deliveryMappingDiagnostic) ?></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
@@ -199,11 +223,39 @@ $defaultCodAmount = $isCod ? number_format($totalWithTax, 2, '.', '') : '0';
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div id="shipment-apaczka-panel" style="<?= $preselectedCarrier !== 'apaczka' ? 'display:none' : '' ?>">
|
||||
<?php if ($apaczkaSvcList === []): ?>
|
||||
<div class="muted">Brak uslug Apaczka (sprawdz konfiguracje App ID/App Secret).</div>
|
||||
<?php else: ?>
|
||||
<select class="form-control" id="shipment-apaczka-select">
|
||||
<option value="">-- Wybierz usluge Apaczka --</option>
|
||||
<?php foreach ($apaczkaSvcList as $aSvc): ?>
|
||||
<?php
|
||||
if (!is_array($aSvc)) {
|
||||
continue;
|
||||
}
|
||||
$aSvcId = trim((string) ($aSvc['service_id'] ?? $aSvc['id'] ?? ''));
|
||||
$aSvcName = trim((string) ($aSvc['name'] ?? ''));
|
||||
$aSvcCarrierCode = trim((string) ($aSvc['carrier_code'] ?? ''));
|
||||
$aSvcSelected = $mappedCarrier === 'apaczka' && $mappedMethodId === $aSvcId;
|
||||
?>
|
||||
<option
|
||||
value="<?= $e($aSvcId) ?>"
|
||||
data-carrier-id="<?= $e($aSvcCarrierCode) ?>"
|
||||
<?= $aSvcSelected ? 'selected' : '' ?>>
|
||||
<?= $e($aSvcName !== '' ? $aSvcName : ('ID ' . $aSvcId)) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div id="shipment-empty-panel" class="muted" style="<?= $preselectedCarrier !== '' ? 'display:none' : '' ?>">Wybierz przewoznika</div>
|
||||
</div>
|
||||
|
||||
<input type="hidden" name="credentials_id" id="shipment-credentials-id" value="">
|
||||
<input type="hidden" name="carrier_id" id="shipment-carrier-id" value="">
|
||||
<input type="hidden" name="provider_code" id="shipment-provider-code" value="<?= $e($preselectedCarrier === 'apaczka' ? 'apaczka' : 'allegro_wza') ?>">
|
||||
|
||||
<label class="form-field">
|
||||
<span class="field-label">Typ paczki</span>
|
||||
@@ -349,7 +401,6 @@ $defaultCodAmount = $isCod ? number_format($totalWithTax, 2, '.', '') : '0';
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
// ── Generic searchable select enhancer ──
|
||||
function enhanceSelect(selectEl) {
|
||||
if (!selectEl || selectEl.dataset.enhanced) return;
|
||||
selectEl.dataset.enhanced = '1';
|
||||
@@ -458,17 +509,17 @@ $defaultCodAmount = $isCod ? number_format($totalWithTax, 2, '.', '') : '0';
|
||||
selectEl._syncTrigger = syncTrigger;
|
||||
}
|
||||
|
||||
// ── Enhance all native selects on the page ──
|
||||
var carrierSelect = document.getElementById('shipment-carrier-select');
|
||||
var inpostSelect = document.getElementById('shipment-inpost-select');
|
||||
var apaczkaSelect = document.getElementById('shipment-apaczka-select');
|
||||
|
||||
document.querySelectorAll('form select.form-control').forEach(function (sel) {
|
||||
enhanceSelect(sel);
|
||||
});
|
||||
|
||||
// ── Carrier / service panel logic ──
|
||||
var allegroPanel = document.getElementById('shipment-allegro-panel');
|
||||
var inpostPanel = document.getElementById('shipment-inpost-panel');
|
||||
var apaczkaPanel = document.getElementById('shipment-apaczka-panel');
|
||||
var emptyPanel = document.getElementById('shipment-empty-panel');
|
||||
|
||||
var wrapper = document.getElementById('shipment-service-wrapper');
|
||||
@@ -477,6 +528,7 @@ $defaultCodAmount = $isCod ? number_format($totalWithTax, 2, '.', '') : '0';
|
||||
var dropdown = document.getElementById('shipment-service-dropdown');
|
||||
var credentialsInput = document.getElementById('shipment-credentials-id');
|
||||
var carrierInput = document.getElementById('shipment-carrier-id');
|
||||
var providerInput = document.getElementById('shipment-provider-code');
|
||||
|
||||
if (!carrierSelect || !hiddenInput) return;
|
||||
|
||||
@@ -489,12 +541,13 @@ $defaultCodAmount = $isCod ? number_format($totalWithTax, 2, '.', '') : '0';
|
||||
}
|
||||
|
||||
function showPanel(carrier) {
|
||||
allegroPanel.style.display = carrier === 'allegro' ? '' : 'none';
|
||||
inpostPanel.style.display = carrier === 'inpost' ? '' : 'none';
|
||||
emptyPanel.style.display = carrier === '' ? '' : 'none';
|
||||
if (allegroPanel) allegroPanel.style.display = carrier === 'allegro' ? '' : 'none';
|
||||
if (inpostPanel) inpostPanel.style.display = carrier === 'inpost' ? '' : 'none';
|
||||
if (apaczkaPanel) apaczkaPanel.style.display = carrier === 'apaczka' ? '' : 'none';
|
||||
if (emptyPanel) emptyPanel.style.display = carrier === '' ? '' : 'none';
|
||||
if (providerInput) providerInput.value = carrier === 'apaczka' ? 'apaczka' : 'allegro_wza';
|
||||
}
|
||||
|
||||
// --- Carrier select ---
|
||||
carrierSelect.addEventListener('change', function () {
|
||||
clearHiddenFields();
|
||||
if (searchInput) searchInput.value = '';
|
||||
@@ -502,17 +555,21 @@ $defaultCodAmount = $isCod ? number_format($totalWithTax, 2, '.', '') : '0';
|
||||
inpostSelect.selectedIndex = 0;
|
||||
if (inpostSelect._syncTrigger) inpostSelect._syncTrigger();
|
||||
}
|
||||
if (apaczkaSelect) {
|
||||
apaczkaSelect.selectedIndex = 0;
|
||||
if (apaczkaSelect._syncTrigger) apaczkaSelect._syncTrigger();
|
||||
}
|
||||
allegroOpts.forEach(function (o) { o.classList.remove('is-selected'); });
|
||||
showPanel(carrierSelect.value);
|
||||
});
|
||||
|
||||
// --- InPost select ---
|
||||
if (inpostSelect) {
|
||||
function syncInpostFields() {
|
||||
var opt = inpostSelect.options[inpostSelect.selectedIndex];
|
||||
hiddenInput.value = inpostSelect.value;
|
||||
credentialsInput.value = opt ? (opt.getAttribute('data-credentials-id') || '') : '';
|
||||
carrierInput.value = opt ? (opt.getAttribute('data-carrier-id') || '') : '';
|
||||
if (providerInput) providerInput.value = 'allegro_wza';
|
||||
}
|
||||
inpostSelect.addEventListener('change', syncInpostFields);
|
||||
if (carrierSelect.value === 'inpost' && inpostSelect.value !== '') {
|
||||
@@ -520,7 +577,20 @@ $defaultCodAmount = $isCod ? number_format($totalWithTax, 2, '.', '') : '0';
|
||||
}
|
||||
}
|
||||
|
||||
// --- Allegro searchable select ---
|
||||
if (apaczkaSelect) {
|
||||
function syncApaczkaFields() {
|
||||
var opt = apaczkaSelect.options[apaczkaSelect.selectedIndex];
|
||||
hiddenInput.value = apaczkaSelect.value;
|
||||
credentialsInput.value = '';
|
||||
carrierInput.value = opt ? (opt.getAttribute('data-carrier-id') || '') : '';
|
||||
if (providerInput) providerInput.value = 'apaczka';
|
||||
}
|
||||
apaczkaSelect.addEventListener('change', syncApaczkaFields);
|
||||
if (carrierSelect.value === 'apaczka' && apaczkaSelect.value !== '') {
|
||||
syncApaczkaFields();
|
||||
}
|
||||
}
|
||||
|
||||
if (wrapper && searchInput && dropdown) {
|
||||
var isAllegroOpen = false;
|
||||
|
||||
@@ -528,6 +598,7 @@ $defaultCodAmount = $isCod ? number_format($totalWithTax, 2, '.', '') : '0';
|
||||
hiddenInput.value = opt.getAttribute('data-value') || '';
|
||||
credentialsInput.value = opt.getAttribute('data-credentials-id') || '';
|
||||
carrierInput.value = opt.getAttribute('data-carrier-id') || '';
|
||||
if (providerInput) providerInput.value = 'allegro_wza';
|
||||
searchInput.value = opt.getAttribute('data-label') || '';
|
||||
closeAllegro();
|
||||
allegroOpts.forEach(function (o) { o.classList.remove('is-selected'); });
|
||||
@@ -592,7 +663,6 @@ $defaultCodAmount = $isCod ? number_format($totalWithTax, 2, '.', '') : '0';
|
||||
}
|
||||
}
|
||||
|
||||
// --- Check status ---
|
||||
function checkPackageStatus(pkgId, oId, btn, attempt) {
|
||||
if (btn) {
|
||||
btn.disabled = true;
|
||||
@@ -601,19 +671,24 @@ $defaultCodAmount = $isCod ? number_format($totalWithTax, 2, '.', '') : '0';
|
||||
fetch('/orders/' + oId + '/shipment/' + pkgId + '/status')
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (data) {
|
||||
if (data.status === 'created' || data.status === 'label_ready') {
|
||||
if (data.status === 'label_ready') {
|
||||
window.location.reload();
|
||||
} else if (data.status === 'created' || data.status === 'in_progress' || data.status === 'pending') {
|
||||
if (attempt < 10) {
|
||||
var delayCreated = Math.min(2000 * Math.pow(1.5, attempt), 15000);
|
||||
setTimeout(function () {
|
||||
checkPackageStatus(pkgId, oId, btn, attempt + 1);
|
||||
}, delayCreated);
|
||||
if (btn) btn.textContent = 'Generuje etykiete... (' + (attempt + 1) + ')';
|
||||
} else if (btn) {
|
||||
btn.textContent = 'W toku... Odswiez status';
|
||||
btn.disabled = false;
|
||||
}
|
||||
} else if (data.status === 'error') {
|
||||
if (btn) {
|
||||
btn.textContent = 'Blad: ' + (data.error || '');
|
||||
btn.disabled = false;
|
||||
}
|
||||
} else if (attempt < 10) {
|
||||
var delay = Math.min(2000 * Math.pow(1.5, attempt), 15000);
|
||||
setTimeout(function () {
|
||||
checkPackageStatus(pkgId, oId, btn, attempt + 1);
|
||||
}, delay);
|
||||
if (btn) btn.textContent = 'Sprawdzam... (' + (attempt + 1) + ')';
|
||||
} else {
|
||||
if (btn) {
|
||||
btn.textContent = 'W toku... Sprobuj ponownie';
|
||||
@@ -629,7 +704,6 @@ $defaultCodAmount = $isCod ? number_format($totalWithTax, 2, '.', '') : '0';
|
||||
});
|
||||
}
|
||||
|
||||
// Manual check buttons
|
||||
document.querySelectorAll('[data-check-status]').forEach(function (btn) {
|
||||
btn.addEventListener('click', function () {
|
||||
checkPackageStatus(
|
||||
@@ -641,15 +715,25 @@ $defaultCodAmount = $isCod ? number_format($totalWithTax, 2, '.', '') : '0';
|
||||
});
|
||||
});
|
||||
|
||||
// Auto-poll pending packages on page load
|
||||
var params = new URLSearchParams(window.location.search);
|
||||
var autoCheckId = params.get('check');
|
||||
if (autoCheckId) {
|
||||
var autoBtn = document.querySelector('[data-check-status="' + autoCheckId + '"]');
|
||||
var autoOrderId = autoBtn ? autoBtn.getAttribute('data-order-id') : params.get('id');
|
||||
if (autoOrderId) {
|
||||
checkPackageStatus(autoCheckId, autoOrderId, autoBtn, 0);
|
||||
|
||||
document.querySelectorAll('[data-check-status][data-auto-check=\"1\"]').forEach(function (btn, idx) {
|
||||
var pkgId = btn.getAttribute('data-check-status');
|
||||
var oId = btn.getAttribute('data-order-id');
|
||||
var pkgStatus = (btn.getAttribute('data-package-status') || '').toLowerCase();
|
||||
if (!pkgId || !oId) return;
|
||||
|
||||
if (autoCheckId) {
|
||||
if (pkgId !== autoCheckId) return;
|
||||
} else {
|
||||
if (pkgStatus !== 'pending') return;
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(function () {
|
||||
checkPackageStatus(pkgId, oId, btn, 0);
|
||||
}, idx * 400);
|
||||
});
|
||||
|
||||
})();
|
||||
</script>
|
||||
|
||||
@@ -17,24 +17,26 @@ use App\Modules\Settings\AllegroOAuthClient;
|
||||
use App\Modules\Settings\AllegroOrderImportService;
|
||||
use App\Modules\Settings\AllegroStatusDiscoveryService;
|
||||
use App\Modules\Settings\AllegroStatusMappingRepository;
|
||||
use App\Modules\Settings\ApaczkaApiClient;
|
||||
use App\Modules\Settings\ApaczkaIntegrationController;
|
||||
use App\Modules\Settings\ApaczkaIntegrationRepository;
|
||||
use App\Modules\Settings\CarrierDeliveryMethodMappingRepository;
|
||||
use App\Modules\Settings\InpostIntegrationController;
|
||||
use App\Modules\Settings\InpostIntegrationRepository;
|
||||
use App\Modules\Settings\IntegrationsHubController;
|
||||
use App\Modules\Settings\IntegrationsRepository;
|
||||
use App\Modules\Settings\ShopproIntegrationsController;
|
||||
use App\Modules\Settings\ShopproDeliveryMethodMappingRepository;
|
||||
use App\Modules\Settings\ShopproIntegrationsRepository;
|
||||
use App\Modules\Settings\ShopproStatusMappingRepository;
|
||||
use App\Modules\Settings\AllegroDeliveryMethodMappingRepository;
|
||||
use App\Modules\Settings\CompanySettingsController;
|
||||
use App\Modules\Settings\CompanySettingsRepository;
|
||||
use App\Modules\Settings\CronSettingsController;
|
||||
use App\Modules\Settings\SettingsController;
|
||||
use App\Modules\Shipments\ApaczkaShipmentService;
|
||||
use App\Modules\Shipments\AllegroShipmentService;
|
||||
use App\Modules\Shipments\ShipmentController;
|
||||
use App\Modules\Shipments\ShipmentPackageRepository;
|
||||
use App\Modules\Shipments\ShipmentProviderRegistry;
|
||||
use App\Modules\Users\UsersController;
|
||||
|
||||
return static function (Application $app): void {
|
||||
@@ -53,9 +55,14 @@ return static function (Application $app): void {
|
||||
(string) $app->config('app.integrations.secret', '')
|
||||
);
|
||||
$allegroStatusMappingRepository = new AllegroStatusMappingRepository($app->db());
|
||||
$allegroDeliveryMappingRepository = new AllegroDeliveryMethodMappingRepository($app->db());
|
||||
$carrierDeliveryMappings = new CarrierDeliveryMethodMappingRepository($app->db());
|
||||
$allegroOAuthClient = new AllegroOAuthClient();
|
||||
$apaczkaApiClient = new ApaczkaApiClient();
|
||||
$cronRepository = new CronRepository($app->db());
|
||||
$apaczkaIntegrationRepository = new ApaczkaIntegrationRepository(
|
||||
$app->db(),
|
||||
(string) $app->config('app.integrations.secret', '')
|
||||
);
|
||||
$allegroIntegrationController = new AllegroIntegrationController(
|
||||
$template,
|
||||
$translator,
|
||||
@@ -80,18 +87,17 @@ return static function (Application $app): void {
|
||||
$allegroStatusMappingRepository
|
||||
),
|
||||
(string) $app->config('app.url', ''),
|
||||
$allegroDeliveryMappingRepository,
|
||||
new AllegroApiClient()
|
||||
);
|
||||
$apaczkaIntegrationRepository = new ApaczkaIntegrationRepository(
|
||||
$app->db(),
|
||||
(string) $app->config('app.integrations.secret', '')
|
||||
$carrierDeliveryMappings,
|
||||
new AllegroApiClient(),
|
||||
$apaczkaIntegrationRepository,
|
||||
$apaczkaApiClient
|
||||
);
|
||||
$apaczkaIntegrationController = new ApaczkaIntegrationController(
|
||||
$template,
|
||||
$translator,
|
||||
$auth,
|
||||
$apaczkaIntegrationRepository
|
||||
$apaczkaIntegrationRepository,
|
||||
$apaczkaApiClient
|
||||
);
|
||||
$inpostIntegrationRepository = new InpostIntegrationRepository(
|
||||
$app->db(),
|
||||
@@ -115,10 +121,12 @@ return static function (Application $app): void {
|
||||
new ShopproStatusMappingRepository($app->db()),
|
||||
$app->orderStatuses(),
|
||||
$cronRepository,
|
||||
new ShopproDeliveryMethodMappingRepository($app->db()),
|
||||
$carrierDeliveryMappings,
|
||||
$allegroIntegrationRepository,
|
||||
$allegroOAuthClient,
|
||||
new AllegroApiClient()
|
||||
new AllegroApiClient(),
|
||||
$apaczkaIntegrationRepository,
|
||||
$apaczkaApiClient
|
||||
);
|
||||
$integrationsHubController = new IntegrationsHubController(
|
||||
$template,
|
||||
@@ -155,16 +163,27 @@ return static function (Application $app): void {
|
||||
$companySettingsRepository,
|
||||
new OrdersRepository($app->db())
|
||||
);
|
||||
$apaczkaShipmentService = new ApaczkaShipmentService(
|
||||
$apaczkaIntegrationRepository,
|
||||
$apaczkaApiClient,
|
||||
$shipmentPackageRepository,
|
||||
$companySettingsRepository,
|
||||
new OrdersRepository($app->db())
|
||||
);
|
||||
$shipmentProviderRegistry = new ShipmentProviderRegistry([
|
||||
$shipmentService,
|
||||
$apaczkaShipmentService,
|
||||
]);
|
||||
$shipmentController = new ShipmentController(
|
||||
$template,
|
||||
$translator,
|
||||
$auth,
|
||||
$app->orders(),
|
||||
$companySettingsRepository,
|
||||
$shipmentService,
|
||||
$shipmentProviderRegistry,
|
||||
$shipmentPackageRepository,
|
||||
$app->basePath('storage'),
|
||||
new AllegroDeliveryMethodMappingRepository($app->db())
|
||||
$carrierDeliveryMappings
|
||||
);
|
||||
$authMiddleware = new AuthMiddleware($auth);
|
||||
|
||||
@@ -220,6 +239,7 @@ return static function (Application $app): void {
|
||||
$router->get('/settings/integrations/allegro/oauth/callback', [$allegroIntegrationController, 'oauthCallback']);
|
||||
$router->get('/settings/integrations/apaczka', [$apaczkaIntegrationController, 'index'], [$authMiddleware]);
|
||||
$router->post('/settings/integrations/apaczka/save', [$apaczkaIntegrationController, 'save'], [$authMiddleware]);
|
||||
$router->post('/settings/integrations/apaczka/test', [$apaczkaIntegrationController, 'test'], [$authMiddleware]);
|
||||
$router->get('/settings/integrations/inpost', [$inpostIntegrationController, 'index'], [$authMiddleware]);
|
||||
$router->post('/settings/integrations/inpost/save', [$inpostIntegrationController, 'save'], [$authMiddleware]);
|
||||
$router->get('/settings/integrations/shoppro', [$shopproIntegrationsController, 'index'], [$authMiddleware]);
|
||||
|
||||
@@ -51,8 +51,10 @@ final class AllegroIntegrationController
|
||||
private readonly AllegroOrderImportService $orderImportService,
|
||||
private readonly AllegroStatusDiscoveryService $statusDiscoveryService,
|
||||
private readonly string $appUrl,
|
||||
private readonly ?AllegroDeliveryMethodMappingRepository $deliveryMappings = null,
|
||||
private readonly ?AllegroApiClient $apiClient = null
|
||||
private readonly ?CarrierDeliveryMethodMappingRepository $deliveryMappings = null,
|
||||
private readonly ?AllegroApiClient $apiClient = null,
|
||||
private readonly ?ApaczkaIntegrationRepository $apaczkaRepository = null,
|
||||
private readonly ?ApaczkaApiClient $apaczkaApiClient = null
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -77,7 +79,7 @@ final class AllegroIntegrationController
|
||||
$importIntervalSeconds = $this->currentImportIntervalSeconds();
|
||||
$statusSyncDirection = $this->currentStatusSyncDirection();
|
||||
$statusSyncIntervalMinutes = $this->currentStatusSyncIntervalMinutes();
|
||||
$deliveryServicesData = $tab === 'delivery' ? $this->loadDeliveryServices($settings) : [[], ''];
|
||||
$deliveryServicesData = $tab === 'delivery' ? $this->loadDeliveryServices($settings) : [[], [], ''];
|
||||
|
||||
$html = $this->template->render('settings/allegro', [
|
||||
'title' => $this->translator->get('settings.allegro.title'),
|
||||
@@ -96,10 +98,11 @@ final class AllegroIntegrationController
|
||||
'errorMessage' => (string) Flash::get('settings_error', ''),
|
||||
'successMessage' => (string) Flash::get('settings_success', ''),
|
||||
'warningMessage' => (string) Flash::get('settings_warning', ''),
|
||||
'deliveryMappings' => $this->deliveryMappings !== null ? $this->deliveryMappings->listMappings() : [],
|
||||
'orderDeliveryMethods' => $this->deliveryMappings !== null ? $this->deliveryMappings->getDistinctOrderDeliveryMethods() : [],
|
||||
'deliveryMappings' => $this->deliveryMappings !== null ? $this->deliveryMappings->listMappings('allegro', 0) : [],
|
||||
'orderDeliveryMethods' => $this->deliveryMappings !== null ? $this->deliveryMappings->getDistinctOrderDeliveryMethods('allegro', 0) : [],
|
||||
'allegroDeliveryServices' => $deliveryServicesData[0],
|
||||
'allegroDeliveryServicesError' => $deliveryServicesData[1],
|
||||
'apaczkaDeliveryServices' => $deliveryServicesData[1],
|
||||
'allegroDeliveryServicesError' => $deliveryServicesData[2],
|
||||
'inpostDeliveryServices' => array_values(array_filter(
|
||||
$deliveryServicesData[0],
|
||||
static fn(array $svc) => stripos((string) ($svc['carrierId'] ?? ''), 'inpost') !== false
|
||||
@@ -518,6 +521,7 @@ final class AllegroIntegrationController
|
||||
$orderMethods = (array) $request->input('order_delivery_method', []);
|
||||
$carriers = (array) $request->input('carrier', []);
|
||||
$allegroMethodIds = (array) $request->input('allegro_delivery_method_id', []);
|
||||
$apaczkaMethodIds = (array) $request->input('apaczka_delivery_method_id', []);
|
||||
$credentialsIds = (array) $request->input('allegro_credentials_id', []);
|
||||
$carrierIds = (array) $request->input('allegro_carrier_id', []);
|
||||
$serviceNames = (array) $request->input('allegro_service_name', []);
|
||||
@@ -526,22 +530,25 @@ final class AllegroIntegrationController
|
||||
foreach ($orderMethods as $idx => $orderMethod) {
|
||||
$orderMethod = trim((string) $orderMethod);
|
||||
$carrier = trim((string) ($carriers[$idx] ?? 'allegro'));
|
||||
$allegroMethodId = trim((string) ($allegroMethodIds[$idx] ?? ''));
|
||||
if ($orderMethod === '' || $allegroMethodId === '') {
|
||||
$provider = $carrier === 'apaczka' ? 'apaczka' : 'allegro_wza';
|
||||
$providerServiceId = $provider === 'apaczka'
|
||||
? trim((string) ($apaczkaMethodIds[$idx] ?? ''))
|
||||
: trim((string) ($allegroMethodIds[$idx] ?? ''));
|
||||
if ($orderMethod === '' || $providerServiceId === '') {
|
||||
continue;
|
||||
}
|
||||
$mappings[] = [
|
||||
'order_delivery_method' => $orderMethod,
|
||||
'carrier' => $carrier,
|
||||
'allegro_delivery_method_id' => $allegroMethodId,
|
||||
'allegro_credentials_id' => trim((string) ($credentialsIds[$idx] ?? '')),
|
||||
'allegro_carrier_id' => trim((string) ($carrierIds[$idx] ?? '')),
|
||||
'allegro_service_name' => trim((string) ($serviceNames[$idx] ?? '')),
|
||||
'provider' => $provider,
|
||||
'provider_service_id' => $providerServiceId,
|
||||
'provider_account_id' => $provider === 'allegro_wza' ? trim((string) ($credentialsIds[$idx] ?? '')) : '',
|
||||
'provider_carrier_id' => $provider === 'allegro_wza' ? trim((string) ($carrierIds[$idx] ?? '')) : '',
|
||||
'provider_service_name' => trim((string) ($serviceNames[$idx] ?? '')),
|
||||
];
|
||||
}
|
||||
|
||||
try {
|
||||
$this->deliveryMappings->saveMappings($mappings);
|
||||
$this->deliveryMappings->saveMappings('allegro', 0, $mappings);
|
||||
Flash::set('settings_success', $this->translator->get('settings.allegro.delivery.flash.saved'));
|
||||
} catch (Throwable $exception) {
|
||||
Flash::set('settings_error', $this->translator->get('settings.allegro.delivery.flash.save_failed') . ' ' . $exception->getMessage());
|
||||
@@ -552,49 +559,73 @@ final class AllegroIntegrationController
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $settings
|
||||
* @return array{0: array<int, array<string, mixed>>, 1: string}
|
||||
* @return array{0: array<int, array<string, mixed>>, 1: array<int, array<string, mixed>>, 2: string}
|
||||
*/
|
||||
private function loadDeliveryServices(array $settings): array
|
||||
{
|
||||
if ($this->apiClient === null) {
|
||||
return [[], ''];
|
||||
}
|
||||
$allegroServices = [];
|
||||
$apaczkaServices = [];
|
||||
$errorMessage = '';
|
||||
|
||||
$isConnected = (bool) ($settings['is_connected'] ?? false);
|
||||
if (!$isConnected) {
|
||||
return [[], $this->translator->get('settings.allegro.delivery.not_connected')];
|
||||
}
|
||||
if ($this->apiClient !== null) {
|
||||
$isConnected = (bool) ($settings['is_connected'] ?? false);
|
||||
if (!$isConnected) {
|
||||
$errorMessage = $this->translator->get('settings.allegro.delivery.not_connected');
|
||||
} else {
|
||||
try {
|
||||
$oauth = $this->repository->getTokenCredentials();
|
||||
if ($oauth === null) {
|
||||
$errorMessage = $this->translator->get('settings.allegro.delivery.not_connected');
|
||||
} else {
|
||||
$env = (string) ($oauth['environment'] ?? 'sandbox');
|
||||
$accessToken = trim((string) ($oauth['access_token'] ?? ''));
|
||||
if ($accessToken === '') {
|
||||
$errorMessage = $this->translator->get('settings.allegro.delivery.not_connected');
|
||||
} else {
|
||||
try {
|
||||
$response = $this->apiClient->getDeliveryServices($env, $accessToken);
|
||||
} catch (RuntimeException $ex) {
|
||||
if (trim($ex->getMessage()) === 'ALLEGRO_HTTP_401') {
|
||||
$refreshed = $this->refreshOAuthToken($oauth);
|
||||
if ($refreshed === null) {
|
||||
$errorMessage = $this->translator->get('settings.allegro.delivery.not_connected');
|
||||
$response = [];
|
||||
} else {
|
||||
$response = $this->apiClient->getDeliveryServices($env, $refreshed);
|
||||
}
|
||||
} else {
|
||||
throw $ex;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$oauth = $this->repository->getTokenCredentials();
|
||||
if ($oauth === null) {
|
||||
return [[], $this->translator->get('settings.allegro.delivery.not_connected')];
|
||||
}
|
||||
$env = (string) ($oauth['environment'] ?? 'sandbox');
|
||||
$accessToken = trim((string) ($oauth['access_token'] ?? ''));
|
||||
if ($accessToken === '') {
|
||||
return [[], $this->translator->get('settings.allegro.delivery.not_connected')];
|
||||
}
|
||||
|
||||
try {
|
||||
$response = $this->apiClient->getDeliveryServices($env, $accessToken);
|
||||
} catch (RuntimeException $ex) {
|
||||
if (trim($ex->getMessage()) === 'ALLEGRO_HTTP_401') {
|
||||
$refreshed = $this->refreshOAuthToken($oauth);
|
||||
if ($refreshed === null) {
|
||||
return [[], $this->translator->get('settings.allegro.delivery.not_connected')];
|
||||
if (is_array($response ?? null)) {
|
||||
$allegroServices = is_array($response['services'] ?? null) ? $response['services'] : [];
|
||||
}
|
||||
}
|
||||
}
|
||||
$response = $this->apiClient->getDeliveryServices($env, $refreshed);
|
||||
} else {
|
||||
throw $ex;
|
||||
} catch (Throwable $e) {
|
||||
$errorMessage = $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
$services = is_array($response['services'] ?? null) ? $response['services'] : [];
|
||||
return [$services, ''];
|
||||
} catch (Throwable $e) {
|
||||
return [[], $e->getMessage()];
|
||||
}
|
||||
|
||||
if ($this->apaczkaRepository !== null && $this->apaczkaApiClient !== null) {
|
||||
try {
|
||||
$credentials = $this->apaczkaRepository->getApiCredentials();
|
||||
if (is_array($credentials)) {
|
||||
$apaczkaServices = $this->apaczkaApiClient->getServiceStructure(
|
||||
(string) ($credentials['app_id'] ?? ''),
|
||||
(string) ($credentials['app_secret'] ?? '')
|
||||
);
|
||||
}
|
||||
} catch (Throwable $exception) {
|
||||
if ($errorMessage === '') {
|
||||
$errorMessage = $exception->getMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [$allegroServices, $apaczkaServices, $errorMessage];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
239
src/Modules/Settings/ApaczkaApiClient.php
Normal file
239
src/Modules/Settings/ApaczkaApiClient.php
Normal file
@@ -0,0 +1,239 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Settings;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
final class ApaczkaApiClient
|
||||
{
|
||||
private const API_BASE_URL = 'https://www.apaczka.pl/api/v2';
|
||||
|
||||
/**
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
public function getServiceStructure(string $appId, string $appSecret): array
|
||||
{
|
||||
$response = $this->request('/service_structure/', 'service_structure', $appId, $appSecret, []);
|
||||
$services = $response['response']['services'] ?? $response['services'] ?? [];
|
||||
return is_array($services) ? $services : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $orderPayload
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function sendOrder(string $appId, string $appSecret, array $orderPayload): array
|
||||
{
|
||||
return $this->request('/order_send/', 'order_send', $appId, $appSecret, [
|
||||
'order' => $orderPayload,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function getOrderDetails(string $appId, string $appSecret, int $orderId): array
|
||||
{
|
||||
$safeOrderId = max(1, $orderId);
|
||||
return $this->request('/order/' . $safeOrderId . '/', 'order/' . $safeOrderId, $appId, $appSecret, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function getWaybill(string $appId, string $appSecret, int $orderId): array
|
||||
{
|
||||
$safeOrderId = max(1, $orderId);
|
||||
return $this->request('/waybill/' . $safeOrderId . '/', 'waybill/' . $safeOrderId, $appId, $appSecret, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
public function getPoints(string $appId, string $appSecret, string $type = 'parcel_locker'): array
|
||||
{
|
||||
$safeType = trim($type) !== '' ? trim($type) : 'parcel_locker';
|
||||
$route = 'points/' . $safeType;
|
||||
$response = $this->request('/' . $route . '/', $route, $appId, $appSecret, []);
|
||||
$points = $response['response']['points'] ?? $response['points'] ?? [];
|
||||
return is_array($points) ? $points : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $request
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function request(string $endpointPath, string $route, string $appId, string $appSecret, array $request): array
|
||||
{
|
||||
$normalizedRoute = trim($route, " \t\n\r\0\x0B/");
|
||||
if ($normalizedRoute === '') {
|
||||
throw new RuntimeException('Nie podano endpointu API Apaczka.');
|
||||
}
|
||||
$routeWithTrailingSlash = $normalizedRoute . '/';
|
||||
|
||||
$expires = (string) (time() + 60);
|
||||
$requestJson = json_encode($request);
|
||||
if (!is_string($requestJson)) {
|
||||
throw new RuntimeException('Nie mozna zakodowac payloadu Apaczka.');
|
||||
}
|
||||
|
||||
$basePayload = [
|
||||
'app_id' => trim($appId),
|
||||
'request' => $requestJson,
|
||||
'expires' => $expires,
|
||||
];
|
||||
$signatureVariants = $this->buildSignatureVariants(
|
||||
trim((string) $basePayload['app_id']),
|
||||
$endpointPath,
|
||||
$routeWithTrailingSlash,
|
||||
$requestJson,
|
||||
$expires,
|
||||
$appSecret
|
||||
);
|
||||
|
||||
$lastSignatureError = null;
|
||||
foreach ($signatureVariants as $signature) {
|
||||
$payload = $basePayload;
|
||||
$payload['signature'] = $signature;
|
||||
[$decoded, $httpCode] = $this->executeRequest($endpointPath, $payload);
|
||||
|
||||
$status = (int) ($decoded['status'] ?? 0);
|
||||
$message = $this->resolveErrorMessage($decoded);
|
||||
$isSignatureMismatch = $status === 400 && stripos($message, 'signature') !== false;
|
||||
if ($isSignatureMismatch) {
|
||||
$lastSignatureError = [$decoded, $status, $message];
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($httpCode < 200 || $httpCode >= 300) {
|
||||
throw new RuntimeException('API Apaczka HTTP ' . $httpCode . ': ' . $message);
|
||||
}
|
||||
if ($status !== 200) {
|
||||
$responsePreview = substr(json_encode($decoded, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?: '', 0, 240);
|
||||
throw new RuntimeException(
|
||||
'Blad API Apaczka (status ' . $status . '): ' . $message . '. Odpowiedz: ' . $responsePreview
|
||||
);
|
||||
}
|
||||
|
||||
return $decoded;
|
||||
}
|
||||
|
||||
if ($lastSignatureError !== null) {
|
||||
[$decoded, $status, $message] = $lastSignatureError;
|
||||
$responsePreview = substr(json_encode($decoded, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?: '', 0, 240);
|
||||
throw new RuntimeException(
|
||||
'Blad API Apaczka (status ' . $status . '): ' . $message . '. Odpowiedz: ' . $responsePreview
|
||||
);
|
||||
}
|
||||
|
||||
throw new RuntimeException('Blad API Apaczka.');
|
||||
}
|
||||
|
||||
private function buildSignature(
|
||||
string $appId,
|
||||
string $route,
|
||||
string $requestJson,
|
||||
string $expires,
|
||||
string $appSecret
|
||||
): string
|
||||
{
|
||||
return hash_hmac('sha256', $appId . ':' . $route . ':' . $requestJson . ':' . $expires, trim($appSecret));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, string>
|
||||
*/
|
||||
private function buildSignatureVariants(
|
||||
string $appId,
|
||||
string $endpointPath,
|
||||
string $route,
|
||||
string $requestJson,
|
||||
string $expires,
|
||||
string $appSecret
|
||||
): array {
|
||||
$endpointTrimmed = trim($endpointPath, " \t\n\r\0\x0B/");
|
||||
$endpointWithSlashes = '/' . $endpointTrimmed . '/';
|
||||
$endpointWithTrailingSlash = $endpointTrimmed . '/';
|
||||
|
||||
$variants = [];
|
||||
$variants[] = hash_hmac('sha256', $appId . ':' . $endpointWithTrailingSlash . ':' . $requestJson . ':' . $expires, trim($appSecret));
|
||||
$variants[] = hash_hmac('sha256', $appId . ':' . $route . ':' . $requestJson . ':' . $expires, trim($appSecret));
|
||||
$variants[] = hash_hmac('sha256', $appId . ':' . $endpointWithSlashes . ':' . $requestJson . ':' . $expires, trim($appSecret));
|
||||
$variants[] = hash_hmac('sha256', $appId . ':' . $endpointTrimmed . ':' . $requestJson . ':' . $expires, trim($appSecret));
|
||||
$variants[] = hash('sha256', $appId . ':' . $endpointWithTrailingSlash . ':' . $requestJson . ':' . $expires . ':' . trim($appSecret));
|
||||
$variants[] = hash('sha256', $appId . ':' . $route . ':' . $requestJson . ':' . $expires . ':' . trim($appSecret));
|
||||
$variants[] = hash('sha256', $appId . ':' . $endpointWithSlashes . ':' . $requestJson . ':' . $expires . ':' . trim($appSecret));
|
||||
|
||||
return array_values(array_unique($variants));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $payload
|
||||
* @return array{0: array<string, mixed>, 1: int}
|
||||
*/
|
||||
private function executeRequest(string $endpointPath, array $payload): array
|
||||
{
|
||||
$url = rtrim(self::API_BASE_URL, '/') . '/' . ltrim($endpointPath, '/');
|
||||
$ch = curl_init($url);
|
||||
if ($ch === false) {
|
||||
throw new RuntimeException('Nie udalo sie zainicjowac polaczenia z API Apaczka.');
|
||||
}
|
||||
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => http_build_query($payload),
|
||||
CURLOPT_TIMEOUT => 30,
|
||||
CURLOPT_CONNECTTIMEOUT => 10,
|
||||
CURLOPT_HTTPHEADER => [
|
||||
'Accept: application/json',
|
||||
'Content-Type: application/x-www-form-urlencoded',
|
||||
'User-Agent: orderPRO/1.0',
|
||||
],
|
||||
]);
|
||||
|
||||
$rawBody = curl_exec($ch);
|
||||
$httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$curlError = curl_error($ch);
|
||||
unset($ch);
|
||||
|
||||
if ($rawBody === false) {
|
||||
throw new RuntimeException('Blad polaczenia z API Apaczka: ' . $curlError);
|
||||
}
|
||||
|
||||
$normalizedBody = ltrim((string) $rawBody, "\xEF\xBB\xBF \t\n\r\0\x0B");
|
||||
$decoded = json_decode($normalizedBody, true);
|
||||
if (!is_array($decoded)) {
|
||||
$snippet = substr(trim(strip_tags($normalizedBody)), 0, 180);
|
||||
throw new RuntimeException(
|
||||
'Nieprawidlowa odpowiedz API Apaczka (brak JSON, HTTP ' . $httpCode . '). Fragment: ' . $snippet
|
||||
);
|
||||
}
|
||||
|
||||
return [$decoded, $httpCode];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $decoded
|
||||
*/
|
||||
private function resolveErrorMessage(array $decoded): string
|
||||
{
|
||||
$topMessage = trim((string) ($decoded['message'] ?? ''));
|
||||
if ($topMessage !== '') {
|
||||
return $topMessage;
|
||||
}
|
||||
|
||||
$responseMessage = trim((string) ($decoded['response']['message'] ?? ''));
|
||||
if ($responseMessage !== '') {
|
||||
return $responseMessage;
|
||||
}
|
||||
|
||||
$errorMessage = trim((string) ($decoded['error']['message'] ?? ''));
|
||||
if ($errorMessage !== '') {
|
||||
return $errorMessage;
|
||||
}
|
||||
|
||||
return 'Blad API Apaczka.';
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,8 @@ final class ApaczkaIntegrationController
|
||||
private readonly Template $template,
|
||||
private readonly Translator $translator,
|
||||
private readonly AuthService $auth,
|
||||
private readonly ApaczkaIntegrationRepository $repository
|
||||
private readonly ApaczkaIntegrationRepository $repository,
|
||||
private readonly ApaczkaApiClient $apiClient
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -49,15 +50,16 @@ final class ApaczkaIntegrationController
|
||||
return Response::redirect($redirectTo);
|
||||
}
|
||||
|
||||
$apiKey = trim((string) $request->input('api_key', ''));
|
||||
if ($apiKey === '') {
|
||||
Flash::set('settings_error', $this->translator->get('settings.apaczka.validation.api_key_required'));
|
||||
$appId = trim((string) $request->input('app_id', ''));
|
||||
if ($appId === '') {
|
||||
Flash::set('settings_error', $this->translator->get('settings.apaczka.validation.app_id_required'));
|
||||
return Response::redirect($redirectTo);
|
||||
}
|
||||
|
||||
try {
|
||||
$this->repository->saveSettings([
|
||||
'api_key' => $apiKey,
|
||||
'app_id' => $appId,
|
||||
'app_secret' => trim((string) $request->input('app_secret', '')),
|
||||
]);
|
||||
Flash::set('settings_success', $this->translator->get('settings.apaczka.flash.saved'));
|
||||
} catch (Throwable $exception) {
|
||||
@@ -70,6 +72,29 @@ final class ApaczkaIntegrationController
|
||||
return Response::redirect($redirectTo);
|
||||
}
|
||||
|
||||
public function test(Request $request): Response
|
||||
{
|
||||
$redirectTo = $this->resolveRedirectPath((string) $request->input('return_to', '/settings/integrations/apaczka'));
|
||||
if (!Csrf::validate((string) $request->input('_token', ''))) {
|
||||
Flash::set('settings_error', $this->translator->get('auth.errors.csrf_expired'));
|
||||
return Response::redirect($redirectTo);
|
||||
}
|
||||
|
||||
try {
|
||||
$services = $this->repository->testConnection($this->apiClient);
|
||||
Flash::set('settings_success', $this->translator->get('settings.apaczka.flash.test_success', [
|
||||
'count' => (string) count($services),
|
||||
]));
|
||||
} catch (Throwable $exception) {
|
||||
Flash::set(
|
||||
'settings_error',
|
||||
$this->translator->get('settings.apaczka.flash.test_failed') . ' ' . $exception->getMessage()
|
||||
);
|
||||
}
|
||||
|
||||
return Response::redirect($redirectTo);
|
||||
}
|
||||
|
||||
private function resolveRedirectPath(string $candidate): string
|
||||
{
|
||||
$value = trim($candidate);
|
||||
|
||||
@@ -4,6 +4,8 @@ declare(strict_types=1);
|
||||
namespace App\Modules\Settings;
|
||||
|
||||
use PDO;
|
||||
use RuntimeException;
|
||||
use Throwable;
|
||||
|
||||
final class ApaczkaIntegrationRepository
|
||||
{
|
||||
@@ -27,11 +29,20 @@ final class ApaczkaIntegrationRepository
|
||||
*/
|
||||
public function getSettings(): array
|
||||
{
|
||||
$this->ensureRow();
|
||||
$integrationId = $this->ensureBaseIntegration();
|
||||
$row = $this->fetchRow();
|
||||
$integration = $this->integrations->findById($integrationId);
|
||||
|
||||
$appId = trim((string) ($row['app_id'] ?? ''));
|
||||
$secretEncrypted = $this->resolveSecretEncrypted($row, $integration);
|
||||
|
||||
return [
|
||||
'has_api_key' => trim((string) ($integration['api_key_encrypted'] ?? '')) !== '',
|
||||
'app_id' => $appId,
|
||||
'has_app_id' => $appId !== '',
|
||||
'has_app_secret' => $secretEncrypted !== null && $secretEncrypted !== '',
|
||||
'has_api_key' => $secretEncrypted !== null && $secretEncrypted !== '',
|
||||
'updated_at' => (string) ($row['updated_at'] ?? ''),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -40,15 +51,91 @@ final class ApaczkaIntegrationRepository
|
||||
*/
|
||||
public function saveSettings(array $payload): void
|
||||
{
|
||||
$this->ensureRow();
|
||||
$integrationId = $this->ensureBaseIntegration();
|
||||
|
||||
$apiKey = trim((string) ($payload['api_key'] ?? ''));
|
||||
if ($apiKey === '') {
|
||||
return;
|
||||
$row = $this->fetchRow();
|
||||
if ($row === null) {
|
||||
throw new RuntimeException('Brak rekordu konfiguracji Apaczka.');
|
||||
}
|
||||
|
||||
$encrypted = $this->cipher->encrypt($apiKey);
|
||||
$this->integrations->updateApiKeyEncrypted($integrationId, $encrypted);
|
||||
$appId = trim((string) ($payload['app_id'] ?? ''));
|
||||
if ($appId === '') {
|
||||
throw new RuntimeException('Podaj App ID Apaczka.');
|
||||
}
|
||||
|
||||
$currentEncrypted = $this->resolveSecretEncrypted($row, $this->integrations->findById($integrationId));
|
||||
$appSecret = trim((string) ($payload['app_secret'] ?? ($payload['api_key'] ?? '')));
|
||||
$nextEncrypted = $currentEncrypted;
|
||||
if ($appSecret !== '') {
|
||||
$nextEncrypted = $this->cipher->encrypt($appSecret);
|
||||
}
|
||||
|
||||
if ($nextEncrypted === null || $nextEncrypted === '') {
|
||||
throw new RuntimeException('Podaj App Secret Apaczka.');
|
||||
}
|
||||
|
||||
$stmt = $this->pdo->prepare(
|
||||
'UPDATE apaczka_integration_settings
|
||||
SET app_id = :app_id,
|
||||
app_secret_encrypted = :app_secret_encrypted,
|
||||
api_key_encrypted = :api_key_encrypted,
|
||||
updated_at = NOW()
|
||||
WHERE id = 1'
|
||||
);
|
||||
$stmt->execute([
|
||||
'app_id' => $appId,
|
||||
'app_secret_encrypted' => $nextEncrypted,
|
||||
'api_key_encrypted' => $nextEncrypted,
|
||||
]);
|
||||
|
||||
$this->integrations->updateApiKeyEncrypted($integrationId, $nextEncrypted);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{app_id:string, app_secret:string}|null
|
||||
*/
|
||||
public function getApiCredentials(): ?array
|
||||
{
|
||||
$settings = $this->getSettings();
|
||||
$appId = trim((string) ($settings['app_id'] ?? ''));
|
||||
if ($appId === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$row = $this->fetchRow();
|
||||
$integrationId = $this->ensureBaseIntegration();
|
||||
$integration = $this->integrations->findById($integrationId);
|
||||
$encrypted = $this->resolveSecretEncrypted($row, $integration);
|
||||
if ($encrypted === null || $encrypted === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$secret = trim($this->cipher->decrypt($encrypted));
|
||||
if ($secret === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'app_id' => $appId,
|
||||
'app_secret' => $secret,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
public function testConnection(ApaczkaApiClient $apiClient): array
|
||||
{
|
||||
$credentials = $this->getApiCredentials();
|
||||
if ($credentials === null) {
|
||||
throw new RuntimeException('Brak konfiguracji App ID/App Secret Apaczka.');
|
||||
}
|
||||
|
||||
return $apiClient->getServiceStructure(
|
||||
(string) $credentials['app_id'],
|
||||
(string) $credentials['app_secret']
|
||||
);
|
||||
}
|
||||
|
||||
private function ensureBaseIntegration(): int
|
||||
@@ -61,4 +148,52 @@ final class ApaczkaIntegrationRepository
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
private function ensureRow(): void
|
||||
{
|
||||
$integrationId = $this->ensureBaseIntegration();
|
||||
|
||||
$stmt = $this->pdo->prepare(
|
||||
'INSERT INTO apaczka_integration_settings (id, integration_id, created_at, updated_at)
|
||||
VALUES (1, :integration_id, NOW(), NOW())
|
||||
ON DUPLICATE KEY UPDATE integration_id = VALUES(integration_id), updated_at = VALUES(updated_at)'
|
||||
);
|
||||
$stmt->execute(['integration_id' => $integrationId]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
private function fetchRow(): ?array
|
||||
{
|
||||
try {
|
||||
$stmt = $this->pdo->prepare('SELECT * FROM apaczka_integration_settings WHERE id = 1 LIMIT 1');
|
||||
$stmt->execute();
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
} catch (Throwable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return is_array($row) ? $row : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed>|null $row
|
||||
* @param array<string, mixed>|null $integration
|
||||
*/
|
||||
private function resolveSecretEncrypted(?array $row, ?array $integration): ?string
|
||||
{
|
||||
$base = trim((string) ($integration['api_key_encrypted'] ?? ''));
|
||||
if ($base !== '') {
|
||||
return $base;
|
||||
}
|
||||
|
||||
$modern = trim((string) ($row['app_secret_encrypted'] ?? ''));
|
||||
if ($modern !== '') {
|
||||
return $modern;
|
||||
}
|
||||
|
||||
$legacy = trim((string) ($row['api_key_encrypted'] ?? ''));
|
||||
return $legacy !== '' ? $legacy : null;
|
||||
}
|
||||
}
|
||||
|
||||
219
src/Modules/Settings/CarrierDeliveryMethodMappingRepository.php
Normal file
219
src/Modules/Settings/CarrierDeliveryMethodMappingRepository.php
Normal file
@@ -0,0 +1,219 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Settings;
|
||||
|
||||
use PDO;
|
||||
use Throwable;
|
||||
|
||||
final class CarrierDeliveryMethodMappingRepository
|
||||
{
|
||||
public function __construct(private readonly PDO $pdo)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
public function listMappings(string $sourceSystem, int $sourceIntegrationId = 0): array
|
||||
{
|
||||
$stmt = $this->pdo->prepare(
|
||||
'SELECT *
|
||||
FROM carrier_delivery_method_mappings
|
||||
WHERE source_system = :source_system
|
||||
AND source_integration_id = :source_integration_id
|
||||
ORDER BY order_delivery_method ASC'
|
||||
);
|
||||
$stmt->execute([
|
||||
'source_system' => $this->normalizeSourceSystem($sourceSystem),
|
||||
'source_integration_id' => max(0, $sourceIntegrationId),
|
||||
]);
|
||||
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
return is_array($rows) ? $rows : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
public function findByOrderMethod(string $sourceSystem, int $sourceIntegrationId, string $orderDeliveryMethod): ?array
|
||||
{
|
||||
$method = trim($orderDeliveryMethod);
|
||||
if ($method === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$stmt = $this->pdo->prepare(
|
||||
'SELECT *
|
||||
FROM carrier_delivery_method_mappings
|
||||
WHERE source_system = :source_system
|
||||
AND source_integration_id = :source_integration_id
|
||||
AND order_delivery_method = :order_delivery_method
|
||||
LIMIT 1'
|
||||
);
|
||||
$stmt->execute([
|
||||
'source_system' => $this->normalizeSourceSystem($sourceSystem),
|
||||
'source_integration_id' => max(0, $sourceIntegrationId),
|
||||
'order_delivery_method' => $method,
|
||||
]);
|
||||
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
return is_array($row) ? $row : null;
|
||||
}
|
||||
|
||||
public function hasMappingsForSource(string $sourceSystem, int $sourceIntegrationId = 0): bool
|
||||
{
|
||||
$stmt = $this->pdo->prepare(
|
||||
'SELECT 1
|
||||
FROM carrier_delivery_method_mappings
|
||||
WHERE source_system = :source_system
|
||||
AND source_integration_id = :source_integration_id
|
||||
LIMIT 1'
|
||||
);
|
||||
$stmt->execute([
|
||||
'source_system' => $this->normalizeSourceSystem($sourceSystem),
|
||||
'source_integration_id' => max(0, $sourceIntegrationId),
|
||||
]);
|
||||
|
||||
$value = $stmt->fetchColumn();
|
||||
return $value !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array<string, string>> $mappings
|
||||
*/
|
||||
public function saveMappings(string $sourceSystem, int $sourceIntegrationId, array $mappings): void
|
||||
{
|
||||
|
||||
$normalizedSource = $this->normalizeSourceSystem($sourceSystem);
|
||||
$normalizedIntegrationId = max(0, $sourceIntegrationId);
|
||||
$this->pdo->beginTransaction();
|
||||
try {
|
||||
$deleteStmt = $this->pdo->prepare(
|
||||
'DELETE FROM carrier_delivery_method_mappings
|
||||
WHERE source_system = :source_system
|
||||
AND source_integration_id = :source_integration_id'
|
||||
);
|
||||
$deleteStmt->execute([
|
||||
'source_system' => $normalizedSource,
|
||||
'source_integration_id' => $normalizedIntegrationId,
|
||||
]);
|
||||
|
||||
if ($mappings !== []) {
|
||||
$insertStmt = $this->pdo->prepare(
|
||||
'INSERT INTO carrier_delivery_method_mappings (
|
||||
source_system,
|
||||
source_integration_id,
|
||||
order_delivery_method,
|
||||
provider,
|
||||
provider_service_id,
|
||||
provider_account_id,
|
||||
provider_carrier_id,
|
||||
provider_service_name
|
||||
) VALUES (
|
||||
:source_system,
|
||||
:source_integration_id,
|
||||
:order_delivery_method,
|
||||
:provider,
|
||||
:provider_service_id,
|
||||
:provider_account_id,
|
||||
:provider_carrier_id,
|
||||
:provider_service_name
|
||||
)'
|
||||
);
|
||||
|
||||
foreach ($mappings as $mapping) {
|
||||
$orderMethod = $this->limit(trim((string) ($mapping['order_delivery_method'] ?? '')), 200);
|
||||
$provider = $this->limit(strtolower(trim((string) ($mapping['provider'] ?? ''))), 50);
|
||||
$providerServiceId = $this->limit(trim((string) ($mapping['provider_service_id'] ?? '')), 128);
|
||||
|
||||
if ($orderMethod === '' || $provider === '' || $providerServiceId === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$insertStmt->execute([
|
||||
'source_system' => $normalizedSource,
|
||||
'source_integration_id' => $normalizedIntegrationId,
|
||||
'order_delivery_method' => $orderMethod,
|
||||
'provider' => $provider,
|
||||
'provider_service_id' => $providerServiceId,
|
||||
'provider_account_id' => $this->nullableLimited((string) ($mapping['provider_account_id'] ?? ''), 128),
|
||||
'provider_carrier_id' => $this->nullableLimited((string) ($mapping['provider_carrier_id'] ?? ''), 128),
|
||||
'provider_service_name' => $this->nullableLimited((string) ($mapping['provider_service_name'] ?? ''), 255),
|
||||
]);
|
||||
}
|
||||
}
|
||||
$this->pdo->commit();
|
||||
} catch (Throwable $exception) {
|
||||
if ($this->pdo->inTransaction()) {
|
||||
$this->pdo->rollBack();
|
||||
}
|
||||
throw $exception;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public function getDistinctOrderDeliveryMethods(string $sourceSystem, int $sourceIntegrationId = 0): array
|
||||
{
|
||||
$normalizedSource = $this->normalizeSourceSystem($sourceSystem);
|
||||
|
||||
if ($normalizedSource === 'shoppro') {
|
||||
$stmt = $this->pdo->prepare(
|
||||
"SELECT DISTINCT external_carrier_id
|
||||
FROM orders
|
||||
WHERE external_carrier_id IS NOT NULL
|
||||
AND external_carrier_id <> ''
|
||||
AND source = 'shoppro'
|
||||
AND integration_id = :integration_id
|
||||
ORDER BY external_carrier_id ASC"
|
||||
);
|
||||
$stmt->execute(['integration_id' => max(0, $sourceIntegrationId)]);
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_COLUMN);
|
||||
return is_array($rows) ? $rows : [];
|
||||
}
|
||||
|
||||
$stmt = $this->pdo->query(
|
||||
"SELECT DISTINCT external_carrier_id
|
||||
FROM orders
|
||||
WHERE external_carrier_id IS NOT NULL
|
||||
AND external_carrier_id <> ''
|
||||
AND source = 'allegro'
|
||||
AND external_carrier_id NOT REGEXP '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'
|
||||
ORDER BY external_carrier_id ASC"
|
||||
);
|
||||
|
||||
$rows = $stmt !== false ? $stmt->fetchAll(PDO::FETCH_COLUMN) : [];
|
||||
return is_array($rows) ? $rows : [];
|
||||
}
|
||||
|
||||
private function normalizeSourceSystem(string $value): string
|
||||
{
|
||||
$source = strtolower(trim($value));
|
||||
return in_array($source, ['allegro', 'shoppro'], true) ? $source : 'allegro';
|
||||
}
|
||||
|
||||
private function nullableString(string $value): ?string
|
||||
{
|
||||
$trimmed = trim($value);
|
||||
return $trimmed === '' ? null : $trimmed;
|
||||
}
|
||||
|
||||
private function nullableLimited(string $value, int $max): ?string
|
||||
{
|
||||
$trimmed = $this->limit(trim($value), $max);
|
||||
return $trimmed === '' ? null : $trimmed;
|
||||
}
|
||||
|
||||
private function limit(string $value, int $max): string
|
||||
{
|
||||
if ($max <= 0) {
|
||||
return '';
|
||||
}
|
||||
if (mb_strlen($value) <= $max) {
|
||||
return $value;
|
||||
}
|
||||
return mb_substr($value, 0, $max);
|
||||
}
|
||||
}
|
||||
@@ -50,6 +50,7 @@ final class CompanySettingsController
|
||||
$this->repository->saveSettings([
|
||||
'company_name' => (string) $request->input('company_name', ''),
|
||||
'person_name' => (string) $request->input('person_name', ''),
|
||||
'sender_contact_person' => (string) $request->input('sender_contact_person', ''),
|
||||
'street' => (string) $request->input('street', ''),
|
||||
'city' => (string) $request->input('city', ''),
|
||||
'postal_code' => (string) $request->input('postal_code', ''),
|
||||
|
||||
@@ -33,6 +33,7 @@ final class CompanySettingsRepository
|
||||
return [
|
||||
'company_name' => trim((string) ($row['company_name'] ?? '')),
|
||||
'person_name' => trim((string) ($row['person_name'] ?? '')),
|
||||
'sender_contact_person' => trim((string) ($row['sender_contact_person'] ?? '')),
|
||||
'street' => trim((string) ($row['street'] ?? '')),
|
||||
'city' => trim((string) ($row['city'] ?? '')),
|
||||
'postal_code' => trim((string) ($row['postal_code'] ?? '')),
|
||||
@@ -61,6 +62,7 @@ final class CompanySettingsRepository
|
||||
'UPDATE company_settings SET
|
||||
company_name = :company_name,
|
||||
person_name = :person_name,
|
||||
sender_contact_person = :sender_contact_person,
|
||||
street = :street,
|
||||
city = :city,
|
||||
postal_code = :postal_code,
|
||||
@@ -82,6 +84,7 @@ final class CompanySettingsRepository
|
||||
$statement->execute([
|
||||
'company_name' => $this->nullableString((string) ($data['company_name'] ?? '')),
|
||||
'person_name' => $this->nullableString((string) ($data['person_name'] ?? '')),
|
||||
'sender_contact_person' => $this->nullableString((string) ($data['sender_contact_person'] ?? '')),
|
||||
'street' => $this->nullableString((string) ($data['street'] ?? '')),
|
||||
'city' => $this->nullableString((string) ($data['city'] ?? '')),
|
||||
'postal_code' => $this->nullableString((string) ($data['postal_code'] ?? '')),
|
||||
@@ -109,6 +112,9 @@ final class CompanySettingsRepository
|
||||
$settings = $this->getSettings();
|
||||
return [
|
||||
'name' => $settings['person_name'] !== '' ? $settings['person_name'] : ($settings['company_name'] !== '' ? $settings['company_name'] : null),
|
||||
'contactPerson' => $settings['sender_contact_person'] !== ''
|
||||
? $settings['sender_contact_person']
|
||||
: ($settings['person_name'] !== '' ? $settings['person_name'] : null),
|
||||
'company' => $settings['company_name'] !== '' ? $settings['company_name'] : null,
|
||||
'street' => $settings['street'] !== '' ? $settings['street'] : null,
|
||||
'city' => $settings['city'] !== '' ? $settings['city'] : null,
|
||||
@@ -140,6 +146,7 @@ final class CompanySettingsRepository
|
||||
return [
|
||||
'company_name' => '',
|
||||
'person_name' => '',
|
||||
'sender_contact_person' => '',
|
||||
'street' => '',
|
||||
'city' => '',
|
||||
'postal_code' => '',
|
||||
|
||||
@@ -82,14 +82,15 @@ final class IntegrationsHubController
|
||||
{
|
||||
$settings = $this->apaczka->getSettings();
|
||||
$meta = $this->integrations->findByTypeAndName('apaczka', 'Apaczka') ?? [];
|
||||
$isConfigured = !empty($settings['has_app_id']) && !empty($settings['has_app_secret']);
|
||||
|
||||
return [
|
||||
'provider' => $this->translator->get('settings.integrations_hub.providers.apaczka'),
|
||||
'instance' => 'Apaczka',
|
||||
'authorization_status' => !empty($settings['has_api_key'])
|
||||
'authorization_status' => $isConfigured
|
||||
? $this->translator->get('settings.integrations_hub.status.configured')
|
||||
: $this->translator->get('settings.integrations_hub.status.not_configured'),
|
||||
'secret_status' => !empty($settings['has_api_key'])
|
||||
'secret_status' => $isConfigured
|
||||
? $this->translator->get('settings.integrations_hub.status.saved')
|
||||
: $this->translator->get('settings.integrations_hub.status.missing'),
|
||||
'is_active' => (int) ($meta['is_active'] ?? 0) === 1,
|
||||
|
||||
@@ -39,10 +39,12 @@ final class ShopproIntegrationsController
|
||||
private readonly ShopproStatusMappingRepository $statusMappings,
|
||||
private readonly OrderStatusRepository $orderStatuses,
|
||||
private readonly CronRepository $cronRepository,
|
||||
private readonly ShopproDeliveryMethodMappingRepository $deliveryMappings,
|
||||
private readonly CarrierDeliveryMethodMappingRepository $deliveryMappings,
|
||||
private readonly AllegroIntegrationRepository $allegroIntegrationRepository,
|
||||
private readonly AllegroOAuthClient $allegroOAuthClient,
|
||||
private readonly AllegroApiClient $allegroApiClient
|
||||
private readonly AllegroApiClient $allegroApiClient,
|
||||
private readonly ?ApaczkaIntegrationRepository $apaczkaRepository = null,
|
||||
private readonly ?ApaczkaApiClient $apaczkaApiClient = null
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -68,12 +70,12 @@ final class ShopproIntegrationsController
|
||||
: [];
|
||||
$deliveryServicesData = $activeTab === 'delivery'
|
||||
? $this->loadDeliveryServices()
|
||||
: [[], ''];
|
||||
: [[], [], ''];
|
||||
$deliveryMappings = $selectedIntegration !== null
|
||||
? $this->deliveryMappings->listMappings((int) ($selectedIntegration['id'] ?? 0))
|
||||
? $this->deliveryMappings->listMappings('shoppro', (int) ($selectedIntegration['id'] ?? 0))
|
||||
: [];
|
||||
$orderDeliveryMethods = $selectedIntegration !== null
|
||||
? $this->deliveryMappings->getDistinctOrderDeliveryMethods((int) ($selectedIntegration['id'] ?? 0))
|
||||
? $this->deliveryMappings->getDistinctOrderDeliveryMethods('shoppro', (int) ($selectedIntegration['id'] ?? 0))
|
||||
: [];
|
||||
|
||||
$html = $this->template->render('settings/shoppro', [
|
||||
@@ -94,7 +96,8 @@ final class ShopproIntegrationsController
|
||||
'deliveryMappings' => $deliveryMappings,
|
||||
'orderDeliveryMethods' => $orderDeliveryMethods,
|
||||
'allegroDeliveryServices' => $deliveryServicesData[0],
|
||||
'allegroDeliveryServicesError' => $deliveryServicesData[1],
|
||||
'apaczkaDeliveryServices' => $deliveryServicesData[1],
|
||||
'allegroDeliveryServicesError' => $deliveryServicesData[2],
|
||||
'inpostDeliveryServices' => array_values(array_filter(
|
||||
$deliveryServicesData[0],
|
||||
static fn (array $svc): bool => stripos((string) ($svc['carrierId'] ?? ''), 'inpost') !== false
|
||||
@@ -350,31 +353,36 @@ final class ShopproIntegrationsController
|
||||
$orderMethods = (array) $request->input('order_delivery_method', []);
|
||||
$carriers = (array) $request->input('carrier', []);
|
||||
$allegroMethodIds = (array) $request->input('allegro_delivery_method_id', []);
|
||||
$apaczkaMethodIds = (array) $request->input('apaczka_delivery_method_id', []);
|
||||
$credentialsIds = (array) $request->input('allegro_credentials_id', []);
|
||||
$carrierIds = (array) $request->input('allegro_carrier_id', []);
|
||||
$serviceNames = (array) $request->input('allegro_service_name', []);
|
||||
|
||||
|
||||
$mappings = [];
|
||||
foreach ($orderMethods as $index => $orderMethod) {
|
||||
$orderMethodValue = trim((string) $orderMethod);
|
||||
$carrier = trim((string) ($carriers[$index] ?? 'allegro'));
|
||||
$allegroMethodId = trim((string) ($allegroMethodIds[$index] ?? ''));
|
||||
if ($orderMethodValue === '' || $allegroMethodId === '') {
|
||||
$provider = $carrier === 'apaczka' ? 'apaczka' : 'allegro_wza';
|
||||
$providerServiceId = $provider === 'apaczka'
|
||||
? trim((string) ($apaczkaMethodIds[$index] ?? ''))
|
||||
: trim((string) ($allegroMethodIds[$index] ?? ''));
|
||||
if ($orderMethodValue === '' || $providerServiceId === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$mappings[] = [
|
||||
'order_delivery_method' => $orderMethodValue,
|
||||
'carrier' => $carrier,
|
||||
'allegro_delivery_method_id' => $allegroMethodId,
|
||||
'allegro_credentials_id' => trim((string) ($credentialsIds[$index] ?? '')),
|
||||
'allegro_carrier_id' => trim((string) ($carrierIds[$index] ?? '')),
|
||||
'allegro_service_name' => trim((string) ($serviceNames[$index] ?? '')),
|
||||
'provider' => $provider,
|
||||
'provider_service_id' => $providerServiceId,
|
||||
'provider_account_id' => $provider === 'allegro_wza' ? trim((string) ($credentialsIds[$index] ?? '')) : '',
|
||||
'provider_carrier_id' => $provider === 'allegro_wza' ? trim((string) ($carrierIds[$index] ?? '')) : '',
|
||||
'provider_service_name' => trim((string) ($serviceNames[$index] ?? '')),
|
||||
];
|
||||
}
|
||||
|
||||
try {
|
||||
$this->deliveryMappings->saveMappings($integrationId, $mappings);
|
||||
$this->deliveryMappings->saveMappings('shoppro', $integrationId, $mappings);
|
||||
Flash::set('settings_success', $this->translator->get('settings.integrations.delivery.flash.saved'));
|
||||
} catch (Throwable $exception) {
|
||||
Flash::set(
|
||||
@@ -792,41 +800,66 @@ final class ShopproIntegrationsController
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{0: array<int, array<string, mixed>>, 1: string}
|
||||
* @return array{0: array<int, array<string, mixed>>, 1: array<int, array<string, mixed>>, 2: string}
|
||||
*/
|
||||
private function loadDeliveryServices(): array
|
||||
{
|
||||
$allegroServices = [];
|
||||
$apaczkaServices = [];
|
||||
$errorMessage = '';
|
||||
|
||||
try {
|
||||
$oauth = $this->allegroIntegrationRepository->getTokenCredentials();
|
||||
if (!is_array($oauth)) {
|
||||
return [[], $this->translator->get('settings.integrations.delivery.not_connected')];
|
||||
}
|
||||
$errorMessage = $this->translator->get('settings.integrations.delivery.not_connected');
|
||||
} else {
|
||||
$env = (string) ($oauth['environment'] ?? 'sandbox');
|
||||
$accessToken = trim((string) ($oauth['access_token'] ?? ''));
|
||||
if ($accessToken === '') {
|
||||
$errorMessage = $this->translator->get('settings.integrations.delivery.not_connected');
|
||||
} else {
|
||||
try {
|
||||
$response = $this->allegroApiClient->getDeliveryServices($env, $accessToken);
|
||||
} catch (RuntimeException $exception) {
|
||||
if (trim($exception->getMessage()) !== 'ALLEGRO_HTTP_401') {
|
||||
throw $exception;
|
||||
}
|
||||
|
||||
$env = (string) ($oauth['environment'] ?? 'sandbox');
|
||||
$accessToken = trim((string) ($oauth['access_token'] ?? ''));
|
||||
if ($accessToken === '') {
|
||||
return [[], $this->translator->get('settings.integrations.delivery.not_connected')];
|
||||
}
|
||||
$refreshedToken = $this->refreshAllegroAccessToken($oauth);
|
||||
if ($refreshedToken === null) {
|
||||
$errorMessage = $this->translator->get('settings.integrations.delivery.not_connected');
|
||||
$response = [];
|
||||
} else {
|
||||
$response = $this->allegroApiClient->getDeliveryServices($env, $refreshedToken);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$response = $this->allegroApiClient->getDeliveryServices($env, $accessToken);
|
||||
} catch (RuntimeException $exception) {
|
||||
if (trim($exception->getMessage()) !== 'ALLEGRO_HTTP_401') {
|
||||
throw $exception;
|
||||
if (is_array($response ?? null)) {
|
||||
$allegroServices = is_array($response['services'] ?? null) ? $response['services'] : [];
|
||||
}
|
||||
}
|
||||
|
||||
$refreshedToken = $this->refreshAllegroAccessToken($oauth);
|
||||
if ($refreshedToken === null) {
|
||||
return [[], $this->translator->get('settings.integrations.delivery.not_connected')];
|
||||
}
|
||||
$response = $this->allegroApiClient->getDeliveryServices($env, $refreshedToken);
|
||||
}
|
||||
|
||||
$services = is_array($response['services'] ?? null) ? $response['services'] : [];
|
||||
return [$services, ''];
|
||||
} catch (Throwable $exception) {
|
||||
return [[], $exception->getMessage()];
|
||||
$errorMessage = $exception->getMessage();
|
||||
}
|
||||
|
||||
if ($this->apaczkaRepository !== null && $this->apaczkaApiClient !== null) {
|
||||
try {
|
||||
$credentials = $this->apaczkaRepository->getApiCredentials();
|
||||
if (is_array($credentials)) {
|
||||
$apaczkaServices = $this->apaczkaApiClient->getServiceStructure(
|
||||
(string) ($credentials['app_id'] ?? ''),
|
||||
(string) ($credentials['app_secret'] ?? '')
|
||||
);
|
||||
}
|
||||
} catch (Throwable $exception) {
|
||||
if ($errorMessage === '') {
|
||||
$errorMessage = $exception->getMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [$allegroServices, $apaczkaServices, $errorMessage];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -434,7 +434,7 @@ final class ShopproOrdersSyncService
|
||||
'customer_login' => $this->nullableString((string) $this->readPath($payload, [
|
||||
'buyer_email', 'customer.email', 'buyer.email', 'client.email', 'email', 'customer.login', 'buyer.login',
|
||||
])),
|
||||
'is_invoice' => !empty($this->readPath($payload, ['is_invoice', 'invoice.required'])),
|
||||
'is_invoice' => $this->resolveInvoiceRequested($payload),
|
||||
'is_encrypted' => false,
|
||||
'is_canceled_by_buyer' => false,
|
||||
'currency' => $currency,
|
||||
@@ -548,6 +548,11 @@ final class ShopproOrdersSyncService
|
||||
],
|
||||
];
|
||||
|
||||
$invoiceAddress = $this->buildInvoiceAddress($payload, $customerName, $customerEmail, $customerPhone);
|
||||
if ($invoiceAddress !== null) {
|
||||
$result[] = $invoiceAddress;
|
||||
}
|
||||
|
||||
$deliveryFirstName = $this->nullableString((string) $this->readPath($payload, [
|
||||
'delivery.address.first_name', 'delivery.address.firstname', 'shipping.address.first_name', 'shipping.address.firstname',
|
||||
'delivery_address.first_name', 'delivery_address.firstname', 'shipping_address.first_name', 'shipping_address.firstname',
|
||||
@@ -638,6 +643,119 @@ final class ShopproOrdersSyncService
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $payload
|
||||
*/
|
||||
private function resolveInvoiceRequested(array $payload): bool
|
||||
{
|
||||
$explicitInvoice = $this->readPath($payload, ['is_invoice', 'invoice.required', 'invoice']);
|
||||
if (!empty($explicitInvoice)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$companyName = $this->nullableString((string) $this->readPath($payload, [
|
||||
'invoice.company_name', 'invoice.company', 'billing.company_name', 'billing.company',
|
||||
'firm_name', 'company_name', 'client_company', 'buyer_company',
|
||||
]));
|
||||
$taxNumber = $this->nullableString((string) $this->readPath($payload, [
|
||||
'invoice.tax_id', 'invoice.nip', 'billing.tax_id', 'billing.nip',
|
||||
'firm_nip', 'company_nip', 'tax_id', 'nip',
|
||||
]));
|
||||
|
||||
return $companyName !== null || $taxNumber !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $payload
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
private function buildInvoiceAddress(
|
||||
array $payload,
|
||||
?string $customerName,
|
||||
?string $customerEmail,
|
||||
?string $customerPhone
|
||||
): ?array {
|
||||
$companyName = $this->nullableString((string) $this->readPath($payload, [
|
||||
'invoice.company_name', 'invoice.company', 'billing.company_name', 'billing.company',
|
||||
'firm_name', 'company_name', 'client_company', 'buyer_company',
|
||||
]));
|
||||
$companyTaxNumber = $this->nullableString((string) $this->readPath($payload, [
|
||||
'invoice.tax_id', 'invoice.nip', 'billing.tax_id', 'billing.nip',
|
||||
'firm_nip', 'company_nip', 'tax_id', 'nip',
|
||||
]));
|
||||
$invoiceFirstName = $this->nullableString((string) $this->readPath($payload, [
|
||||
'invoice.first_name', 'invoice.firstname', 'billing_address.first_name', 'billing_address.firstname',
|
||||
'buyer.first_name', 'customer.first_name', 'client_name',
|
||||
]));
|
||||
$invoiceLastName = $this->nullableString((string) $this->readPath($payload, [
|
||||
'invoice.last_name', 'invoice.lastname', 'billing_address.last_name', 'billing_address.lastname',
|
||||
'buyer.last_name', 'customer.last_name', 'client_surname',
|
||||
]));
|
||||
$invoiceName = $companyName ?? $this->composeName($invoiceFirstName, $invoiceLastName, $customerName ?? 'Faktura');
|
||||
|
||||
$streetName = $this->nullableString((string) $this->readPath($payload, [
|
||||
'invoice.address.street', 'invoice.street', 'billing_address.street', 'billing.street',
|
||||
'firm_street', 'company_street',
|
||||
]));
|
||||
$streetNumber = $this->nullableString((string) $this->readPath($payload, [
|
||||
'invoice.address.street_number', 'invoice.street_number', 'invoice.house_number',
|
||||
'billing_address.street_number', 'billing_address.house_number',
|
||||
'billing.street_number', 'house_number', 'street_number',
|
||||
]));
|
||||
$city = $this->nullableString((string) $this->readPath($payload, [
|
||||
'invoice.address.city', 'invoice.city', 'billing_address.city', 'billing.city',
|
||||
'firm_city', 'company_city',
|
||||
]));
|
||||
$zipCode = $this->nullableString((string) $this->readPath($payload, [
|
||||
'invoice.address.zip', 'invoice.address.postcode', 'invoice.zip', 'invoice.postcode',
|
||||
'billing_address.zip', 'billing_address.postcode', 'billing.zip', 'billing.postcode',
|
||||
'firm_postal_code', 'company_postal_code',
|
||||
]));
|
||||
$country = $this->nullableString((string) $this->readPath($payload, [
|
||||
'invoice.address.country', 'invoice.country', 'billing_address.country', 'billing.country',
|
||||
'firm_country', 'company_country',
|
||||
]));
|
||||
$email = $this->nullableString((string) $this->readPath($payload, [
|
||||
'invoice.email', 'billing_address.email', 'billing.email', 'client_email',
|
||||
])) ?? $customerEmail;
|
||||
$phone = $this->nullableString((string) $this->readPath($payload, [
|
||||
'invoice.phone', 'billing_address.phone', 'billing.phone', 'client_phone',
|
||||
])) ?? $customerPhone;
|
||||
|
||||
$hasInvoiceData = $companyName !== null
|
||||
|| $companyTaxNumber !== null
|
||||
|| $streetName !== null
|
||||
|| $city !== null
|
||||
|| $zipCode !== null;
|
||||
if (!$hasInvoiceData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'address_type' => 'invoice',
|
||||
'name' => $invoiceName ?? 'Faktura',
|
||||
'phone' => $phone,
|
||||
'email' => $email,
|
||||
'street_name' => $streetName,
|
||||
'street_number' => $streetNumber,
|
||||
'city' => $city,
|
||||
'zip_code' => $zipCode,
|
||||
'country' => $country,
|
||||
'company_tax_number' => $companyTaxNumber,
|
||||
'company_name' => $companyName,
|
||||
'payload_json' => [
|
||||
'invoice' => $this->readPath($payload, ['invoice']),
|
||||
'billing' => $this->readPath($payload, ['billing']),
|
||||
'billing_address' => $this->readPath($payload, ['billing_address']),
|
||||
'firm_name' => $this->readPath($payload, ['firm_name']),
|
||||
'firm_nip' => $this->readPath($payload, ['firm_nip']),
|
||||
'firm_street' => $this->readPath($payload, ['firm_street']),
|
||||
'firm_postal_code' => $this->readPath($payload, ['firm_postal_code']),
|
||||
'firm_city' => $this->readPath($payload, ['firm_city']),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
private function composeName(?string $firstName, ?string $lastName, ?string $fallback): ?string
|
||||
{
|
||||
$name = trim(trim((string) $firstName) . ' ' . trim((string) $lastName));
|
||||
|
||||
@@ -13,7 +13,7 @@ use DateTimeImmutable;
|
||||
use RuntimeException;
|
||||
use Throwable;
|
||||
|
||||
final class AllegroShipmentService
|
||||
final class AllegroShipmentService implements ShipmentProviderInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly AllegroIntegrationRepository $integrationRepository,
|
||||
@@ -25,6 +25,11 @@ final class AllegroShipmentService
|
||||
) {
|
||||
}
|
||||
|
||||
public function code(): string
|
||||
{
|
||||
return 'allegro_wza';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
|
||||
882
src/Modules/Shipments/ApaczkaShipmentService.php
Normal file
882
src/Modules/Shipments/ApaczkaShipmentService.php
Normal file
@@ -0,0 +1,882 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Shipments;
|
||||
|
||||
use App\Modules\Orders\OrdersRepository;
|
||||
use App\Modules\Settings\ApaczkaApiClient;
|
||||
use App\Modules\Settings\ApaczkaIntegrationRepository;
|
||||
use App\Modules\Settings\CompanySettingsRepository;
|
||||
use RuntimeException;
|
||||
use Throwable;
|
||||
|
||||
final class ApaczkaShipmentService implements ShipmentProviderInterface
|
||||
{
|
||||
/**
|
||||
* @var array<string, array{street:string,postal_code:string,city:string}>
|
||||
*/
|
||||
private array $pointAddressCache = [];
|
||||
|
||||
public function __construct(
|
||||
private readonly ApaczkaIntegrationRepository $integrationRepository,
|
||||
private readonly ApaczkaApiClient $apiClient,
|
||||
private readonly ShipmentPackageRepository $packages,
|
||||
private readonly CompanySettingsRepository $companySettings,
|
||||
private readonly OrdersRepository $ordersRepository
|
||||
) {
|
||||
}
|
||||
|
||||
public function code(): string
|
||||
{
|
||||
return 'apaczka';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
public function getDeliveryServices(): array
|
||||
{
|
||||
[$appId, $appSecret] = $this->requireCredentials();
|
||||
return $this->apiClient->getServiceStructure($appId, $appSecret);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $formData
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function createShipment(int $orderId, array $formData): array
|
||||
{
|
||||
$order = $this->ordersRepository->findDetails($orderId);
|
||||
if ($order === null) {
|
||||
throw new RuntimeException('Zamowienie nie znalezione.');
|
||||
}
|
||||
|
||||
[$appId, $appSecret] = $this->requireCredentials();
|
||||
|
||||
$sender = $this->companySettings->getSenderAddress();
|
||||
$this->validateSenderAddress($sender);
|
||||
|
||||
$orderData = is_array($order['order'] ?? null) ? $order['order'] : [];
|
||||
$sourceOrderId = trim((string) ($orderData['source_order_id'] ?? ''));
|
||||
|
||||
$deliveryMethodId = trim((string) ($formData['delivery_method_id'] ?? ''));
|
||||
if ($deliveryMethodId === '') {
|
||||
throw new RuntimeException('Nie podano uslugi Apaczka.');
|
||||
}
|
||||
|
||||
$serviceDefinition = $this->resolveServiceDefinition($appId, $appSecret, $deliveryMethodId);
|
||||
$receiverAddress = $this->buildReceiverAddress($order, $formData, $appId, $appSecret);
|
||||
$senderAddress = $this->normalizeSender($sender);
|
||||
$senderPointId = trim((string) ($formData['sender_point_id'] ?? ''));
|
||||
if ($senderPointId !== '') {
|
||||
$this->applyPointIdentifiers($senderAddress, $senderPointId);
|
||||
}
|
||||
|
||||
$weightKg = max(0.001, (float) ($formData['weight_kg'] ?? 1.0));
|
||||
$lengthCm = max(1.0, (float) ($formData['length_cm'] ?? 25.0));
|
||||
$widthCm = max(1.0, (float) ($formData['width_cm'] ?? 20.0));
|
||||
$heightCm = max(1.0, (float) ($formData['height_cm'] ?? 8.0));
|
||||
|
||||
$insuranceAmount = max(0.0, (float) ($formData['insurance_amount'] ?? 0));
|
||||
$codAmount = max(0.0, (float) ($formData['cod_amount'] ?? 0));
|
||||
|
||||
$apiPayload = [
|
||||
'service_id' => ctype_digit($deliveryMethodId) ? (int) $deliveryMethodId : $deliveryMethodId,
|
||||
'address' => [
|
||||
'sender' => $senderAddress,
|
||||
'receiver' => $receiverAddress,
|
||||
],
|
||||
'shipment' => [[
|
||||
'shipment_type_code' => 'PACZKA',
|
||||
'dimension1' => (int) round($lengthCm),
|
||||
'dimension2' => (int) round($widthCm),
|
||||
'dimension3' => (int) round($heightCm),
|
||||
'weight' => (float) round($weightKg, 3),
|
||||
'is_nstd' => 0,
|
||||
]],
|
||||
'content' => 'orderPRO ' . ($sourceOrderId !== '' ? $sourceOrderId : (string) $orderId),
|
||||
'comment' => 'orderPRO ' . ($sourceOrderId !== '' ? $sourceOrderId : (string) $orderId),
|
||||
];
|
||||
$pickup = $this->buildPickupPayload($serviceDefinition, $senderPointId, $formData);
|
||||
if ($pickup !== []) {
|
||||
$apiPayload['pickup'] = $pickup;
|
||||
}
|
||||
$this->validateServiceRequirements($serviceDefinition, $receiverAddress, $senderAddress);
|
||||
|
||||
if ($insuranceAmount > 0) {
|
||||
$apiPayload['shipment_value'] = (int) round($insuranceAmount * 100);
|
||||
$apiPayload['shipment_currency'] = strtoupper(trim((string) ($formData['insurance_currency'] ?? 'PLN')));
|
||||
}
|
||||
|
||||
if ($codAmount > 0) {
|
||||
$apiPayload['cod'] = [
|
||||
'amount' => (int) round($codAmount * 100),
|
||||
'currency' => strtoupper(trim((string) ($formData['cod_currency'] ?? 'PLN'))),
|
||||
];
|
||||
}
|
||||
|
||||
$packageId = $this->packages->create([
|
||||
'order_id' => $orderId,
|
||||
'provider' => 'apaczka',
|
||||
'delivery_method_id' => $deliveryMethodId,
|
||||
'credentials_id' => null,
|
||||
'command_id' => null,
|
||||
'status' => 'pending',
|
||||
'carrier_id' => null,
|
||||
'package_type' => strtoupper(trim((string) ($formData['package_type'] ?? 'PACKAGE'))),
|
||||
'weight_kg' => $weightKg,
|
||||
'length_cm' => $lengthCm,
|
||||
'width_cm' => $widthCm,
|
||||
'height_cm' => $heightCm,
|
||||
'insurance_amount' => $insuranceAmount > 0 ? $insuranceAmount : null,
|
||||
'insurance_currency' => strtoupper(trim((string) ($formData['insurance_currency'] ?? 'PLN'))),
|
||||
'cod_amount' => $codAmount > 0 ? $codAmount : null,
|
||||
'cod_currency' => strtoupper(trim((string) ($formData['cod_currency'] ?? 'PLN'))),
|
||||
'label_format' => 'PDF',
|
||||
'receiver_point_id' => trim((string) ($formData['receiver_point_id'] ?? '')),
|
||||
'sender_point_id' => trim((string) ($formData['sender_point_id'] ?? '')),
|
||||
'reference_number' => $sourceOrderId !== '' ? $sourceOrderId : (string) $orderId,
|
||||
'payload_json' => $apiPayload,
|
||||
]);
|
||||
|
||||
try {
|
||||
$response = $this->apiClient->sendOrder($appId, $appSecret, $apiPayload);
|
||||
} catch (Throwable $exception) {
|
||||
$errorMessage = $this->buildShipmentErrorMessage(
|
||||
$exception,
|
||||
$serviceDefinition,
|
||||
$receiverAddress,
|
||||
$senderAddress,
|
||||
$apiPayload
|
||||
);
|
||||
$this->packages->update($packageId, [
|
||||
'status' => 'error',
|
||||
'error_message' => $errorMessage,
|
||||
]);
|
||||
throw new RuntimeException($errorMessage, 0, $exception);
|
||||
}
|
||||
|
||||
$orderResponse = is_array($response['response']['order'] ?? null) ? $response['response']['order'] : [];
|
||||
$apaczkaOrderId = (int) ($orderResponse['id'] ?? 0);
|
||||
$tracking = trim((string) ($orderResponse['waybill_number'] ?? ''));
|
||||
|
||||
$this->packages->update($packageId, [
|
||||
'status' => $apaczkaOrderId > 0 ? 'created' : 'pending',
|
||||
'shipment_id' => $apaczkaOrderId > 0 ? (string) $apaczkaOrderId : null,
|
||||
'tracking_number' => $tracking !== '' ? $tracking : null,
|
||||
'payload_json' => $response,
|
||||
]);
|
||||
|
||||
return [
|
||||
'package_id' => $packageId,
|
||||
'command_id' => $apaczkaOrderId > 0 ? (string) $apaczkaOrderId : null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function checkCreationStatus(int $packageId): array
|
||||
{
|
||||
$package = $this->packages->findById($packageId);
|
||||
if ($package === null) {
|
||||
throw new RuntimeException('Paczka nie znaleziona.');
|
||||
}
|
||||
|
||||
$shipmentId = max(0, (int) ($package['shipment_id'] ?? 0));
|
||||
if ($shipmentId <= 0) {
|
||||
return ['status' => 'in_progress'];
|
||||
}
|
||||
|
||||
[$appId, $appSecret] = $this->requireCredentials();
|
||||
$details = $this->apiClient->getOrderDetails($appId, $appSecret, $shipmentId);
|
||||
$order = is_array($details['response']['order'] ?? null) ? $details['response']['order'] : [];
|
||||
$tracking = trim((string) ($order['waybill_number'] ?? ($package['tracking_number'] ?? '')));
|
||||
|
||||
$this->packages->update($packageId, [
|
||||
'status' => 'created',
|
||||
'tracking_number' => $tracking !== '' ? $tracking : null,
|
||||
'payload_json' => $details,
|
||||
]);
|
||||
|
||||
return [
|
||||
'status' => 'created',
|
||||
'shipment_id' => (string) $shipmentId,
|
||||
'tracking_number' => $tracking,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function downloadLabel(int $packageId, string $storagePath): array
|
||||
{
|
||||
$package = $this->packages->findById($packageId);
|
||||
if ($package === null) {
|
||||
throw new RuntimeException('Paczka nie znaleziona.');
|
||||
}
|
||||
|
||||
$shipmentId = max(0, (int) ($package['shipment_id'] ?? 0));
|
||||
if ($shipmentId <= 0) {
|
||||
throw new RuntimeException('Przesylka nie zostala jeszcze utworzona.');
|
||||
}
|
||||
|
||||
[$appId, $appSecret] = $this->requireCredentials();
|
||||
try {
|
||||
$response = $this->apiClient->getWaybill($appId, $appSecret, $shipmentId);
|
||||
} catch (Throwable $exception) {
|
||||
$message = trim($exception->getMessage());
|
||||
if ($this->isLabelUnavailableError($message)) {
|
||||
$this->packages->update($packageId, [
|
||||
'status' => 'error',
|
||||
'error_message' => $message,
|
||||
]);
|
||||
}
|
||||
throw $exception;
|
||||
}
|
||||
|
||||
$waybillRaw = $response['response']['waybill'] ?? null;
|
||||
$base64 = '';
|
||||
if (is_string($waybillRaw)) {
|
||||
$base64 = trim($waybillRaw);
|
||||
} elseif (is_array($waybillRaw)) {
|
||||
$base64 = trim((string) ($waybillRaw['data'] ?? ''));
|
||||
}
|
||||
if ($base64 === '') {
|
||||
throw new RuntimeException('Apaczka nie zwrocila danych etykiety.');
|
||||
}
|
||||
|
||||
$binary = base64_decode($base64, true);
|
||||
if (!is_string($binary) || $binary === '') {
|
||||
throw new RuntimeException('Nie mozna odczytac etykiety Apaczka.');
|
||||
}
|
||||
|
||||
$dir = rtrim($storagePath, '/\\') . '/labels';
|
||||
if (!is_dir($dir)) {
|
||||
mkdir($dir, 0775, true);
|
||||
}
|
||||
|
||||
$filename = 'label_' . $packageId . '_' . $shipmentId . '.pdf';
|
||||
$filePath = $dir . '/' . $filename;
|
||||
file_put_contents($filePath, $binary);
|
||||
|
||||
$this->packages->update($packageId, [
|
||||
'status' => 'label_ready',
|
||||
'label_path' => 'labels/' . $filename,
|
||||
'payload_json' => $response,
|
||||
]);
|
||||
|
||||
return [
|
||||
'label_path' => 'labels/' . $filename,
|
||||
'full_path' => $filePath,
|
||||
];
|
||||
}
|
||||
|
||||
private function isLabelUnavailableError(string $message): bool
|
||||
{
|
||||
$normalized = strtolower(trim($message));
|
||||
if ($normalized === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return str_contains($normalized, 'label is not available for this order');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{0: string, 1: string}
|
||||
*/
|
||||
private function requireCredentials(): array
|
||||
{
|
||||
$credentials = $this->integrationRepository->getApiCredentials();
|
||||
$appId = trim((string) ($credentials['app_id'] ?? ''));
|
||||
$appSecret = trim((string) ($credentials['app_secret'] ?? ''));
|
||||
|
||||
if ($appId === '' || $appSecret === '') {
|
||||
throw new RuntimeException('Brak konfiguracji Apaczka (app_id/app_secret).');
|
||||
}
|
||||
|
||||
return [$appId, $appSecret];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed>|null $orderDetails
|
||||
* @param array<string, mixed> $formData
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function buildReceiverAddress(
|
||||
?array $orderDetails,
|
||||
array $formData,
|
||||
string $appId,
|
||||
string $appSecret
|
||||
): array
|
||||
{
|
||||
$addresses = is_array($orderDetails['addresses'] ?? null) ? $orderDetails['addresses'] : [];
|
||||
$deliveryAddr = null;
|
||||
$customerAddr = null;
|
||||
foreach ($addresses as $addr) {
|
||||
$type = (string) ($addr['address_type'] ?? '');
|
||||
if ($type === 'delivery') {
|
||||
$deliveryAddr = $addr;
|
||||
}
|
||||
if ($type === 'customer') {
|
||||
$customerAddr = $addr;
|
||||
}
|
||||
}
|
||||
|
||||
$delivery = is_array($deliveryAddr) ? $deliveryAddr : [];
|
||||
$customer = is_array($customerAddr) ? $customerAddr : [];
|
||||
$deliveryStreet = $this->composeStreetLine($delivery);
|
||||
$customerStreet = $this->composeStreetLine($customer);
|
||||
$pickupMeta = $this->parsePickupMeta(
|
||||
trim((string) ($delivery['parcel_name'] ?? '')),
|
||||
trim((string) ($delivery['parcel_external_id'] ?? ''))
|
||||
);
|
||||
|
||||
$receiverPointId = trim((string) ($formData['receiver_point_id'] ?? ($delivery['parcel_external_id'] ?? '')));
|
||||
$pointAddress = $receiverPointId !== ''
|
||||
? $this->resolvePointAddress($appId, $appSecret, $receiverPointId)
|
||||
: ['street' => '', 'postal_code' => '', 'city' => ''];
|
||||
|
||||
$name = $this->resolveStringField(
|
||||
$formData,
|
||||
'receiver_name',
|
||||
[
|
||||
$delivery['name'] ?? null,
|
||||
$customer['name'] ?? null,
|
||||
$delivery['company_name'] ?? null,
|
||||
$customer['company_name'] ?? null,
|
||||
$orderDetails['order']['customer_login'] ?? null,
|
||||
'Klient',
|
||||
]
|
||||
);
|
||||
$street = $this->resolveStringField(
|
||||
$formData,
|
||||
'receiver_street',
|
||||
[
|
||||
$deliveryStreet,
|
||||
$pickupMeta['street'] ?? null,
|
||||
$pointAddress['street'] ?? null,
|
||||
$customerStreet,
|
||||
$receiverPointId !== '' ? ('Punkt odbioru ' . $receiverPointId) : null,
|
||||
]
|
||||
);
|
||||
$city = $this->resolveStringField(
|
||||
$formData,
|
||||
'receiver_city',
|
||||
[
|
||||
$delivery['city'] ?? null,
|
||||
$pickupMeta['city'] ?? null,
|
||||
$pointAddress['city'] ?? null,
|
||||
$customer['city'] ?? null,
|
||||
]
|
||||
);
|
||||
$postalCode = $this->resolveStringField(
|
||||
$formData,
|
||||
'receiver_postal_code',
|
||||
[
|
||||
$delivery['zip_code'] ?? null,
|
||||
$pickupMeta['postal_code'] ?? null,
|
||||
$pointAddress['postal_code'] ?? null,
|
||||
$customer['zip_code'] ?? null,
|
||||
]
|
||||
);
|
||||
$countryCode = strtoupper($this->resolveStringField(
|
||||
$formData,
|
||||
'receiver_country_code',
|
||||
[$delivery['country'] ?? null, $customer['country'] ?? null, 'PL']
|
||||
));
|
||||
$phone = $this->resolveStringField(
|
||||
$formData,
|
||||
'receiver_phone',
|
||||
[$delivery['phone'] ?? null, $customer['phone'] ?? null]
|
||||
);
|
||||
$email = $this->resolveStringField(
|
||||
$formData,
|
||||
'receiver_email',
|
||||
[$delivery['email'] ?? null, $customer['email'] ?? null]
|
||||
);
|
||||
|
||||
if ($receiverPointId !== '') {
|
||||
if ($street === '') {
|
||||
$street = 'Punkt odbioru ' . $receiverPointId;
|
||||
}
|
||||
if ($city === '') {
|
||||
$city = $this->resolveStringField($formData, 'receiver_city', [$customer['city'] ?? null, 'Warszawa']);
|
||||
}
|
||||
if ($postalCode === '') {
|
||||
$postalCode = $this->resolveStringField($formData, 'receiver_postal_code', [$customer['zip_code'] ?? null, '00-000']);
|
||||
}
|
||||
}
|
||||
|
||||
if ($name === '' || $street === '' || $city === '' || $postalCode === '' || $countryCode === '') {
|
||||
throw new RuntimeException('Brak wymaganych danych adresowych odbiorcy.');
|
||||
}
|
||||
|
||||
$receiver = [
|
||||
'name' => $name,
|
||||
'line1' => $street,
|
||||
'postal_code' => $postalCode,
|
||||
'city' => $city,
|
||||
'country_code' => $countryCode,
|
||||
];
|
||||
$contactPerson = $this->resolveStringField(
|
||||
$formData,
|
||||
'receiver_contact_person',
|
||||
[$name, $customer['name'] ?? null, $delivery['name'] ?? null]
|
||||
);
|
||||
if ($contactPerson !== '') {
|
||||
$receiver['contact_person'] = $contactPerson;
|
||||
$receiver['person'] = $contactPerson;
|
||||
}
|
||||
if ($receiverPointId !== '') {
|
||||
$this->applyPointIdentifiers($receiver, $receiverPointId);
|
||||
}
|
||||
|
||||
if ($phone !== '') {
|
||||
$receiver['phone'] = $phone;
|
||||
}
|
||||
if ($email !== '') {
|
||||
$receiver['email'] = $email;
|
||||
}
|
||||
|
||||
return $receiver;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $address
|
||||
*/
|
||||
private function applyPointIdentifiers(array &$address, string $pointId): void
|
||||
{
|
||||
$normalizedPointId = trim($pointId);
|
||||
if ($normalizedPointId === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
$address['point'] = $normalizedPointId;
|
||||
$address['foreign_address_id'] = $normalizedPointId;
|
||||
$address['point_id'] = $normalizedPointId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{street:string,postal_code:string,city:string}
|
||||
*/
|
||||
private function resolvePointAddress(string $appId, string $appSecret, string $pointId): array
|
||||
{
|
||||
$normalizedPointId = strtoupper(trim($pointId));
|
||||
if ($normalizedPointId === '') {
|
||||
return ['street' => '', 'postal_code' => '', 'city' => ''];
|
||||
}
|
||||
if (isset($this->pointAddressCache[$normalizedPointId])) {
|
||||
return $this->pointAddressCache[$normalizedPointId];
|
||||
}
|
||||
|
||||
$types = ['parcel_locker', 'pickup_point'];
|
||||
foreach ($types as $type) {
|
||||
try {
|
||||
$points = $this->apiClient->getPoints($appId, $appSecret, $type);
|
||||
} catch (Throwable) {
|
||||
continue;
|
||||
}
|
||||
foreach ($points as $point) {
|
||||
if (!is_array($point)) {
|
||||
continue;
|
||||
}
|
||||
$candidateId = strtoupper(trim((string) ($point['id'] ?? $point['point_id'] ?? $point['code'] ?? '')));
|
||||
if ($candidateId === '' || $candidateId !== $normalizedPointId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$resolved = [
|
||||
'street' => trim((string) ($point['street'] ?? $point['line1'] ?? $point['address'] ?? $point['street_name'] ?? '')),
|
||||
'postal_code' => trim((string) ($point['postal_code'] ?? $point['zip_code'] ?? $point['zip'] ?? $point['postcode'] ?? '')),
|
||||
'city' => trim((string) ($point['city'] ?? $point['town'] ?? '')),
|
||||
];
|
||||
$this->pointAddressCache[$normalizedPointId] = $resolved;
|
||||
return $resolved;
|
||||
}
|
||||
}
|
||||
|
||||
$this->pointAddressCache[$normalizedPointId] = ['street' => '', 'postal_code' => '', 'city' => ''];
|
||||
return $this->pointAddressCache[$normalizedPointId];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $address
|
||||
*/
|
||||
private function composeStreetLine(array $address): string
|
||||
{
|
||||
$street = trim((string) ($address['street_name'] ?? ''));
|
||||
$number = trim((string) ($address['street_number'] ?? ''));
|
||||
if ($street === '') {
|
||||
return '';
|
||||
}
|
||||
if ($number === '') {
|
||||
return $street;
|
||||
}
|
||||
|
||||
return trim($street . ' ' . $number);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $formData
|
||||
* @param array<int, mixed> $fallbacks
|
||||
*/
|
||||
private function resolveStringField(array $formData, string $fieldName, array $fallbacks): string
|
||||
{
|
||||
$direct = trim((string) ($formData[$fieldName] ?? ''));
|
||||
if ($direct !== '') {
|
||||
return $direct;
|
||||
}
|
||||
|
||||
foreach ($fallbacks as $fallback) {
|
||||
$value = trim((string) $fallback);
|
||||
if ($value !== '') {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{point_id:string,street:string,postal_code:string,city:string}
|
||||
*/
|
||||
private function parsePickupMeta(string $parcelName, string $parcelExternalId): array
|
||||
{
|
||||
$label = trim($parcelName);
|
||||
$pointId = trim($parcelExternalId);
|
||||
if ($label === '') {
|
||||
return [
|
||||
'point_id' => $pointId,
|
||||
'street' => '',
|
||||
'postal_code' => '',
|
||||
'city' => '',
|
||||
];
|
||||
}
|
||||
|
||||
$addressPart = $label;
|
||||
if (str_contains($label, '|')) {
|
||||
$parts = explode('|', $label, 2);
|
||||
$left = trim((string) ($parts[0] ?? ''));
|
||||
$right = trim((string) ($parts[1] ?? ''));
|
||||
if ($pointId === '' && $left !== '') {
|
||||
$pointId = $left;
|
||||
}
|
||||
if ($right !== '') {
|
||||
$addressPart = $right;
|
||||
}
|
||||
}
|
||||
|
||||
$street = '';
|
||||
$postalCode = '';
|
||||
$city = '';
|
||||
if (preg_match('/^(.*?),\s*(\d{2}-\d{3})\s+(.+)$/u', $addressPart, $matches) === 1) {
|
||||
$street = trim((string) ($matches[1] ?? ''));
|
||||
$postalCode = trim((string) ($matches[2] ?? ''));
|
||||
$city = trim((string) ($matches[3] ?? ''));
|
||||
} elseif (preg_match('/(\d{2}-\d{3})\s+(.+)$/u', $addressPart, $matches) === 1) {
|
||||
$postalCode = trim((string) ($matches[1] ?? ''));
|
||||
$city = trim((string) ($matches[2] ?? ''));
|
||||
$street = trim(preg_replace('/\s*\d{2}-\d{3}\s+.+$/u', '', $addressPart) ?? '');
|
||||
}
|
||||
|
||||
return [
|
||||
'point_id' => $pointId,
|
||||
'street' => $street,
|
||||
'postal_code' => $postalCode,
|
||||
'city' => $city,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $sender
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function normalizeSender(array $sender): array
|
||||
{
|
||||
$result = [
|
||||
'name' => trim((string) ($sender['name'] ?? $sender['company'] ?? '')),
|
||||
'line1' => trim((string) ($sender['street'] ?? '')),
|
||||
'postal_code' => trim((string) ($sender['postalCode'] ?? '')),
|
||||
'city' => trim((string) ($sender['city'] ?? '')),
|
||||
'country_code' => strtoupper(trim((string) ($sender['countryCode'] ?? 'PL'))),
|
||||
'phone' => trim((string) ($sender['phone'] ?? '')),
|
||||
'email' => trim((string) ($sender['email'] ?? '')),
|
||||
];
|
||||
$contactPerson = trim((string) ($sender['contactPerson'] ?? $sender['name'] ?? ''));
|
||||
if ($contactPerson !== '') {
|
||||
$result['contact_person'] = $contactPerson;
|
||||
$result['person'] = $contactPerson;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
private function resolveServiceDefinition(string $appId, string $appSecret, string $serviceId): ?array
|
||||
{
|
||||
if ($serviceId === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
$services = $this->apiClient->getServiceStructure($appId, $appSecret);
|
||||
} catch (Throwable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach ($services as $service) {
|
||||
if (!is_array($service)) {
|
||||
continue;
|
||||
}
|
||||
$currentServiceId = trim((string) ($service['service_id'] ?? $service['id'] ?? ''));
|
||||
if ($currentServiceId === $serviceId) {
|
||||
return $service;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed>|null $serviceDefinition
|
||||
* @param array<string, mixed> $receiverAddress
|
||||
* @param array<string, mixed> $senderAddress
|
||||
*/
|
||||
private function validateServiceRequirements(
|
||||
?array $serviceDefinition,
|
||||
array $receiverAddress,
|
||||
array $senderAddress
|
||||
): void {
|
||||
if ($serviceDefinition === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$requiresReceiverPoint = ((int) ($serviceDefinition['door_to_point'] ?? 0) === 1)
|
||||
|| ((int) ($serviceDefinition['point_to_point'] ?? 0) === 1);
|
||||
$pickupCourierMode = (int) ($serviceDefinition['pickup_courier'] ?? 0);
|
||||
$requiresSenderPoint = $pickupCourierMode === 0;
|
||||
$receiverPoint = trim((string) ($receiverAddress['foreign_address_id'] ?? ''));
|
||||
$senderPoint = trim((string) ($senderAddress['foreign_address_id'] ?? ''));
|
||||
$serviceName = trim((string) ($serviceDefinition['name'] ?? ''));
|
||||
|
||||
if ($requiresReceiverPoint && $receiverPoint === '') {
|
||||
throw new RuntimeException(
|
||||
'Wybrana usluga Apaczka (' . ($serviceName !== '' ? $serviceName : 'ID')
|
||||
. ') wymaga punktu odbioru (`receiver_point_id`).'
|
||||
);
|
||||
}
|
||||
if ($requiresSenderPoint && $senderPoint === '') {
|
||||
throw new RuntimeException(
|
||||
'Wybrana usluga Apaczka (' . ($serviceName !== '' ? $serviceName : 'ID')
|
||||
. ') wymaga punktu nadania (`sender_point_id`).'
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed>|null $serviceDefinition
|
||||
*/
|
||||
private function resolvePickupType(?array $serviceDefinition, string $senderPointId): string
|
||||
{
|
||||
$hasSenderPoint = trim($senderPointId) !== '';
|
||||
if ($hasSenderPoint) {
|
||||
return 'SELF';
|
||||
}
|
||||
|
||||
if ($serviceDefinition === null) {
|
||||
return 'COURIER';
|
||||
}
|
||||
|
||||
$pickupCourierMode = (int) ($serviceDefinition['pickup_courier'] ?? 0);
|
||||
if ($pickupCourierMode === 0) {
|
||||
return 'SELF';
|
||||
}
|
||||
|
||||
return 'COURIER';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed>|null $serviceDefinition
|
||||
* @param array<string, mixed> $formData
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function buildPickupPayload(?array $serviceDefinition, string $senderPointId, array $formData): array
|
||||
{
|
||||
$pickupType = $this->resolvePickupType($serviceDefinition, $senderPointId);
|
||||
if ($pickupType === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
$pickup = ['type' => $pickupType];
|
||||
if ($pickupType !== 'COURIER') {
|
||||
return $pickup;
|
||||
}
|
||||
|
||||
$pickupDate = trim((string) ($formData['pickup_date'] ?? ''));
|
||||
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $pickupDate) !== 1) {
|
||||
$pickupDate = date('Y-m-d');
|
||||
}
|
||||
$pickupDate = $this->normalizeCourierPickupDate($pickupDate);
|
||||
$hoursFrom = trim((string) ($formData['pickup_hours_from'] ?? ''));
|
||||
if (preg_match('/^\d{2}:\d{2}$/', $hoursFrom) !== 1) {
|
||||
$hoursFrom = '09:00';
|
||||
}
|
||||
$hoursTo = trim((string) ($formData['pickup_hours_to'] ?? ''));
|
||||
if (preg_match('/^\d{2}:\d{2}$/', $hoursTo) !== 1) {
|
||||
$hoursTo = '16:00';
|
||||
}
|
||||
[$hoursFrom, $hoursTo] = $this->normalizeCourierPickupHours($hoursFrom, $hoursTo);
|
||||
|
||||
$pickup['date'] = $pickupDate;
|
||||
$pickup['hours_from'] = $hoursFrom;
|
||||
$pickup['hours_to'] = $hoursTo;
|
||||
|
||||
return $pickup;
|
||||
}
|
||||
|
||||
private function normalizeCourierPickupDate(string $pickupDate): string
|
||||
{
|
||||
$ts = strtotime($pickupDate);
|
||||
if ($ts === false) {
|
||||
$ts = time();
|
||||
}
|
||||
|
||||
// Apaczka rejects Sunday as pickup date.
|
||||
$weekday = (int) date('N', $ts);
|
||||
if ($weekday === 7) {
|
||||
$ts = strtotime('+1 day', $ts);
|
||||
}
|
||||
|
||||
return date('Y-m-d', $ts);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{0:string,1:string}
|
||||
*/
|
||||
private function normalizeCourierPickupHours(string $hoursFrom, string $hoursTo): array
|
||||
{
|
||||
$normalizedFrom = $hoursFrom;
|
||||
$normalizedTo = $hoursTo;
|
||||
$maxTo = '16:00';
|
||||
|
||||
if ($this->compareTime($normalizedTo, $maxTo) > 0) {
|
||||
$normalizedTo = $maxTo;
|
||||
}
|
||||
if ($this->compareTime($normalizedFrom, $normalizedTo) >= 0) {
|
||||
$normalizedFrom = '09:00';
|
||||
}
|
||||
if ($this->compareTime($normalizedFrom, $normalizedTo) >= 0) {
|
||||
$normalizedFrom = '08:00';
|
||||
}
|
||||
if ($this->compareTime($normalizedFrom, $normalizedTo) >= 0) {
|
||||
$normalizedFrom = '00:00';
|
||||
}
|
||||
|
||||
return [$normalizedFrom, $normalizedTo];
|
||||
}
|
||||
|
||||
private function compareTime(string $left, string $right): int
|
||||
{
|
||||
return strcmp($left, $right);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed>|null $serviceDefinition
|
||||
* @param array<string, mixed> $receiverAddress
|
||||
* @param array<string, mixed> $senderAddress
|
||||
* @param array<string, mixed> $apiPayload
|
||||
*/
|
||||
private function buildShipmentErrorMessage(
|
||||
Throwable $exception,
|
||||
?array $serviceDefinition,
|
||||
array $receiverAddress,
|
||||
array $senderAddress,
|
||||
array $apiPayload
|
||||
): string {
|
||||
$message = trim($exception->getMessage());
|
||||
if ($message === '') {
|
||||
$message = 'Nieznany blad tworzenia przesylki Apaczka.';
|
||||
}
|
||||
|
||||
$isValuationError = stripos($message, 'Brak wyceny dla podanych parametr') !== false;
|
||||
$isPickupMethodError = stripos($message, 'Niepoprawny sposób nadania przesyłki') !== false
|
||||
|| stripos($message, 'Niepoprawny sposob nadania przesylki') !== false;
|
||||
if (!$isValuationError && !$isPickupMethodError) {
|
||||
return $message;
|
||||
}
|
||||
|
||||
$serviceId = trim((string) ($apiPayload['service_id'] ?? ''));
|
||||
$serviceName = trim((string) ($serviceDefinition['name'] ?? ''));
|
||||
$supplier = trim((string) ($serviceDefinition['supplier'] ?? ''));
|
||||
$receiverPoint = trim((string) ($receiverAddress['foreign_address_id'] ?? ''));
|
||||
$senderPoint = trim((string) ($senderAddress['foreign_address_id'] ?? ''));
|
||||
$weight = (string) ($apiPayload['shipment'][0]['weight'] ?? '0');
|
||||
$width = (string) ((int) ($apiPayload['shipment'][0]['dimension2'] ?? 0));
|
||||
$height = (string) ((int) ($apiPayload['shipment'][0]['dimension3'] ?? 0));
|
||||
$length = (string) ((int) ($apiPayload['shipment'][0]['dimension1'] ?? 0));
|
||||
$parts = [];
|
||||
$parts[] = 'service_id=' . ($serviceId !== '' ? $serviceId : '-');
|
||||
if ($serviceName !== '') {
|
||||
$parts[] = 'service_name=' . $serviceName;
|
||||
}
|
||||
if ($supplier !== '') {
|
||||
$parts[] = 'supplier=' . $supplier;
|
||||
}
|
||||
$parts[] = 'receiver_point_id=' . ($receiverPoint !== '' ? $receiverPoint : '(brak)');
|
||||
$parts[] = 'sender_point_id=' . ($senderPoint !== '' ? $senderPoint : '(brak)');
|
||||
$parts[] = 'gabaryt_cm=' . $length . 'x' . $width . 'x' . $height;
|
||||
$parts[] = 'waga_kg=' . $weight;
|
||||
$pickupType = trim((string) ($apiPayload['pickup']['type'] ?? ''));
|
||||
$pickupDate = trim((string) ($apiPayload['pickup']['date'] ?? ''));
|
||||
$pickupFrom = trim((string) ($apiPayload['pickup']['hours_from'] ?? ''));
|
||||
$pickupTo = trim((string) ($apiPayload['pickup']['hours_to'] ?? ''));
|
||||
if ($pickupType !== '') {
|
||||
$parts[] = 'pickup.type=' . $pickupType;
|
||||
}
|
||||
if ($pickupDate !== '') {
|
||||
$parts[] = 'pickup.date=' . $pickupDate;
|
||||
}
|
||||
if ($pickupFrom !== '' || $pickupTo !== '') {
|
||||
$parts[] = 'pickup.hours=' . ($pickupFrom !== '' ? $pickupFrom : '-') . '-' . ($pickupTo !== '' ? $pickupTo : '-');
|
||||
}
|
||||
|
||||
$hint = 'Sprawdz zgodnosc typu uslugi z typem punktu oraz uzupelnij wymagane punkty nadania/odbioru.';
|
||||
if ($isPickupMethodError) {
|
||||
$hint = 'Apaczka odrzucila sposob nadania. Dla uslug punktowych uzupelnij `sender_point_id` '
|
||||
. 'lub wybierz usluge z odbiorem przez kuriera (door_to_point).';
|
||||
}
|
||||
if ($receiverPoint !== '' && str_starts_with(strtoupper($receiverPoint), 'POP-') && strtoupper($supplier) === 'INPOST') {
|
||||
$hint = 'Prefiks punktu (`POP-`) moze byc mapowany roznie zaleznie od konfiguracji przewoznika. '
|
||||
. 'Jesli to punkt InPost, zignoruj ta sugestie.';
|
||||
}
|
||||
|
||||
return $message . ' Diagnostyka: ' . implode('; ', $parts) . '. ' . $hint;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $sender
|
||||
*/
|
||||
private function validateSenderAddress(array $sender): void
|
||||
{
|
||||
$required = ['street', 'city', 'postalCode', 'phone', 'email'];
|
||||
foreach ($required as $field) {
|
||||
if (trim((string) ($sender[$field] ?? '')) === '') {
|
||||
throw new RuntimeException('Uzupelnij dane nadawcy w Ustawienia > Dane firmy (brak: ' . $field . ').');
|
||||
}
|
||||
}
|
||||
|
||||
$name = trim((string) ($sender['name'] ?? ''));
|
||||
$company = trim((string) ($sender['company'] ?? ''));
|
||||
if ($name === '' && $company === '') {
|
||||
throw new RuntimeException('Uzupelnij dane nadawcy w Ustawienia > Dane firmy (brak nazwy/firmy).');
|
||||
}
|
||||
$contactPerson = trim((string) ($sender['contactPerson'] ?? $name ?? ''));
|
||||
if ($contactPerson === '') {
|
||||
throw new RuntimeException('Uzupelnij dane nadawcy w Ustawienia > Dane firmy (brak osoby kontaktowej).');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,8 +10,9 @@ use App\Core\Security\Csrf;
|
||||
use App\Core\View\Template;
|
||||
use App\Modules\Auth\AuthService;
|
||||
use App\Modules\Orders\OrdersRepository;
|
||||
use App\Modules\Settings\AllegroDeliveryMethodMappingRepository;
|
||||
use App\Modules\Settings\CarrierDeliveryMethodMappingRepository;
|
||||
use App\Modules\Settings\CompanySettingsRepository;
|
||||
use RuntimeException;
|
||||
use Throwable;
|
||||
|
||||
final class ShipmentController
|
||||
@@ -22,10 +23,10 @@ final class ShipmentController
|
||||
private readonly AuthService $auth,
|
||||
private readonly OrdersRepository $ordersRepository,
|
||||
private readonly CompanySettingsRepository $companySettings,
|
||||
private readonly AllegroShipmentService $shipmentService,
|
||||
private readonly ShipmentProviderRegistry $providerRegistry,
|
||||
private readonly ShipmentPackageRepository $packageRepository,
|
||||
private readonly string $storagePath,
|
||||
private readonly ?AllegroDeliveryMethodMappingRepository $deliveryMappings = null
|
||||
private readonly ?CarrierDeliveryMethodMappingRepository $deliveryMappings = null
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -55,17 +56,33 @@ final class ShipmentController
|
||||
}
|
||||
}
|
||||
|
||||
$receiverAddr = $deliveryAddr ?? $customerAddr ?? [];
|
||||
$receiverAddr = $this->buildReceiverAddress($deliveryAddr, $customerAddr);
|
||||
$preferences = is_array($order['preferences_json'] ?? null)
|
||||
? $order['preferences_json']
|
||||
: (is_string($order['preferences_json'] ?? null) ? (json_decode($order['preferences_json'], true) ?: []) : []);
|
||||
|
||||
$deliveryServices = [];
|
||||
$apaczkaServices = [];
|
||||
$deliveryServicesError = '';
|
||||
try {
|
||||
$deliveryServices = $this->shipmentService->getDeliveryServices();
|
||||
} catch (Throwable $exception) {
|
||||
$deliveryServicesError = $exception->getMessage();
|
||||
|
||||
$allegroProvider = $this->providerRegistry->get('allegro_wza');
|
||||
if ($allegroProvider !== null) {
|
||||
try {
|
||||
$deliveryServices = $allegroProvider->getDeliveryServices();
|
||||
} catch (Throwable $exception) {
|
||||
$deliveryServicesError = $exception->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
$apaczkaProvider = $this->providerRegistry->get('apaczka');
|
||||
if ($apaczkaProvider !== null) {
|
||||
try {
|
||||
$apaczkaServices = $apaczkaProvider->getDeliveryServices();
|
||||
} catch (Throwable $exception) {
|
||||
if ($deliveryServicesError === '') {
|
||||
$deliveryServicesError = $exception->getMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$inpostServices = array_values(array_filter(
|
||||
@@ -78,9 +95,26 @@ final class ShipmentController
|
||||
unset($_SESSION['shipment_flash_success'], $_SESSION['shipment_flash_error']);
|
||||
|
||||
$deliveryMapping = null;
|
||||
$deliveryMappingDiagnostic = '';
|
||||
$orderCarrierName = trim((string) ($order['external_carrier_id'] ?? ''));
|
||||
if ($orderCarrierName !== '' && $this->deliveryMappings !== null) {
|
||||
$deliveryMapping = $this->deliveryMappings->findByOrderMethod($orderCarrierName);
|
||||
$source = strtolower(trim((string) ($order['source'] ?? '')));
|
||||
$sourceIntegrationId = $source === 'shoppro'
|
||||
? max(0, (int) ($order['integration_id'] ?? 0))
|
||||
: 0;
|
||||
if ($orderCarrierName !== '' && $this->deliveryMappings !== null && in_array($source, ['allegro', 'shoppro'], true)) {
|
||||
$deliveryMapping = $this->deliveryMappings->findByOrderMethod($source, $sourceIntegrationId, $orderCarrierName);
|
||||
if ($deliveryMapping === null) {
|
||||
$hasMappingsForSource = $this->deliveryMappings->hasMappingsForSource($source, $sourceIntegrationId);
|
||||
if (!$hasMappingsForSource && $source === 'shoppro' && $sourceIntegrationId > 0) {
|
||||
$deliveryMappingDiagnostic = 'Brak mapowan form dostawy dla tej instancji shopPRO (ID integracji: '
|
||||
. $sourceIntegrationId
|
||||
. ').';
|
||||
} elseif (!$hasMappingsForSource) {
|
||||
$deliveryMappingDiagnostic = 'Brak skonfigurowanych mapowan form dostawy dla tego zrodla zamowienia.';
|
||||
} else {
|
||||
$deliveryMappingDiagnostic = 'Brak mapowania dla metody dostawy: ' . $orderCarrierName . '.';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$html = $this->template->render('shipments/prepare', [
|
||||
@@ -96,11 +130,13 @@ final class ShipmentController
|
||||
'preferences' => $preferences,
|
||||
'company' => $company,
|
||||
'deliveryServices' => $deliveryServices,
|
||||
'apaczkaServices' => $apaczkaServices,
|
||||
'deliveryServicesError' => $deliveryServicesError,
|
||||
'existingPackages' => $existingPackages,
|
||||
'flashSuccess' => $flashSuccess,
|
||||
'flashError' => $flashError,
|
||||
'deliveryMapping' => $deliveryMapping,
|
||||
'deliveryMappingDiagnostic' => $deliveryMappingDiagnostic,
|
||||
'inpostServices' => $inpostServices,
|
||||
], 'layouts/app');
|
||||
|
||||
@@ -125,7 +161,17 @@ final class ShipmentController
|
||||
$actorName = ($actorName !== null && $actorName !== '') ? $actorName : null;
|
||||
|
||||
try {
|
||||
$result = $this->shipmentService->createShipment($orderId, [
|
||||
$providerCode = strtolower(trim((string) $request->input('provider_code', 'allegro_wza')));
|
||||
if ($providerCode === 'inpost') {
|
||||
$providerCode = 'allegro_wza';
|
||||
}
|
||||
$provider = $this->providerRegistry->get($providerCode);
|
||||
if ($provider === null) {
|
||||
throw new RuntimeException('Nieznany provider przesylek: ' . $providerCode);
|
||||
}
|
||||
|
||||
$result = $provider->createShipment($orderId, [
|
||||
'provider_code' => $providerCode,
|
||||
'delivery_method_id' => (string) $request->input('delivery_method_id', ''),
|
||||
'credentials_id' => (string) $request->input('credentials_id', ''),
|
||||
'carrier_id' => (string) $request->input('carrier_id', ''),
|
||||
@@ -155,8 +201,8 @@ final class ShipmentController
|
||||
$this->ordersRepository->recordActivity(
|
||||
$orderId,
|
||||
'shipment_created',
|
||||
'Zlecono utworzenie przesylki WZA (ID paczki: ' . $packageId . ')',
|
||||
['package_id' => $packageId, 'command_id' => $result['command_id'] ?? null],
|
||||
'Zlecono utworzenie przesylki (' . $providerCode . ', ID paczki: ' . $packageId . ')',
|
||||
['package_id' => $packageId, 'command_id' => $result['command_id'] ?? null, 'provider' => $providerCode],
|
||||
'user',
|
||||
$actorName
|
||||
);
|
||||
@@ -185,14 +231,25 @@ final class ShipmentController
|
||||
}
|
||||
|
||||
try {
|
||||
$result = $this->shipmentService->checkCreationStatus($packageId);
|
||||
$package = $this->packageRepository->findById($packageId);
|
||||
if ($package === null) {
|
||||
return Response::json(['error' => 'Not found'], 404);
|
||||
}
|
||||
|
||||
$providerCode = strtolower(trim((string) ($package['provider'] ?? 'allegro_wza')));
|
||||
$provider = $this->providerRegistry->get($providerCode);
|
||||
if ($provider === null) {
|
||||
return Response::json(['status' => 'error', 'error' => 'Brak providera: ' . $providerCode]);
|
||||
}
|
||||
|
||||
$result = $provider->checkCreationStatus($packageId);
|
||||
|
||||
if (($result['status'] ?? '') === 'created') {
|
||||
try {
|
||||
$this->shipmentService->downloadLabel($packageId, $this->storagePath);
|
||||
$provider->downloadLabel($packageId, $this->storagePath);
|
||||
$result['status'] = 'label_ready';
|
||||
} catch (Throwable) {
|
||||
// label generation failed – return created so user can retry manually
|
||||
// label generation failed, user can retry manually
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,10 +278,20 @@ final class ShipmentController
|
||||
$actorName = ($actorName !== null && $actorName !== '') ? $actorName : null;
|
||||
|
||||
try {
|
||||
$result = $this->shipmentService->downloadLabel($packageId, $this->storagePath);
|
||||
$package = $this->packageRepository->findById($packageId);
|
||||
if ($package === null) {
|
||||
throw new RuntimeException('Paczka nie znaleziona.');
|
||||
}
|
||||
|
||||
$providerCode = strtolower(trim((string) ($package['provider'] ?? 'allegro_wza')));
|
||||
$provider = $this->providerRegistry->get($providerCode);
|
||||
if ($provider === null) {
|
||||
throw new RuntimeException('Brak providera: ' . $providerCode);
|
||||
}
|
||||
|
||||
$result = $provider->downloadLabel($packageId, $this->storagePath);
|
||||
$fullPath = (string) ($result['full_path'] ?? '');
|
||||
if ($fullPath !== '' && file_exists($fullPath)) {
|
||||
$package = $this->packageRepository->findById($packageId);
|
||||
$labelFormat = strtoupper(trim((string) ($package['label_format'] ?? 'PDF')));
|
||||
$contentType = $labelFormat === 'ZPL' ? 'application/octet-stream' : 'application/pdf';
|
||||
$filename = basename($fullPath);
|
||||
@@ -233,7 +300,7 @@ final class ShipmentController
|
||||
$orderId,
|
||||
'shipment_label_downloaded',
|
||||
'Pobrano etykiete dla przesylki #' . $packageId,
|
||||
['package_id' => $packageId, 'filename' => $filename],
|
||||
['package_id' => $packageId, 'filename' => $filename, 'provider' => $providerCode],
|
||||
'user',
|
||||
$actorName
|
||||
);
|
||||
@@ -263,4 +330,43 @@ final class ShipmentController
|
||||
|
||||
return Response::redirect('/orders/' . $orderId . '/shipment/prepare');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed>|null $deliveryAddr
|
||||
* @param array<string, mixed>|null $customerAddr
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function buildReceiverAddress(?array $deliveryAddr, ?array $customerAddr): array
|
||||
{
|
||||
$delivery = is_array($deliveryAddr) ? $deliveryAddr : [];
|
||||
$customer = is_array($customerAddr) ? $customerAddr : [];
|
||||
if ($delivery === []) {
|
||||
return $customer;
|
||||
}
|
||||
|
||||
$result = $delivery;
|
||||
$deliveryName = trim((string) ($delivery['name'] ?? ''));
|
||||
$customerName = trim((string) ($customer['name'] ?? ''));
|
||||
if (($this->isPickupPointDelivery($delivery) || $deliveryName === '') && $customerName !== '') {
|
||||
$result['name'] = $customerName;
|
||||
}
|
||||
if (trim((string) ($result['phone'] ?? '')) === '' && trim((string) ($customer['phone'] ?? '')) !== '') {
|
||||
$result['phone'] = $customer['phone'];
|
||||
}
|
||||
if (trim((string) ($result['email'] ?? '')) === '' && trim((string) ($customer['email'] ?? '')) !== '') {
|
||||
$result['email'] = $customer['email'];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $deliveryAddr
|
||||
*/
|
||||
private function isPickupPointDelivery(array $deliveryAddr): bool
|
||||
{
|
||||
$parcelId = trim((string) ($deliveryAddr['parcel_external_id'] ?? ''));
|
||||
$parcelName = trim((string) ($deliveryAddr['parcel_name'] ?? ''));
|
||||
return $parcelId !== '' || $parcelName !== '';
|
||||
}
|
||||
}
|
||||
|
||||
30
src/Modules/Shipments/ShipmentProviderInterface.php
Normal file
30
src/Modules/Shipments/ShipmentProviderInterface.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Shipments;
|
||||
|
||||
interface ShipmentProviderInterface
|
||||
{
|
||||
public function code(): string;
|
||||
|
||||
/**
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
public function getDeliveryServices(): array;
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $formData
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function createShipment(int $orderId, array $formData): array;
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function checkCreationStatus(int $packageId): array;
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function downloadLabel(int $packageId, string $storagePath): array;
|
||||
}
|
||||
40
src/Modules/Shipments/ShipmentProviderRegistry.php
Normal file
40
src/Modules/Shipments/ShipmentProviderRegistry.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Shipments;
|
||||
|
||||
final class ShipmentProviderRegistry
|
||||
{
|
||||
/**
|
||||
* @var array<string, ShipmentProviderInterface>
|
||||
*/
|
||||
private array $providers = [];
|
||||
|
||||
/**
|
||||
* @param iterable<int, ShipmentProviderInterface> $providers
|
||||
*/
|
||||
public function __construct(iterable $providers)
|
||||
{
|
||||
foreach ($providers as $provider) {
|
||||
$this->providers[$provider->code()] = $provider;
|
||||
}
|
||||
}
|
||||
|
||||
public function get(string $code): ?ShipmentProviderInterface
|
||||
{
|
||||
$key = strtolower(trim($code));
|
||||
if ($key === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->providers[$key] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, ShipmentProviderInterface>
|
||||
*/
|
||||
public function all(): array
|
||||
{
|
||||
return $this->providers;
|
||||
}
|
||||
}
|
||||
474
tools/apaczka_probe_order.php
Normal file
474
tools/apaczka_probe_order.php
Normal file
@@ -0,0 +1,474 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Debug helper for probing Apaczka order_send payload variants for a single order.
|
||||
*
|
||||
* Usage examples:
|
||||
* php tools/apaczka_probe_order.php --order-id=21 --use-remote
|
||||
* php tools/apaczka_probe_order.php --order-id=21 --service-id=41 --use-remote
|
||||
*/
|
||||
|
||||
const DEFAULT_ORDER_ID = 0;
|
||||
const DEFAULT_TIMEOUT_SECONDS = 30;
|
||||
|
||||
$options = parseOptions($argv);
|
||||
if (($options['order_id'] ?? 0) <= 0) {
|
||||
fwrite(STDERR, "Missing --order-id=<int>\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$basePath = dirname(__DIR__);
|
||||
registerAutoloader($basePath);
|
||||
$env = parse_ini_file($basePath . '/.env');
|
||||
if (!is_array($env)) {
|
||||
fwrite(STDERR, "Cannot read .env\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
[$pdo, $hostUsed] = openDatabase($env, !empty($options['use_remote']));
|
||||
$order = fetchOrder($pdo, (int) $options['order_id']);
|
||||
if ($order === null) {
|
||||
fwrite(STDERR, "Order not found: " . (int) $options['order_id'] . "\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$addresses = fetchAddresses($pdo, (int) $order['id']);
|
||||
$sender = fetchSenderAddress($pdo);
|
||||
$mapping = fetchDeliveryMapping($pdo, $order);
|
||||
|
||||
[$appId, $appSecret] = fetchApaczkaCredentials($pdo, $env);
|
||||
$client = new App\Modules\Settings\ApaczkaApiClient();
|
||||
$services = $client->getServiceStructure($appId, $appSecret);
|
||||
$serviceIndex = buildServiceIndex($services);
|
||||
|
||||
$basePayload = buildBasePayload($order, $addresses, $sender, $mapping);
|
||||
$candidates = buildCandidates($basePayload, $mapping, $serviceIndex, $options);
|
||||
|
||||
echo "DB host: {$hostUsed}\n";
|
||||
echo "Order: #" . (int) $order['id'] . " source=" . (string) ($order['source'] ?? '') . " source_order_id=" . (string) ($order['source_order_id'] ?? '') . "\n";
|
||||
echo "Mapped service: " . (string) ($mapping['provider_service_id'] ?? '(none)') . " (" . (string) ($mapping['provider_service_name'] ?? '') . ")\n";
|
||||
echo "Receiver point: " . (string) ($basePayload['receiver_point_id'] ?? '(none)') . "\n";
|
||||
echo "Candidates: " . count($candidates) . "\n\n";
|
||||
|
||||
$attempt = 0;
|
||||
$success = false;
|
||||
foreach ($candidates as $candidate) {
|
||||
$attempt++;
|
||||
$label = buildCandidateLabel($candidate, $serviceIndex);
|
||||
echo "[{$attempt}] {$label}\n";
|
||||
|
||||
$apiPayload = buildApiPayloadFromCandidate($basePayload, $candidate);
|
||||
try {
|
||||
$response = $client->sendOrder($appId, $appSecret, $apiPayload);
|
||||
$orderResponse = is_array($response['response']['order'] ?? null) ? $response['response']['order'] : [];
|
||||
$apaczkaOrderId = (string) ($orderResponse['id'] ?? '');
|
||||
$waybill = (string) ($orderResponse['waybill_number'] ?? '');
|
||||
echo " OK: order_id=" . ($apaczkaOrderId !== '' ? $apaczkaOrderId : '-') . " waybill=" . ($waybill !== '' ? $waybill : '-') . "\n";
|
||||
$success = true;
|
||||
|
||||
if (empty($options['continue_on_success'])) {
|
||||
break;
|
||||
}
|
||||
} catch (Throwable $exception) {
|
||||
echo " ERR: " . trim($exception->getMessage()) . "\n";
|
||||
}
|
||||
}
|
||||
|
||||
echo "\nResult: " . ($success ? "SUCCESS" : "NO SUCCESS") . "\n";
|
||||
exit($success ? 0 : 2);
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
function parseOptions(array $argv): array
|
||||
{
|
||||
$options = [
|
||||
'order_id' => DEFAULT_ORDER_ID,
|
||||
'service_id' => '',
|
||||
'use_remote' => false,
|
||||
'continue_on_success' => false,
|
||||
];
|
||||
|
||||
foreach (array_slice($argv, 1) as $arg) {
|
||||
if (preg_match('/^--order-id=(\d+)$/', (string) $arg, $m) === 1) {
|
||||
$options['order_id'] = (int) $m[1];
|
||||
continue;
|
||||
}
|
||||
if (preg_match('/^--service-id=([A-Za-z0-9_-]+)$/', (string) $arg, $m) === 1) {
|
||||
$options['service_id'] = (string) $m[1];
|
||||
continue;
|
||||
}
|
||||
if ((string) $arg === '--use-remote') {
|
||||
$options['use_remote'] = true;
|
||||
continue;
|
||||
}
|
||||
if ((string) $arg === '--continue-on-success') {
|
||||
$options['continue_on_success'] = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
function registerAutoloader(string $basePath): void
|
||||
{
|
||||
spl_autoload_register(static function (string $class) use ($basePath): void {
|
||||
$prefix = 'App\\';
|
||||
if (!str_starts_with($class, $prefix)) {
|
||||
return;
|
||||
}
|
||||
$relative = substr($class, strlen($prefix));
|
||||
$file = $basePath . '/src/' . str_replace('\\', '/', $relative) . '.php';
|
||||
if (is_file($file)) {
|
||||
require $file;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{0: PDO, 1: string}
|
||||
*/
|
||||
function openDatabase(array $env, bool $useRemote): array
|
||||
{
|
||||
$host = $useRemote
|
||||
? (string) ($env['DB_HOST_REMOTE'] ?? ($env['DB_HOST'] ?? '127.0.0.1'))
|
||||
: (string) ($env['DB_HOST'] ?? '127.0.0.1');
|
||||
$port = (string) ($env['DB_PORT'] ?? '3306');
|
||||
$db = (string) ($env['DB_DATABASE'] ?? '');
|
||||
$user = (string) ($env['DB_USERNAME'] ?? '');
|
||||
$pass = (string) ($env['DB_PASSWORD'] ?? '');
|
||||
$dsn = 'mysql:host=' . $host . ';port=' . $port . ';dbname=' . $db . ';charset=utf8mb4';
|
||||
$pdo = new PDO($dsn, $user, $pass, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
|
||||
return [$pdo, $host];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
function fetchOrder(PDO $pdo, int $orderId): ?array
|
||||
{
|
||||
$stmt = $pdo->prepare('SELECT * FROM orders WHERE id = :id LIMIT 1');
|
||||
$stmt->execute(['id' => $orderId]);
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
return is_array($row) ? $row : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
function fetchAddresses(PDO $pdo, int $orderId): array
|
||||
{
|
||||
$stmt = $pdo->prepare('SELECT * FROM order_addresses WHERE order_id = :order_id ORDER BY id ASC');
|
||||
$stmt->execute(['order_id' => $orderId]);
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
return is_array($rows) ? $rows : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
function fetchSenderAddress(PDO $pdo): array
|
||||
{
|
||||
$stmt = $pdo->prepare('SELECT * FROM company_settings WHERE id = 1 LIMIT 1');
|
||||
$stmt->execute();
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
$data = is_array($row) ? $row : [];
|
||||
|
||||
return [
|
||||
'name' => trim((string) (($data['person_name'] ?? '') !== '' ? $data['person_name'] : ($data['company_name'] ?? ''))),
|
||||
'line1' => trim((string) ($data['street'] ?? '')),
|
||||
'postal_code' => trim((string) ($data['postal_code'] ?? '')),
|
||||
'city' => trim((string) ($data['city'] ?? '')),
|
||||
'country_code' => strtoupper(trim((string) ($data['country_code'] ?? 'PL'))),
|
||||
'phone' => trim((string) ($data['phone'] ?? '')),
|
||||
'email' => trim((string) ($data['email'] ?? '')),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $order
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
function fetchDeliveryMapping(PDO $pdo, array $order): ?array
|
||||
{
|
||||
$source = strtolower(trim((string) ($order['source'] ?? '')));
|
||||
$orderMethod = trim((string) ($order['external_carrier_id'] ?? ''));
|
||||
if ($source === '' || $orderMethod === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$sourceIntegrationId = $source === 'shoppro' ? max(0, (int) ($order['integration_id'] ?? 0)) : 0;
|
||||
$stmt = $pdo->prepare(
|
||||
'SELECT *
|
||||
FROM carrier_delivery_method_mappings
|
||||
WHERE source_system = :source_system
|
||||
AND source_integration_id = :source_integration_id
|
||||
AND order_delivery_method = :order_delivery_method
|
||||
LIMIT 1'
|
||||
);
|
||||
$stmt->execute([
|
||||
'source_system' => $source,
|
||||
'source_integration_id' => $sourceIntegrationId,
|
||||
'order_delivery_method' => $orderMethod,
|
||||
]);
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
return is_array($row) ? $row : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{0: string, 1: string}
|
||||
*/
|
||||
function fetchApaczkaCredentials(PDO $pdo, array $env): array
|
||||
{
|
||||
$stmt = $pdo->prepare(
|
||||
'SELECT a.app_id,
|
||||
COALESCE(NULLIF(i.api_key_encrypted, \'\'), a.app_secret_encrypted, a.api_key_encrypted) AS secret_encrypted
|
||||
FROM apaczka_integration_settings a
|
||||
LEFT JOIN integrations i ON i.id = a.integration_id
|
||||
WHERE a.id = 1
|
||||
LIMIT 1'
|
||||
);
|
||||
$stmt->execute();
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if (!is_array($row)) {
|
||||
throw new RuntimeException('Missing apaczka_integration_settings row.');
|
||||
}
|
||||
|
||||
$appId = trim((string) ($row['app_id'] ?? ''));
|
||||
$encrypted = trim((string) ($row['secret_encrypted'] ?? ''));
|
||||
if ($appId === '' || $encrypted === '') {
|
||||
throw new RuntimeException('Missing Apaczka app_id or app_secret.');
|
||||
}
|
||||
|
||||
$cipher = new App\Modules\Settings\IntegrationSecretCipher((string) ($env['INTEGRATIONS_SECRET'] ?? ''));
|
||||
$appSecret = trim($cipher->decrypt($encrypted));
|
||||
if ($appSecret === '') {
|
||||
throw new RuntimeException('Cannot decrypt Apaczka app_secret.');
|
||||
}
|
||||
|
||||
return [$appId, $appSecret];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array<string, mixed>> $services
|
||||
* @return array<string, array<string, mixed>>
|
||||
*/
|
||||
function buildServiceIndex(array $services): array
|
||||
{
|
||||
$result = [];
|
||||
foreach ($services as $service) {
|
||||
if (!is_array($service)) {
|
||||
continue;
|
||||
}
|
||||
$serviceId = trim((string) ($service['service_id'] ?? $service['id'] ?? ''));
|
||||
if ($serviceId === '') {
|
||||
continue;
|
||||
}
|
||||
$result[$serviceId] = $service;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $order
|
||||
* @param array<int, array<string, mixed>> $addresses
|
||||
* @param array<string, string> $sender
|
||||
* @param array<string, mixed>|null $mapping
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
function buildBasePayload(array $order, array $addresses, array $sender, ?array $mapping): array
|
||||
{
|
||||
$delivery = null;
|
||||
$customer = null;
|
||||
foreach ($addresses as $address) {
|
||||
if (!is_array($address)) {
|
||||
continue;
|
||||
}
|
||||
$type = trim((string) ($address['address_type'] ?? ''));
|
||||
if ($type === 'delivery' && $delivery === null) {
|
||||
$delivery = $address;
|
||||
}
|
||||
if ($type === 'customer' && $customer === null) {
|
||||
$customer = $address;
|
||||
}
|
||||
}
|
||||
|
||||
$receiver = is_array($delivery) ? $delivery : (is_array($customer) ? $customer : []);
|
||||
$receiverPointId = trim((string) ($receiver['parcel_external_id'] ?? ''));
|
||||
$receiverName = trim((string) ($receiver['name'] ?? ''));
|
||||
$customerName = trim((string) (($customer['name'] ?? '')));
|
||||
if ($receiverPointId !== '' && $customerName !== '') {
|
||||
$receiverName = $customerName;
|
||||
}
|
||||
if ($receiverName === '') {
|
||||
$receiverName = 'Klient';
|
||||
}
|
||||
|
||||
$totalWithTax = (float) ($order['total_with_tax'] ?? 0);
|
||||
$serviceId = trim((string) ($mapping['provider_service_id'] ?? ''));
|
||||
if ($serviceId === '') {
|
||||
$serviceId = trim((string) ($order['external_carrier_account_id'] ?? ''));
|
||||
}
|
||||
|
||||
return [
|
||||
'service_id' => $serviceId,
|
||||
'source_order_id' => trim((string) ($order['source_order_id'] ?? $order['id'] ?? '')),
|
||||
'insurance_cents' => $totalWithTax > 0 ? (int) round($totalWithTax * 100) : 0,
|
||||
'receiver_point_id' => $receiverPointId,
|
||||
'sender_point_id' => '',
|
||||
'receiver' => [
|
||||
'name' => $receiverName,
|
||||
'line1' => trim((string) ($receiver['street_name'] ?? '')),
|
||||
'postal_code' => trim((string) ($receiver['zip_code'] ?? '')),
|
||||
'city' => trim((string) ($receiver['city'] ?? '')),
|
||||
'country_code' => strtoupper(trim((string) ($receiver['country'] ?? 'PL'))),
|
||||
'phone' => trim((string) ($receiver['phone'] ?? '')),
|
||||
'email' => trim((string) ($receiver['email'] ?? '')),
|
||||
],
|
||||
'sender' => $sender,
|
||||
'shipment' => [
|
||||
'shipment_type_code' => 'PACZKA',
|
||||
'dimension1' => 25,
|
||||
'dimension2' => 20,
|
||||
'dimension3' => 8,
|
||||
'weight' => 1.0,
|
||||
'is_nstd' => 0,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $basePayload
|
||||
* @param array<string, mixed>|null $mapping
|
||||
* @param array<string, array<string, mixed>> $serviceIndex
|
||||
* @param array<string, mixed> $options
|
||||
* @return array<int, array<string, string>>
|
||||
*/
|
||||
function buildCandidates(array $basePayload, ?array $mapping, array $serviceIndex, array $options): array
|
||||
{
|
||||
$serviceIds = [];
|
||||
$forcedServiceId = trim((string) ($options['service_id'] ?? ''));
|
||||
if ($forcedServiceId !== '') {
|
||||
$serviceIds[] = $forcedServiceId;
|
||||
} else {
|
||||
$mapped = trim((string) ($basePayload['service_id'] ?? ''));
|
||||
if ($mapped !== '') {
|
||||
$serviceIds[] = $mapped;
|
||||
}
|
||||
foreach ($serviceIndex as $serviceId => $service) {
|
||||
$supplier = strtoupper(trim((string) ($service['supplier'] ?? '')));
|
||||
$doorToPoint = (int) ($service['door_to_point'] ?? 0) === 1;
|
||||
$pointToPoint = (int) ($service['point_to_point'] ?? 0) === 1;
|
||||
if ($supplier !== 'INPOST') {
|
||||
continue;
|
||||
}
|
||||
if (!$doorToPoint && !$pointToPoint) {
|
||||
continue;
|
||||
}
|
||||
$serviceIds[] = $serviceId;
|
||||
}
|
||||
}
|
||||
|
||||
$serviceIds = array_values(array_unique(array_filter($serviceIds, static fn(string $v): bool => $v !== '')));
|
||||
if ($serviceIds === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$pointModes = ['all_keys', 'point_only', 'foreign_only', 'point_id_only'];
|
||||
$result = [];
|
||||
foreach ($serviceIds as $serviceId) {
|
||||
foreach ($pointModes as $mode) {
|
||||
$result[] = [
|
||||
'service_id' => $serviceId,
|
||||
'point_mode' => $mode,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $candidate
|
||||
* @param array<string, array<string, mixed>> $serviceIndex
|
||||
*/
|
||||
function buildCandidateLabel(array $candidate, array $serviceIndex): string
|
||||
{
|
||||
$serviceId = (string) ($candidate['service_id'] ?? '');
|
||||
$pointMode = (string) ($candidate['point_mode'] ?? '');
|
||||
$service = $serviceIndex[$serviceId] ?? null;
|
||||
$serviceName = is_array($service) ? trim((string) ($service['name'] ?? '')) : '';
|
||||
return 'service=' . $serviceId
|
||||
. ($serviceName !== '' ? ' (' . $serviceName . ')' : '')
|
||||
. ' point_mode=' . $pointMode;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $basePayload
|
||||
* @param array<string, string> $candidate
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
function buildApiPayloadFromCandidate(array $basePayload, array $candidate): array
|
||||
{
|
||||
$serviceId = (string) ($candidate['service_id'] ?? '');
|
||||
$pointMode = (string) ($candidate['point_mode'] ?? 'all_keys');
|
||||
$receiverPointId = trim((string) ($basePayload['receiver_point_id'] ?? ''));
|
||||
$senderPointId = trim((string) ($basePayload['sender_point_id'] ?? ''));
|
||||
|
||||
$receiver = (array) ($basePayload['receiver'] ?? []);
|
||||
$sender = (array) ($basePayload['sender'] ?? []);
|
||||
unset($receiver['point'], $receiver['foreign_address_id'], $receiver['point_id']);
|
||||
unset($sender['point'], $sender['foreign_address_id'], $sender['point_id']);
|
||||
|
||||
applyPointMode($receiver, $receiverPointId, $pointMode);
|
||||
if ($senderPointId !== '') {
|
||||
applyPointMode($sender, $senderPointId, $pointMode);
|
||||
}
|
||||
|
||||
$payload = [
|
||||
'service_id' => ctype_digit($serviceId) ? (int) $serviceId : $serviceId,
|
||||
'address' => [
|
||||
'receiver' => $receiver,
|
||||
'sender' => $sender,
|
||||
],
|
||||
'shipment' => [(array) ($basePayload['shipment'] ?? [])],
|
||||
'content' => 'orderPRO ' . (string) ($basePayload['source_order_id'] ?? ''),
|
||||
'comment' => 'orderPRO ' . (string) ($basePayload['source_order_id'] ?? ''),
|
||||
];
|
||||
$insuranceCents = (int) ($basePayload['insurance_cents'] ?? 0);
|
||||
if ($insuranceCents > 0) {
|
||||
$payload['shipment_value'] = $insuranceCents;
|
||||
}
|
||||
|
||||
return $payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $node
|
||||
*/
|
||||
function applyPointMode(array &$node, string $pointId, string $mode): void
|
||||
{
|
||||
if ($pointId === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($mode === 'point_only') {
|
||||
$node['point'] = $pointId;
|
||||
return;
|
||||
}
|
||||
if ($mode === 'foreign_only') {
|
||||
$node['foreign_address_id'] = $pointId;
|
||||
return;
|
||||
}
|
||||
if ($mode === 'point_id_only') {
|
||||
$node['point_id'] = $pointId;
|
||||
return;
|
||||
}
|
||||
|
||||
$node['point'] = $pointId;
|
||||
$node['foreign_address_id'] = $pointId;
|
||||
$node['point_id'] = $pointId;
|
||||
}
|
||||
Reference in New Issue
Block a user