Merge branch 'main' of https://git.project-pro.pl/Project-Pro/orderPRO
# Conflicts: # .paul/PROJECT.md # .paul/ROADMAP.md # .paul/STATE.md # .paul/codebase/tech_changelog.md # resources/lang/pl.php # resources/views/shipments/prepare.php # routes/web.php # src/Modules/Settings/IntegrationsHubController.php # src/Modules/Shipments/ShipmentController.php
This commit is contained in:
@@ -130,6 +130,10 @@ Sprzedawca może obsługiwać zamówienia ze wszystkich kanałów
|
|||||||
- [x] Import zamowien Erli: pobieranie `/inbox` przez cron i recznie, mapper do orderPRO, delta-only re-import, `invoice_requested` z danych firmowych/NIP, bezpieczny ACK `/inbox/mark-read` po bezblednym batchu — Phase 128
|
- [x] Import zamowien Erli: pobieranie `/inbox` przez cron i recznie, mapper do orderPRO, delta-only re-import, `invoice_requested` z danych firmowych/NIP, bezpieczny ACK `/inbox/mark-read` po bezblednym batchu — Phase 128
|
||||||
- [x] Mapowanie i synchronizacja statusow Erli: osobne pull/push mappings, discovery statusow z inboxa, reczny-only push `PATCH /orders/{id}/status`, cron `erli_status_sync` i zakladki w ustawieniach Erli — Phase 129
|
- [x] Mapowanie i synchronizacja statusow Erli: osobne pull/push mappings, discovery statusow z inboxa, reczny-only push `PATCH /orders/{id}/status`, cron `erli_status_sync` i zakladki w ustawieniach Erli — Phase 129
|
||||||
- [x] Przesylki Erli: zakladka mapowania dostaw, etykiety przez lokalne providery InPost/Apaczka i rejestracja paczek zewnetrznych w Erli przez `POST /shipping/external` — Phase 130
|
- [x] Przesylki Erli: zakladka mapowania dostaw, etykiety przez lokalne providery InPost/Apaczka i rejestracja paczek zewnetrznych w Erli przez `POST /shipping/external` — Phase 130
|
||||||
|
- [x] Integracja polkurier.pl (fundament): pojedyncza globalna konfiguracja w `/settings/integrations/polkurier`, szyfrowany Token API + login, karta w hubie integracji obok Apaczki i realny test polaczenia przez `apimetod=test_auth_api` zweryfikowany na zywym koncie operatora; `ShipmentProviderRegistry` netkniety — `PolkurierShipmentService/TrackingService` w kolejnych fazach — Phase 127
|
||||||
|
- [x] polkurier ShipmentService + TrackingService + UI prepare panel: pelen kontrakt API (createShipment/getLabel/getStatus/cancelOrder/getAvailableCarriers), `PolkurierShipmentService` implementujacy `ShipmentProviderInterface` z normalizacja shipmenttype (lowercase) i splitem ulicy na street/housenumber/flatnumber, `PolkurierTrackingService` mapujacy statusy O/P/A/WP/D/Z/W na znormalizowane, panel "polkurier" w `prepare.php` z dynamiczna lista uslug z `available_carriers`, seed migracja `delivery_status_mappings(provider='polkurier')` z 7 wpisami z PDF v1.11; live test na #114/#115 zakonczony sukcesem po 4 iteracjach (ReferenceError → uppercase shipmenttype → orderno parsing → A4/A6); rozmiar etykiety sterowany w panelu klienta polkurier.pl (Ustawienia konta → Preferencje etykiet), NIE przez API — Phase 128
|
||||||
|
- [x] Order User Notes module (Phase 129): pelen CRUD notatek autorskich operatora per zamowienie. Reuse `order_notes` przez nowy `note_type='user'` z `user_id` (FK→users SET NULL) + `author_name` (snapshot) + indeks `idx_order_notes_type_order`. `OrderNotesService` z autoryzacja DB-level (`WHERE user_id = :user_id`, rowCount=0 ⇒ 403). Sekcja `#notes` w "Wiadomosci i zalaczniki" w `/orders/{id}` z inline edit form + delete przez `OrderProAlerts.confirm`. Badge `[N]` (indigo neutralny) przy nr zamowienia na `/orders/list` (subquery `user_notes_count` w paginate). Brak admin override (brak systemu rol w aplikacji) — edit/delete tylko dla autora — Phase 129
|
||||||
|
- [x] polkurier delivery status mappings UI (Phase 130): polkurier jako 4. provider w dropdownie `/settings/delivery-statuses?tab=mapping`. `POLKURIER_MAP` + `POLKURIER_DESCRIPTIONS` w `DeliveryStatus.php` (7 wpisow O/P/A/WP/D/Z/W z oficjalnej dokumentacji v1.11, identyczne z migracja Phase 128 — DB seed staje sie no-op). `PROVIDERS` rozszerzone w `DeliveryStatusesController` + `DeliveryStatusMappingController`. `countAllUnmappedForBadge()` zlicza polkurier. Zero zmian w widoku (`_delivery-status-mappings-content.php` auto-iteruje po providerach z controllera) — Phase 130
|
||||||
|
|
||||||
### Deferred
|
### Deferred
|
||||||
|
|
||||||
@@ -253,6 +257,22 @@ PHP (XAMPP/Laravel), integracje z API marketplace'Ăłw (Allegro, Erli) oraz API
|
|||||||
| Erli settings korzysta z zakladek Integracja/Statusy/Ustawienia | Po dodaniu mapowan strona wymagala parytetu UX z Allegro/shopPRO | 2026-05-16 | Active |
|
| Erli settings korzysta z zakladek Integracja/Statusy/Ustawienia | Po dodaniu mapowan strona wymagala parytetu UX z Allegro/shopPRO | 2026-05-16 | Active |
|
||||||
| Erli etykiety uzywaja lokalnych providerow, a Erli dostaje paczke zewnetrzna przez `POST /shipping/external` | Operator nie chce nadawac na umowie Erli; API wspiera zewnetrzne paczki/tracking | 2026-05-16 | Active |
|
| Erli etykiety uzywaja lokalnych providerow, a Erli dostaje paczke zewnetrzna przez `POST /shipping/external` | Operator nie chce nadawac na umowie Erli; API wspiera zewnetrzne paczki/tracking | 2026-05-16 | Active |
|
||||||
| `carrier_delivery_method_mappings` przechowuje `source_vendor_code`/`source_service_id` dla Erli | Vendor Erli i lokalny provider to osobne kontrakty, nie nalezy ich mieszac w polach Apaczki/InPost | 2026-05-16 | Active |
|
| `carrier_delivery_method_mappings` przechowuje `source_vendor_code`/`source_service_id` dla Erli | Vendor Erli i lokalny provider to osobne kontrakty, nie nalezy ich mieszac w polach Apaczki/InPost | 2026-05-16 | Active |
|
||||||
|
| polkurier startuje jako jedna globalna konfiguracja (single-instance, mirror Apaczka/HostedSMS/SMSPLANET) z realnym testowym wywolaniem `apimetod=test_auth_api` | Operator ma jedno konto polkurier; fundament musi byc zweryfikowany na zywym API zanim dolozymy `PolkurierShipmentService` | 2026-05-14 | Active |
|
||||||
|
| polkurier wymaga `login + token` razem w body `authorization` (nie samego tokena) | Zweryfikowane w SDK polkurier-sdk (`Auth.php`/`Request.php`); kolumna `login VARCHAR(190)` w `polkurier_integration_settings` mimo ze PLAN tego nie wymagal — kontrakt API to dyktuje | 2026-05-14 | Active |
|
||||||
|
| polkurier API: top-level `status` === `'success'` (nie `'ok'`), tresc bledu w polu `response` envelope'a | `ResponseStatus::SUCCESS = 'success'` z `src/Type/ResponseStatus.php` SDK; bledy rzucane przez `ErrorException($response->get('response'))` w `PolkurierWebService.php`. Pattern dla wszystkich przyszlych metod polkurier API (`createShipment`, `getLabel`, `getStatus`, `cancelOrder`, etc.) | 2026-05-14 | Active |
|
||||||
|
| polkurier API odrzuca `Content-Type` z parametrem (`application/json; charset=UTF-8`) — wymagany dokladnie `application/json` | Strict equality check po stronie polkuriera; pattern do reuse jezeli inne integracje sa rownie strict | 2026-05-14 | Active |
|
||||||
|
| polkurier dziala obok Apaczki (nie zamiast) | Decyzja operatora — oba dostawcy zyja niezaleznie, `ShipmentProviderRegistry` rejestruje obu (Apaczka netknieta w Phase 127; polkurier dodany w nastepnej fazie razem z `PolkurierShipmentService`) | 2026-05-14 | Active |
|
||||||
|
| polkurier `shipmenttype` wymaga lowercase z zbioru `[box, envelope, palette, small_parcel, parcel_size_20]` | API odrzuca uppercase `BOX` (komunikat: "Typ paczki musi przyjmowac jeden z parametrow ze zbioru ..."). `normalizeShipmentType()` w `PolkurierShipmentService` mapuje legacy PACKAGE/BOX/PARCEL/PACZKA/KOPERTA/PALETA na format polkuriera z aliasami i defaultem `box`. | 2026-05-14 | Active |
|
||||||
|
| polkurier `create_order` zwraca Order entity z polem `number` (nie `orderno`) i `waybills[0].number` | SDK Order.php uzywa setNumber()/addWaybill() — JSON shape entity, nie parametrow input. `extractOrderNumber` priorytetuje `number`, fallback na `orderno`/`order_no`/`order_number`/`order_id`/`id` + obsluga wrapperow `{order:{...}}` i list. `extractTrackingNumber` priorytetuje `waybills[0].number`. Pattern do reuse dla innych metod polkurier SDK. | 2026-05-14 | Active |
|
||||||
|
| polkurier API nie ma parametru rozmiaru etykiety (A4/A6) | Zweryfikowane na PDF v1.11: `get_label` przyjmuje wylacznie `orderno: Array<String>`, `create_order` nie ma pola format/size. Rozmiar sterowany w panelu klienta polkurier.pl (Ustawienia konta → Preferencje etykiet) — operator zmienia preferencje konta jednorazowo. `polkurier_integration_settings.default_label_format` (PDF/ZPL/EPL) odnosi sie tylko do typu pliku, NIE rozmiaru. | 2026-05-14 | Active |
|
||||||
|
| Brak dedykowanego selektora punktow paczkomatowych w UI polkurier (Phase 128) | Istnieje juz pole `name="receiver_point_id"` w sekcji Adres odbiorcy z auto-fillem `parcel_external_id` z importu zamowienia — operator wpisuje ID recznie (np. `POP-RZE54`). Usuniete: `lookupPickupPoints`/`ShipmentController::polkurierPoints`/AJAX route/JS handler. `getInpostParcelMachines`/`getCourierPoints` zachowane jako stuby w API client — gotowe dla kolejnej fazy paczkomaty UI. | 2026-05-14 | Active |
|
||||||
|
| Diagnostyka silent-fail w ShipmentService — zapis surowej odpowiedzi do `error_message` | Gdy parsing API odpowiedzi zwraca pusty wynik mimo `status=success` (np. nieznany shape pola order number), zapisuj fragment surowej odpowiedzi (400 znakow) do `shipment_packages.error_message` — widoczne operatorowi w UI bez podgladu serwerowych logow. Pattern uratowal 3. iteracje live testu Phase 128. Reuse dla nowych integracji z API o nieznanym shape odpowiedzi. | 2026-05-14 | Active |
|
||||||
|
| `order_notes` jako jedna tabela dla notatek importowanych ze zrodla i autorskich operatora (Phase 129) | Reuse istniejacej tabeli przez nowy `note_type='user'` z `user_id`/`author_name` — mniej obiektow DB, jeden punkt zarzadzania. UNIQUE `(order_id, source_note_id)` nadal dziala bo MySQL traktuje wiele NULL jako unique (user notes maja `source_note_id=NULL`). `loadOrderNotes()` zawezone do `note_type <> 'user'`; notatki autorskie ladowane przez `OrderNotesService::listUserNotes()`. | 2026-05-14 | Active |
|
||||||
|
| Autoryzacja CRUD przez `WHERE user_id = :user_id` + rowCount=0 ⇒ `RuntimeException(403)` (Phase 129) | Eliminacja konieczności osobnego SELECT pre-check'a — atomowy UPDATE/DELETE z filtrem user_id robi to w jednym query. Wzorzec do reuse dla innych zasobow "ownership-based" w aplikacji. | 2026-05-14 | Active |
|
||||||
|
| Brak admin override dla notatek (Phase 129) — tylko autor edit/delete | Aplikacja nie ma systemu rol (`grep is_admin\|role=` zwrocil 0 trafien). Odlozone do osobnej fazy gdy beda role; obecnie operator ktory dodal notatke moze ja modyfikowac, inni widzą ale nie modyfikują. | 2026-05-14 | Deferred |
|
||||||
|
| Badge `[N]` w `order_ref` przy nr zamowienia (Phase 129) — neutralny indigo, NIE alertowy | Subtelniejszy niz `.risk-return-badge` (czerwony, alertowy) — notatki to informacja, nie ostrzezenie. Klik scrolluje do `#notes` w szczegolach zamowienia. Pattern do reuse dla kolejnych metryk per-order (np. liczba SMS, liczba dokumentow). | 2026-05-14 | Active |
|
||||||
|
| Provider-addition recipe dla `/settings/delivery-statuses?tab=mapping` (Phase 130) | 5 punktow edycji w 4 plikach: (1) const definition `XXX_MAP`/`XXX_DESCRIPTIONS` w `DeliveryStatus.php`, (2) rejestracja w `PROVIDER_MAPS`/`PROVIDER_DESCRIPTIONS`, (3) match arms w `normalize()`/`description()`, (4) `PROVIDERS` const w `DeliveryStatusesController` + `DeliveryStatusMappingController`, (5) lista providerow w `DeliveryStatusMappingRepository::countAllUnmappedForBadge()`. Widok `_delivery-status-mappings-content.php` automatycznie iteruje. Pattern do reuse dla kazdego nowego przewoznika. | 2026-05-14 | Active |
|
||||||
|
| Defaultowe mapowania statusow dostawy hardcoded w kodzie (nie tylko z DB seed) | Spojnosc z InPost/Apaczka/Allegro — wszyscy maja hardcoded fallback w `DeliveryStatus.php`. UI dziala od razu po deploy, niezaleznie czy operator uruchomil migracje seed. DB override (`delivery_status_mappings`) nadal dziala dla kazdego raw statusu — pattern dual-source (kod default + DB override) zachowany. | 2026-05-14 | Active |
|
||||||
|
|
||||||
## Success Metrics
|
## Success Metrics
|
||||||
|
|
||||||
|
|||||||
@@ -73,8 +73,14 @@ Wystawianie faktur dla klientow z NIP poprzez integracje z Fakturownia (app.fakt
|
|||||||
| 124 | SMS Templates | 1/1 | Complete (2026-05-13; migration + manual SMS smoke pending operator) |
|
| 124 | SMS Templates | 1/1 | Complete (2026-05-13; migration + manual SMS smoke pending operator) |
|
||||||
| 125 | invoice_requested Import Fix (shopPRO+Allegro NIP detection, drop legacy is_invoice column) | 1/1 | Complete (2026-05-13; migration + manual smoke pending operator) |
|
| 125 | invoice_requested Import Fix (shopPRO+Allegro NIP detection, drop legacy is_invoice column) | 1/1 | Complete (2026-05-13; migration + manual smoke pending operator) |
|
||||||
| 126 | Invoice GUS Field Mapping Fix (KRS-based heuristic: JDG → name do "Imię i nazwisko", spółka → "Nazwa firmy") | 1/1 | Complete (2026-05-13; manual smoke pending operator) |
|
| 126 | Invoice GUS Field Mapping Fix (KRS-based heuristic: JDG → name do "Imię i nazwisko", spółka → "Nazwa firmy") | 1/1 | Complete (2026-05-13; manual smoke pending operator) |
|
||||||
|
| 127 | polkurier Integration Foundation (single-instance settings + Token API + realny test polaczenia; obok Apaczki) | 1/1 | Complete (2026-05-14; live API verified — `Autoryzacja: 1`) |
|
||||||
|
| 128 | polkurier ShipmentService + TrackingService + UI prepare panel + delivery_status_mappings seed (live test na #114/#115) | 1/1 | Complete (2026-05-14; live test passed po 4 iteracjach; migracja + cron tracking weryfikacja pending) |
|
||||||
|
| 129 | Order User Notes module (extend `order_notes` o user_id/author_name/note_type='user' + pelen CRUD restricted to author + badge `[N]` na liscie zamowien) | 1/1 | Complete (2026-05-14; migracja + manualny smoke pending operator) |
|
||||||
|
| 130 | polkurier delivery status mappings UI (hardcoded POLKURIER_MAP/DESCRIPTIONS + dropdown w `/settings/delivery-statuses?tab=mapping` + badge counter) | 1/1 | Complete (2026-05-14; manualny smoke pending operator) |
|
||||||
|
|
||||||
Planowane kolejne fazy v3.7 (kandydaci, do rozplanowania):
|
Planowane kolejne fazy v3.7 (kandydaci, do rozplanowania):
|
||||||
|
- polkurier TrackingService + `delivery_status_mappings` (provider='polkurier')
|
||||||
|
- polkurier paczkomaty (`InpostParcelMachines` / `PocztexPostOffices` / `Kurier48PostOffices` z SDK polkuriera)
|
||||||
- Eksport XLSX listy wystawionych faktur (analogicznie do paragonow)
|
- Eksport XLSX listy wystawionych faktur (analogicznie do paragonow)
|
||||||
- Idempotencja podwojnego POST do Fakturowni (INVOICE-IDEMP-115)
|
- Idempotencja podwojnego POST do Fakturowni (INVOICE-IDEMP-115)
|
||||||
- Event automatyzacji `invoice.created` (jezeli operator chce wysylac faktury mailem)
|
- Event automatyzacji `invoice.created` (jezeli operator chce wysylac faktury mailem)
|
||||||
|
|||||||
@@ -85,6 +85,17 @@ Branch: main
|
|||||||
- Phase 130 follow-up: uruchom `php bin/migrate.php` (dodaje `carrier_delivery_method_mappings.source_service_id/source_vendor_code`), otworz `/settings/integrations/erli?tab=delivery`, zapisz mapowanie metody Erli na InPost/Apaczka oraz vendor Erli, a potem utworz etykiete dla zamowienia Erli i potwierdz `POST /shipping/external`.
|
- Phase 130 follow-up: uruchom `php bin/migrate.php` (dodaje `carrier_delivery_method_mappings.source_service_id/source_vendor_code`), otworz `/settings/integrations/erli?tab=delivery`, zapisz mapowanie metody Erli na InPost/Apaczka oraz vendor Erli, a potem utworz etykiete dla zamowienia Erli i potwierdz `POST /shipping/external`.
|
||||||
- Phase 130 verification gap: `vendor/bin/phpunit` nie istnieje w checkoutcie, wiec test `tests/Unit/ErliExternalShipmentServiceTest.php` nie zostal uruchomiony przez PHPUnit; wykonano `php -l` i `git diff --check`.
|
- Phase 130 verification gap: `vendor/bin/phpunit` nie istnieje w checkoutcie, wiec test `tests/Unit/ErliExternalShipmentServiceTest.php` nie zostal uruchomiony przez PHPUnit; wykonano `php -l` i `git diff --check`.
|
||||||
- Phase 130 skill gap: `sonar-scanner` nie jest dostepny w PATH, wiec skan SonarQube nie zostal uruchomiony.
|
- Phase 130 skill gap: `sonar-scanner` nie jest dostepny w PATH, wiec skan SonarQube nie zostal uruchomiony.
|
||||||
|
- Phase 127 follow-up: zaplanowac kolejna faze polkurier — `PolkurierShipmentService` (CreateOrder + GetLabel + OrderValuationV2 + AvailableCarriers mapping + UI mapowan metod dostawy + presety przesylek) — fundament + zweryfikowany kontrakt API gotowy.
|
||||||
|
- Phase 127 follow-up: drugi krok — `PolkurierTrackingService` + wpisy w `delivery_status_mappings` (provider='polkurier').
|
||||||
|
- Phase 127 follow-up: po polkurier shipment service rozwazyc fazy paczkomaty (`InpostParcelMachines` / `PocztexPostOffices` / `Kurier48PostOffices` API juz dostepne w SDK polkuriera).
|
||||||
|
- Phase 128 follow-up: uruchom migracje gdy XAMPP MySQL online: `php bin/migrate.php` (seed 7 wpisow `provider='polkurier'` w `delivery_status_mappings`).
|
||||||
|
- Phase 128 follow-up: weryfikacja crona `shipment_tracking_sync` przy pierwszej zywej paczce polkurier w `in_transit` — sprawdz ze `shipment_packages.delivery_status` aktualizuje sie z `D`/`WP`/`Z` przez `DeliveryStatus::normalizeWithOverrides('polkurier', ...)`.
|
||||||
|
- Phase 128 follow-up: rozmiar etykiety A4 vs A6 sterowany jest w panelu klienta polkurier.pl (Ustawienia konta → Preferencje etykiet), NIE przez API — operator ustawil A6.
|
||||||
|
- Phase 129 follow-up: uruchom migracje gdy XAMPP MySQL online: `php bin/migrate.php` (utworzy `order_notes.user_id` + `author_name` + FK + indeks `idx_order_notes_type_order`).
|
||||||
|
- Phase 129 follow-up: manualny smoke — `/orders/{X}` → sekcja "Notatki" widoczna, dodanie notatki tworzy wiersz + wpis w `order_activity_log`. Drugi user (`session.user_id != note.user_id`) nie widzi przycisków Edytuj/Usuń; POST `/notes/{noteId}/delete` jako inny user → 403 flash.
|
||||||
|
- Phase 129 follow-up: `/orders/list` → badge `[N]` widoczny przy zamówieniach z notatkami autorskimi; klik scrolluje do `#notes` w szczegółach. Sprawdzić że badge zwrotów (Phase 106) działa równolegle.
|
||||||
|
- Phase 130 follow-up: manualny smoke `/settings/delivery-statuses?tab=mapping` → dropdown ma 4 pozycje; `?provider=polkurier` → 7 wierszy (O/P/A/WP/D/Z/W) z `is_custom=false`. Override (zapis nowego mapowania) → wiersz przechodzi w `is_custom=true`.
|
||||||
|
- Phase 130 follow-up: migracja Phase 128 (`20260514_000115_seed_polkurier_delivery_status_mappings.sql`) staje się no-op — można ją uruchomić lub pominąć, defaulty z kodu pokryją tę samą wartość.
|
||||||
|
|
||||||
## Deferred to Next Milestones
|
## Deferred to Next Milestones
|
||||||
|
|
||||||
|
|||||||
101
.paul/changelog/2026-05-14.md
Normal file
101
.paul/changelog/2026-05-14.md
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
# 2026-05-14
|
||||||
|
|
||||||
|
## Co zrobiono
|
||||||
|
|
||||||
|
- [Phase 127, Plan 01] polkurier.pl Integration Foundation — pojedyncza globalna konfiguracja brokera kurierskiego polkurier (login + Token API zaszyfrowany przez `IntegrationSecretCipher`), karta w hubie integracji obok Apaczki, realny test polaczenia przez `apimetod=test_auth_api`. Zweryfikowane na zywym koncie operatora (`Autoryzacja: 1`).
|
||||||
|
- Task 1: Migracja DDL (`polkurier_integration_settings` + seed `integrations.type='polkurier'`) + `PolkurierIntegrationRepository` (single-instance, mirror HostedSMS/SMSPLANET).
|
||||||
|
- Task 2: `PolkurierApiClient` (POST do `https://api.polkurier.pl/`, JSON envelope `{authorization, apimetod, data}`) + `PolkurierIntegrationController` + widok formularza + 3 routy + i18n.
|
||||||
|
- Task 3: Wpiecie polkuriera do `IntegrationsHubController` (`buildPolkurierRow()`, kolejnosc: po Apaczce) + aktualizacja `.paul/codebase/{db_schema,architecture,tech_changelog}.md`.
|
||||||
|
- Auto-fix (live debugging): `status='success'` zamiast `'ok'` (ResponseStatus z SDK), `Content-Type: application/json` bez charset suffix (polkurier strict), parser bledu z pola `response` envelope'a.
|
||||||
|
- Scope deviation vs PLAN: kolumna `login` dodana (API wymaga login+token), kolumna `environment` pominieta (polkurier nie ma sandbox).
|
||||||
|
|
||||||
|
## Zmienione pliki
|
||||||
|
|
||||||
|
- `database/migrations/20260514_000114_create_polkurier_integration_settings.sql`
|
||||||
|
- `src/Modules/Settings/PolkurierIntegrationRepository.php`
|
||||||
|
- `src/Modules/Settings/PolkurierApiClient.php`
|
||||||
|
- `src/Modules/Settings/PolkurierIntegrationController.php`
|
||||||
|
- `resources/views/settings/polkurier.php`
|
||||||
|
- `routes/web.php`
|
||||||
|
- `src/Modules/Settings/IntegrationsHubController.php`
|
||||||
|
- `resources/lang/pl.php`
|
||||||
|
- `.paul/codebase/db_schema.md`
|
||||||
|
- `.paul/codebase/architecture.md`
|
||||||
|
- `.paul/codebase/tech_changelog.md`
|
||||||
|
- `.paul/STATE.md`
|
||||||
|
- `.paul/ROADMAP.md`
|
||||||
|
- `.paul/phases/127-polkurier-integration-foundation/127-01-PLAN.md`
|
||||||
|
- `.paul/phases/127-polkurier-integration-foundation/127-01-SUMMARY.md`
|
||||||
|
|
||||||
|
## Co zrobiono (cd.)
|
||||||
|
|
||||||
|
- [Phase 128, Plan 01] polkurier ShipmentService + TrackingService + UI prepare panel + delivery_status_mappings seed. polkurier zarejestrowany jako 4. provider w `ShipmentProviderRegistry` (obok allegro_wza/apaczka/inpost). Operator tworzy paczki z `/orders/{id}/shipment/prepare`, etykieta A6 generowana, cron tracking gotowy do mapowania O/P/A/WP/D/Z/W → created/confirmed/cancelled/in_transit/delivered/returned/problem.
|
||||||
|
- Task 1: `PolkurierApiClient` rozszerzony z stubów Phase 127 do 7 metod (createShipment/getLabel/getStatus/cancelOrder/getAvailableCarriers/getInpostParcelMachines/getCourierPoints). Wspólny prywatny `call($apimetod, $data, $login, $token)` parsuje envelope `{status, response}`. Kontrakt zweryfikowany na oficjalnej dokumentacji PDF v1.11 (pobrana i zachowana w `.paul/phases/128-polkurier-shipment-service/polkurier-api-docs.txt`).
|
||||||
|
- Task 2: `PolkurierShipmentService` (~520 LOC, implements ShipmentProviderInterface) + `PolkurierTrackingService` (~110 LOC, implements ShipmentTrackingInterface). `normalizeShipmentType()` mapuje legacy PACKAGE/BOX/PARCEL na lowercase zbiór polkuriera. `extractOrderNumber`/`extractTrackingNumber` priorytetują SDK Order entity (`number`, `waybills[0].number`).
|
||||||
|
- Task 3: Wiring `routes/web.php` + `CronHandlerFactory` + `ShipmentController.prepare/create` (rozszerzony o `service_code`/`pickup_*` w form data). UI panel "polkurier" w `prepare.php` z dynamiczną listą usług + JS toggle. `DeliveryStatus::trackingUrl` fallback dla provider='polkurier'.
|
||||||
|
- Task 4 (checkpoint live test #114/#115): 4 iteracje — ReferenceError w JS `clearHiddenFields` → uppercase `shipmenttype` → parsing `number` vs `orderno` → A4 vs A6 etykieta. Każda iteracja autopoprawiona w tej samej sesji APPLY.
|
||||||
|
- Task 5: Migracja `20260514_000115_seed_polkurier_delivery_status_mappings.sql` z 7 wpisami z oficjalnej tabeli ORDER_STATUS PDF v1.11 (O/P/A/WP/D/Z/W). Idempotentna `ON DUPLICATE KEY UPDATE`.
|
||||||
|
- Task 6: Aktualizacja `.paul/codebase/{architecture,db_schema,tech_changelog}.md` z sekcją Phase 128.
|
||||||
|
- Scope removal vs PLAN: UI selektor punktów paczkomatowych usunięty (operator zgłosił duplikat z polem "Punkt odbioru" w sekcji Adres odbiorcy). `lookupPickupPoints` + AJAX route + JS handler usunięte. `getInpostParcelMachines`/`getCourierPoints` zostawione jako stuby na przyszłą fazę paczkomatów UI.
|
||||||
|
- Decyzja: rozmiar etykiety A4/A6 sterowany w panelu klienta polkurier.pl, NIE przez API (zweryfikowane w PDF v1.11). Operator zmienia preferencje konta jednorazowo.
|
||||||
|
|
||||||
|
## Zmienione pliki (cd.)
|
||||||
|
|
||||||
|
- `src/Modules/Settings/PolkurierApiClient.php` (rozszerzenie z stubów do 7 metod)
|
||||||
|
- `src/Modules/Shipments/PolkurierShipmentService.php` (nowy plik)
|
||||||
|
- `src/Modules/Shipments/PolkurierTrackingService.php` (nowy plik)
|
||||||
|
- `src/Modules/Shipments/DeliveryStatus.php` (fallback URL polkurier)
|
||||||
|
- `src/Modules/Shipments/ShipmentController.php` (polkurierServices + service_code/pickup_*)
|
||||||
|
- `src/Modules/Cron/CronHandlerFactory.php` (rejestracja PolkurierTrackingService)
|
||||||
|
- `routes/web.php` (rejestracja PolkurierShipmentService w registry)
|
||||||
|
- `resources/views/shipments/prepare.php` (panel polkurier + JS)
|
||||||
|
- `database/migrations/20260514_000115_seed_polkurier_delivery_status_mappings.sql` (nowy plik)
|
||||||
|
- `.paul/phases/128-polkurier-shipment-service/128-01-PLAN.md` (nowy plik)
|
||||||
|
- `.paul/phases/128-polkurier-shipment-service/128-01-SUMMARY.md` (nowy plik)
|
||||||
|
- `.paul/phases/128-polkurier-shipment-service/polkurier-api-docs.txt` (nowy plik — referencyjna doca z PDF v1.11)
|
||||||
|
|
||||||
|
## Co zrobiono (cd. — Phase 129)
|
||||||
|
|
||||||
|
- [Phase 129, Plan 01] Order User Notes module — pelen CRUD notatek autorskich operatora per zamowienie z badge `[N]` na liscie zamowien. Reuse istniejacej tabeli `order_notes` przez nowy `note_type='user'` z `user_id` (FK→users SET NULL) i `author_name` (snapshot). Sekcja `#notes` w "Wiadomosci i zalaczniki" w szczegolach zamowienia z inline edit form + delete przez `OrderProAlerts.confirm`.
|
||||||
|
- Task 1: Migracja `20260514_000116_extend_order_notes_user_authored.sql` (ADD COLUMN user_id + author_name + FK + indeks `idx_order_notes_type_order`) z idempotentnymi `INFORMATION_SCHEMA` guard'ami i DDL no-op fallback'iem.
|
||||||
|
- Task 2: `OrderNotesService` (5 metod CRUD + autoryzacja przez `WHERE user_id = :user_id`, rowCount=0 ⇒ 403). `OrdersRepository::userNotesCountSubquerySql()` + kolumna `user_notes_count` w paginate. `OrdersController::storeNote/updateNote/deleteNote` + badge HTML w `toTableRow()`. 3 nowe POST routes.
|
||||||
|
- Task 3: Sekcja `#notes` w `show.php` (3 bloki — lista user notes + form dodawania + opcjonalny block "Wiadomosci ze zrodla"). SCSS `_order-notes.scss` z `.order-notes-badge` (indigo neutralny). JS `order-notes.js` (inline edit toggle + delete confirm). 9 nowych kluczy i18n PL. `npm run build:css` rebuilt.
|
||||||
|
- Auto-fix: plan referowal nieistniejaca metode `formatOrderRow()` — wlasciwa nazwa `toTableRow()` znaleziona przez Grep "public function". Edycja zaaplikowana w wlasciwej metodzie.
|
||||||
|
- Brak admin override w CRUD (decyzja podczas planowania): aplikacja nie ma systemu rol, autoryzacja przez `note.user_id = session.user_id` — odlozone do osobnej fazy.
|
||||||
|
|
||||||
|
## Zmienione pliki (cd. — Phase 129)
|
||||||
|
|
||||||
|
- `database/migrations/20260514_000116_extend_order_notes_user_authored.sql` (nowy plik)
|
||||||
|
- `src/Modules/Orders/OrderNotesService.php` (nowy plik)
|
||||||
|
- `src/Modules/Orders/OrdersController.php` (3 nowe akcje + badge HTML)
|
||||||
|
- `src/Modules/Orders/OrdersRepository.php` (subquery `user_notes_count` + `loadOrderNotes` zawezone do `note_type <> 'user'`)
|
||||||
|
- `routes/web.php` (3 nowe routes + `OrderNotesService` instancjonowany)
|
||||||
|
- `resources/views/orders/show.php` (sekcja `#notes` + inline edit form)
|
||||||
|
- `resources/views/layouts/app.php` (script `order-notes.js`)
|
||||||
|
- `resources/lang/pl.php` (9 kluczy `orders.details.notes_user_*` + `notes_imported_title`)
|
||||||
|
- `resources/scss/modules/_order-notes.scss` (nowy plik)
|
||||||
|
- `resources/scss/app.scss` (`@use "modules/order-notes"`)
|
||||||
|
- `public/assets/js/modules/order-notes.js` (nowy plik)
|
||||||
|
- `public/assets/css/app.css` (rebuilt)
|
||||||
|
- `.paul/codebase/db_schema.md` (sekcja `order_notes` rozszerzona)
|
||||||
|
- `.paul/codebase/tech_changelog.md` (wpis Phase 129)
|
||||||
|
- `.paul/STATE.md`, `.paul/ROADMAP.md`
|
||||||
|
- `.paul/phases/129-order-user-notes/129-01-PLAN.md` (nowy plik)
|
||||||
|
- `.paul/phases/129-order-user-notes/129-01-SUMMARY.md` (nowy plik)
|
||||||
|
|
||||||
|
## Co zrobiono (cd. — Phase 130)
|
||||||
|
|
||||||
|
- [Phase 130, Plan 01] polkurier delivery status mappings UI — polkurier widoczny jako 4. provider w dropdownie `/settings/delivery-statuses?tab=mapping`. 7 oficjalnych kodow ORDER_STATUS z dokumentacji polkurier v1.11 (O/P/A/WP/D/Z/W) hardcoded w `DeliveryStatus::POLKURIER_MAP`/`POLKURIER_DESCRIPTIONS` jako defaulty (spojnie z InPost/Apaczka/Allegro). Badge "niezmapowane" w menu zlicza teraz polkurier obok innych providerow.
|
||||||
|
- Task 1: `DeliveryStatus.php` — `POLKURIER_MAP` (7 wpisow) + `POLKURIER_DESCRIPTIONS` + rejestracja w `PROVIDER_MAPS`, `PROVIDER_DESCRIPTIONS`, oraz w match expressions `normalize()`/`description()`. Wartosci identyczne z migracja Phase 128 (DB seed staje sie no-op).
|
||||||
|
- Task 2: Stale `PROVIDERS` w `DeliveryStatusesController` i `DeliveryStatusMappingController` rozszerzone o `'polkurier' => 'polkurier'`. `DeliveryStatusMappingRepository::countAllUnmappedForBadge()`: lista providerow rozszerzona z 3 do 4.
|
||||||
|
- Brak deviacji vs PLAN — wszystkie 5 punktow edycji zaaplikowane czysto, PHP lint clean na 4 plikach, runtime `getDefaultMappings('polkurier')` zwrocil oczekiwane 7 wpisow.
|
||||||
|
|
||||||
|
## Zmienione pliki (cd. — Phase 130)
|
||||||
|
|
||||||
|
- `src/Modules/Shipments/DeliveryStatus.php` (+25 linii)
|
||||||
|
- `src/Modules/Settings/DeliveryStatusesController.php` (+1)
|
||||||
|
- `src/Modules/Settings/DeliveryStatusMappingController.php` (+1)
|
||||||
|
- `src/Modules/Shipments/DeliveryStatusMappingRepository.php` (1 ↔)
|
||||||
|
- `.paul/phases/130-polkurier-delivery-status-mappings/130-01-PLAN.md` (nowy plik)
|
||||||
|
- `.paul/phases/130-polkurier-delivery-status-mappings/130-01-SUMMARY.md` (nowy plik)
|
||||||
|
- `.paul/STATE.md`, `.paul/ROADMAP.md`
|
||||||
@@ -343,6 +343,116 @@ tests/
|
|||||||
### IntegrationsHubController
|
### IntegrationsHubController
|
||||||
- Dodaje wiersz SMSPLANET do `/settings/integrations` ze statusem konfiguracji, sekretu, aktywnosci i ostatniego testu.
|
- Dodaje wiersz SMSPLANET do `/settings/integrations` ze statusem konfiguracji, sekretu, aktywnosci i ostatniego testu.
|
||||||
|
|
||||||
|
## Phase 127 — polkurier Integration Settings
|
||||||
|
|
||||||
|
### Schema
|
||||||
|
- Tabela `polkurier_integration_settings` (fixed `id=1`, `integration_id INT UNSIGNED NULL UNIQUE FK -> integrations(id) CASCADE`, `login`, `api_token_encrypted`, `default_label_format`).
|
||||||
|
- Pojedynczy rekord `integrations.type='polkurier'`, `name='polkurier'`, `base_url='https://api.polkurier.pl/'` (mirror Apaczki/HostedSMS/SMSPLANET).
|
||||||
|
- Migracja `20260514_000114_create_polkurier_integration_settings.sql` jest idempotentna (`CREATE TABLE IF NOT EXISTS` + `INSERT ... ON DUPLICATE KEY UPDATE`).
|
||||||
|
|
||||||
|
### PolkurierIntegrationRepository (`src/Modules/Settings/PolkurierIntegrationRepository.php`)
|
||||||
|
- Konstruktor `(PDO $pdo, string $secret)` — buduje wewnetrznie `IntegrationsRepository` i `IntegrationSecretCipher` (mirror `HostedSmsIntegrationRepository`).
|
||||||
|
- `getSettings()` zwraca `login`, `default_label_format`, flage `has_api_token: bool` (NIE plaintext), `is_active`, `last_test_*`.
|
||||||
|
- `saveSettings($payload)` waliduje `login` (<=190 znakow) i `default_label_format` (PDF/ZPL/EPL), szyfruje Token API; gdy token w payloadzie jest pusty -> nie nadpisuje istniejacego (BC).
|
||||||
|
- `getCredentials()` zwraca odszyfrowany `login + api_token + default_label_format` TYLKO gdy `is_active=1` i token istnieje; inaczej `null`. Konsumowane przez `PolkurierApiClient::testConnection()` i przyszly `PolkurierShipmentService`.
|
||||||
|
- `getIntegrationId()` — single source of truth dla przyszlych modulow.
|
||||||
|
|
||||||
|
### PolkurierApiClient (`src/Modules/Settings/PolkurierApiClient.php`)
|
||||||
|
- Kontrakt API zweryfikowany na podstawie oficjalnego SDK (https://github.com/Polkurier/polkurier-sdk): jedno publiczne POST endpoint `https://api.polkurier.pl/`, JSON body `{"authorization": {"login", "token"}, "apimetod": "<method>", "data": {...}}`.
|
||||||
|
- `testConnection(login, apiToken)` wywoluje `apimetod="test_auth_api"` z `data={platform: 'orderPRO', platform_version: '1.0'}`; sukces gdy `status='ok'` lub `response.authorization` niepusta.
|
||||||
|
- cURL z `SslCertificateResolver::resolve()`, `CURLOPT_TIMEOUT=$timeoutSeconds` (default 15), `CURLOPT_SSL_VERIFYPEER=true`, `Content-Type: application/json`. PHP 8.5 compatible (brak `curl_close()`).
|
||||||
|
- Stuby `createShipment()`, `getLabel()`, `getStatus()`, `cancelOrder()` rzucaja `RuntimeException("Not implemented in Phase 127")` — dolozone w kolejnych fazach.
|
||||||
|
|
||||||
|
### PolkurierIntegrationController (`src/Modules/Settings/PolkurierIntegrationController.php`)
|
||||||
|
- Endpointy: `GET /settings/integrations/polkurier`, `POST /settings/integrations/polkurier/save`, `POST /settings/integrations/polkurier/test`.
|
||||||
|
- `test` realnie wywoluje API polkurier i zapisuje wynik w `integrations.last_test_*` przez `IntegrationsRepository::updateTestResult()`.
|
||||||
|
- Flash przez legacy `Flash::set('settings_success'|'settings_error'|'polkurier_test', ...)` — spojnie z HostedSMS/SMSPLANET; renderer flash w `layouts/app.php` (Phase 120) obsluguje BC mapping przez `Flash::all()`.
|
||||||
|
- Widok `resources/views/settings/polkurier.php` uzywa wylacznie komponentu `resources/views/components/alert.php` (Phase 120 contract).
|
||||||
|
|
||||||
|
### IntegrationsHubController (Phase 127 patch)
|
||||||
|
- Dodany parametr `PolkurierIntegrationRepository $polkurier`.
|
||||||
|
- Metoda `buildPolkurierRow()` zwraca te same klucze co `buildApaczkaRow()` (`provider`, `instance`, `authorization_status`, `secret_status`, `is_active`, `last_test_at`, `configure_url`).
|
||||||
|
- Wiersz polkurier wstawiony zaraz po Apaczka (sasiednio — semantycznie oba to brokery kurierskie).
|
||||||
|
|
||||||
|
### Boundaries / co NIE zostalo dotkniete
|
||||||
|
- `ShipmentProviderRegistry` i `src/Modules/Shipments/*` — `PolkurierShipmentService` nie istnieje w Phase 127. Tworzenie przesylek, etykiety, tracking i mapowania metod dostawy beda dodane w kolejnej fazie.
|
||||||
|
- `apaczka_integration_settings`, `ApaczkaShipmentService`, `ApaczkaTrackingService` — Apaczka netknieta, dziala rownolegle.
|
||||||
|
- `delivery_status_mappings` — brak nowych wpisow `provider='polkurier'` (dolozone razem z tracking service w kolejnej fazie).
|
||||||
|
|
||||||
|
## Phase 128 — polkurier ShipmentService + Tracking + UI prepare
|
||||||
|
|
||||||
|
### PolkurierApiClient (Phase 128 extension)
|
||||||
|
- 6 nowych metod publicznych obok zachowanego `testConnection()`:
|
||||||
|
- `getAvailableCarriers($login, $token)` → `apimetod=available_carriers`. Zwraca tablice przewoznikow z polami `servicecode`, `name`, `additional_data`, `foreign_shipments`. Konsumowane przez `PolkurierShipmentService::getDeliveryServices()`.
|
||||||
|
- `createShipment($login, $token, $payload)` → `apimetod=create_order`. Payload zgodny z oficjalna doca PDF v1.11 (zweryfikowany): `shipmenttype` (lowercase: box/envelope/palette/small_parcel/parcel_size_20), `courier` (servicecode), `description`, `sender`/`recipient` (company/person/street/housenumber/flatnumber/postcode/city/email/phone/country/point_id), `packs[]` (length/width/height/weight/amount/type), `pickup` (pickupdate/pickuptimefrom/pickuptimeto/nocourierorder), opcjonalnie `COD` i `insurance`.
|
||||||
|
- `getLabel($login, $token, $orderno)` → `apimetod=get_label`. **API przyjmuje WYLACZNIE `orderno: Array<String>`** (zweryfikowane w dokumentacji PDF). Rozmiar etykiety A4 vs A6 sterowany jest w panelu klienta polkurier.pl (Ustawienia konta → Preferencje etykiet), NIE przez API. Odpowiedz: `{file: <base64 PDF>}`.
|
||||||
|
- `getStatus($login, $token, $orderno)` → `apimetod=get_status`. Zwraca `{url, status_date, status, status_code, delivered_date}`. Kody w tabeli ORDER_STATUS (O/P/A/WP/D/Z/W).
|
||||||
|
- `cancelOrder($login, $token, $orderno)` → `apimetod=cancel_order`. Zwraca `{cancellation: true}`. Nie wywolywane przez nasz kod w Phase 128 (operator anuluje w panelu polkuriera).
|
||||||
|
- `getInpostParcelMachines` + `getCourierPoints` — stuby na przyszle rozszerzenie UI (panel paczkomatow). Aktualnie nie wykorzystywane w UI (operator wpisuje `receiver_point_id` recznie w sekcji Adres odbiorcy).
|
||||||
|
- Wspolny prywatny `call($apimetod, $data, $login, $token): mixed` parsuje envelope `{status, response}`. Sukces -> zwraca tresc `response`. Blad -> rzuca `RuntimeException` z trescia z `response` (string albo zserializowany JSON dla tablic).
|
||||||
|
|
||||||
|
### PolkurierShipmentService (`src/Modules/Shipments/PolkurierShipmentService.php`)
|
||||||
|
- `final class implements ShipmentProviderInterface` (`code()='polkurier'`). DI: `PolkurierIntegrationRepository`, `PolkurierApiClient`, `ShipmentPackageRepository`, `CompanySettingsRepository`, `OrdersRepository`.
|
||||||
|
- `getDeliveryServices()` cache'uje per-request liste z `available_carriers`, normalizuje do `[{id, name, supports_pickup_point, point_courier, foreign_shipments, raw}]`. Pole `supports_pickup_point` to heurystyka po `servicecode`/`name` (paczkomat/parcel/inpost/orlen/pocztex/kurier48/punkt).
|
||||||
|
- `createShipment($orderId, $formData)` orchestruje pelny flow:
|
||||||
|
1. Walidacja: order istnieje, `service_code`/`delivery_method_id` niepusty, credentials aktywne, sender ma street/city/postcode + name|company.
|
||||||
|
2. Mapowanie `package_type` przez `normalizeShipmentType()` na zbior `[box, envelope, palette, small_parcel, parcel_size_20]` (lowercase, aliases dla PACKAGE/PARCEL/PACZKA/...).
|
||||||
|
3. Buduje `recipient` z `order_addresses` (delivery → fallback customer) + override z formularza. Splituje ulice na `street`/`housenumber`/`flatnumber` regexem (`Marszalkowska 10/5` → street="Marszalkowska", house="10", flat="5").
|
||||||
|
4. `pickup` default: `nextBusinessDay()` + 10:00-16:00, `nocourierorder=false` (override mozliwy przez formularz: `pickup_date`, `pickup_time_from`, `pickup_time_to`, `no_courier_order`).
|
||||||
|
5. `COD` jezeli `cod_amount > 0` (codtype='transfer', codbankaccount z `company_settings.bank_account` po stripowaniu nie-cyfr; throw `ShipmentException` jezeli pusty).
|
||||||
|
6. INSERT do `shipment_packages` (provider='polkurier', status='pending', payload_json z pelnym requestem).
|
||||||
|
7. Wywolanie `apiClient->createShipment()`. On success: parsing przez `extractOrderNumber()` (priorytet `number` → Order entity z SDK, fallback `orderno`/`order_no`/`order_number`/`order_id`/`id`, obsluga wrappera `{order:{...}}` i list) i `extractTrackingNumber()` (priorytet `waybills[0].number` → OrderWaybill entity, fallback top-level klucze).
|
||||||
|
8. UPDATE `shipment_packages`: `status='created'`, `shipment_id=command_id=orderno`, `tracking_number`.
|
||||||
|
9. Synchroniczna proba `downloadLabel()` (niekrytyczna — przy bledzie ignoruje, operator klikni "Pobierz" pozniej).
|
||||||
|
10. Diagnostyka: gdy `orderno=''` mimo `status=success`, zapisuje fragment surowej odpowiedzi (400 znakow) do `shipment_packages.error_message` dla debug.
|
||||||
|
- `downloadLabel($packageId, $storagePath)`: wywoluje `get_label`, parsuje `extractLabelBase64()` (priorytet klucz `file` — zweryfikowane w SDK GetLabel.php, fallback `label`/`pdf`/`data`/`content`/`zpl`/`epl`), `base64_decode`, zapis do `storage/labels/polkurier_{packageId}_{orderno}.pdf`, UPDATE `label_path`.
|
||||||
|
- `checkCreationStatus($packageId)`: graceful — przy bledzie API zwraca zachowany `tracking_number` z DB (cron sam zaktualizuje przez TrackingService).
|
||||||
|
|
||||||
|
### PolkurierTrackingService (`src/Modules/Shipments/PolkurierTrackingService.php`)
|
||||||
|
- `final class implements ShipmentTrackingInterface`. DI: `PolkurierApiClient`, `PolkurierIntegrationRepository`, `DeliveryStatusMappingRepository`.
|
||||||
|
- `supports('polkurier')`. `getDeliveryStatus($package)` woła `get_status` po `shipment_id`/`command_id` (orderno), parsuje `status_code` z `response` (z obsluga listy w response[0]).
|
||||||
|
- Mapowanie surowego `status_code` (O/P/A/WP/D/Z/W) → znormalizowany przez `DeliveryStatus::normalizeWithOverrides('polkurier', $rawStatus, $overrides)` z DB. Seed mapowan w migracji `20260514_000115`.
|
||||||
|
- Odporny na bledy: brak credentials → `null` (skip), wyjatek API → `null`, brak `status_code` → `null`. Cron nie crashuje.
|
||||||
|
|
||||||
|
### DeliveryStatus::trackingUrl (Phase 128 patch)
|
||||||
|
- Carrier_id routing (DPD/UPS/GLS/InPost/Pocztex/...) dziala dla polkuriera automatycznie przez istniejacy `matchCarrierByName($encoded, $carrier)` (carrier_id ustawiany na servicecode z polkuriera, np. "INPOST", "DPD" — pasuje do substring matchu).
|
||||||
|
- Fallback dla provider='polkurier' bez carrier matchu: `https://polkurier.pl/sledz-paczke/<tracking>`.
|
||||||
|
|
||||||
|
### Wiring
|
||||||
|
- `routes/web.php`: `new PolkurierShipmentService(...)` zarejestrowany w `ShipmentProviderRegistry` obok Apaczki/InPost/AllegroWZA.
|
||||||
|
- `src/Modules/Cron/CronHandlerFactory.php`: `new PolkurierTrackingService(...)` w `ShipmentTrackingRegistry` w `shipment_tracking_sync` handler.
|
||||||
|
- `src/Modules/Shipments/ShipmentController.php`: `prepare()` fetchuje `polkurierServices` przez registry i przekazuje do widoku. `create()` rozszerzony o `service_code`/`pickup_date`/`pickup_time_from`/`pickup_time_to` przekazywane do `createShipment()`.
|
||||||
|
|
||||||
|
### UI prepare panel (`resources/views/shipments/prepare.php`)
|
||||||
|
- Opcja "polkurier" w dropdownie `#shipment-carrier-select` (obok Allegro/InPost/Apaczka).
|
||||||
|
- `<div id="shipment-polkurier-panel">` z dynamicznym `<select id="shipment-polkurier-select">` (lista uslug z `available_carriers`).
|
||||||
|
- `<input type="hidden" name="service_code">` synchronizowany z polkurier select przez `syncPolkurierFields()`.
|
||||||
|
- Brak dedykowanego selektora punktu odbioru — operator wpisuje `receiver_point_id` w istniejacy text input w sekcji Adres odbiorcy (np. `POP-RZE54`). Format string `POP-RZE54 | Lukasiewicza 78, 35-604 Rzeszow` z importu zamowienia nie jest parsowany — operator skraca recznie.
|
||||||
|
- JS toggle widocznosci paneli rozszerzony o polkurier; `clearHiddenFields()` czysci `service_code`; `showPanel('polkurier')` ustawia `provider_code='polkurier'`.
|
||||||
|
|
||||||
|
### Rozmiar etykiety A4 vs A6
|
||||||
|
- API polkurier nie udostepnia parametru sterowania rozmiarem etykiety w `get_label` ani `create_order` (zweryfikowane w PDF v1.11).
|
||||||
|
- Domyslny rozmiar ustawiany jest w **panelu klienta polkurier.pl → Ustawienia konta → Preferencje etykiet** (per-konto, globalnie dla wszystkich `get_label` calli).
|
||||||
|
- `polkurier_integration_settings.default_label_format` (PDF/ZPL/EPL) sluzy tylko typowi pliku, NIE rozmiarowi.
|
||||||
|
|
||||||
|
### Seed delivery_status_mappings (`20260514_000115_seed_polkurier_delivery_status_mappings.sql`)
|
||||||
|
- 7 wpisow `provider='polkurier'` (kody z oficjalnej tabeli ORDER_STATUS w PDF v1.11):
|
||||||
|
- `O` → `created` (Oczekuje na platnosc)
|
||||||
|
- `P` → `confirmed` (Potwierdzone, list wygenerowany)
|
||||||
|
- `A` → `cancelled` (Anulowane)
|
||||||
|
- `WP` → `in_transit` (W przewozie)
|
||||||
|
- `D` → `delivered` (Dostarczona)
|
||||||
|
- `Z` → `returned` (Zwrot do nadawcy)
|
||||||
|
- `W` → `problem` (Wyjatek)
|
||||||
|
- Idempotentne: `ON DUPLICATE KEY UPDATE normalized_status / description / updated_at`.
|
||||||
|
|
||||||
|
### Boundaries / co NIE zostalo zmienione
|
||||||
|
- Apaczka (`ApaczkaShipmentService`, `ApaczkaTrackingService`, `apaczka_integration_settings`) niezalezna, dziala obok polkuriera.
|
||||||
|
- `ShipmentProviderInterface` i `ShipmentTrackingInterface` kontrakty niezmienione.
|
||||||
|
- `getInpostParcelMachines`/`getCourierPoints` w API client zaimplementowane ale nieuzywane przez UI w Phase 128 (operator wpisuje punkt recznie).
|
||||||
|
- `cancelOrder` zaimplementowane w API client ale nie wywolywane z UI/cron — operator anuluje w panelu polkuriera.
|
||||||
|
- Brak presetow przesylek dla polkuriera (`shipment_presets.provider_code='polkurier'`) — kolejna faza.
|
||||||
|
|
||||||
## Phase 121 - SMSPLANET Conversation + Notifications
|
## Phase 121 - SMSPLANET Conversation + Notifications
|
||||||
|
|
||||||
### SmsConversationService (`src/Modules/Sms/SmsConversationService.php`)
|
### SmsConversationService (`src/Modules/Sms/SmsConversationService.php`)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Database Schema
|
# Database Schema
|
||||||
|
|
||||||
**Updated:** 2026-05-13 | **Total tables:** 61 | **Engine:** InnoDB | **Charset:** utf8mb4_unicode_ci
|
**Updated:** 2026-05-14 | **Total tables:** 62 | **Engine:** InnoDB | **Charset:** utf8mb4_unicode_ci
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -328,6 +328,26 @@ UNIQUE: `(integration_id, external_order_id)`
|
|||||||
|
|
||||||
UNIQUE: `(order_id, source_payment_id)`
|
UNIQUE: `(order_id, source_payment_id)`
|
||||||
|
|
||||||
|
**order_notes** — Notatki przypisane do zamówienia (importowane ze źródła + autorskie operatora)
|
||||||
|
| Column | Type | Nullable | Notes |
|
||||||
|
|--------|------|----------|-------|
|
||||||
|
| `id` | BIGINT UNSIGNED | NO | PK, AUTO_INCREMENT |
|
||||||
|
| `order_id` | BIGINT UNSIGNED | NO | FK → orders(id) CASCADE |
|
||||||
|
| `source_note_id` | VARCHAR(64) | YES | ID notatki ze źródła (shopPRO/Allegro); NULL dla notatek autorskich |
|
||||||
|
| `note_type` | VARCHAR(32) | NO | `shoppro`/`allegro`/`message` (imported) lub `user` (Phase 129 — autorska notatka operatora) |
|
||||||
|
| `user_id` | INT UNSIGNED | YES | FK → users(id) ON DELETE SET NULL (Phase 129); set tylko dla `note_type='user'` |
|
||||||
|
| `author_name` | VARCHAR(190) | YES | Snapshot `users.name` w momencie tworzenia (Phase 129); chroni przed zmianą nazwy usera |
|
||||||
|
| `created_at_external` | DATETIME | YES | Data ze źródła (import); NULL dla `note_type='user'` |
|
||||||
|
| `comment` | TEXT | NO | Treść notatki (reuse dla `note_type='user'` jako body) |
|
||||||
|
| `payload_json` | JSON | YES | Raw payload ze źródła; NULL dla `note_type='user'` |
|
||||||
|
| `created_at` | DATETIME | NO | DEFAULT CURRENT_TIMESTAMP |
|
||||||
|
| `updated_at` | DATETIME | NO | DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP |
|
||||||
|
|
||||||
|
UNIQUE: `(order_id, source_note_id)` — note: MySQL traktuje wiele NULL jako unique, więc nie blokuje wielu rekordów `note_type='user'` (source_note_id zawsze NULL).
|
||||||
|
Indexes: `order_notes_order_idx (order_id)`, `idx_order_notes_type_order (note_type, order_id)` (Phase 129 — wspiera subquery `user_notes_count` na liście zamówień i `listUserNotes`).
|
||||||
|
|
||||||
|
> Note (Phase 129-01, 2026-05-14): Dodano `user_id`/`author_name` oraz `note_type='user'` dla notatek autorskich operatora. Edycja/usuwanie dozwolone tylko dla autora (`note.user_id === session.user_id`) — brak admin override (brak systemu ról w aplikacji). Importowane notatki ze źródła (`note_type IN ('shoppro','allegro','message')`) zachowują `user_id=NULL` i pozostają nieedytowalne.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Order Statuses
|
## Order Statuses
|
||||||
@@ -460,6 +480,18 @@ Indexes: `shipment_packages_order_idx`, `shipment_packages_status_idx`, `shipmen
|
|||||||
|
|
||||||
UNIQUE: `(provider, raw_status)`
|
UNIQUE: `(provider, raw_status)`
|
||||||
|
|
||||||
|
**Seedowane mapowania:**
|
||||||
|
- `provider='inpost'`, `provider='apaczka'`, `provider='allegro_wza'` — w `DeliveryStatus.php` (hardcoded fallback przez `DeliveryStatus::normalize($provider, $rawStatus)`).
|
||||||
|
- `provider='polkurier'` (Phase 128, migracja `20260514_000115_seed_polkurier_delivery_status_mappings.sql`):
|
||||||
|
- `O` → `created` (Oczekuje na platnosc)
|
||||||
|
- `P` → `confirmed` (Potwierdzone, list wygenerowany)
|
||||||
|
- `A` → `cancelled` (Anulowane)
|
||||||
|
- `WP` → `in_transit` (W przewozie)
|
||||||
|
- `D` → `delivered` (Dostarczona)
|
||||||
|
- `Z` → `returned` (Zwrot do nadawcy)
|
||||||
|
- `W` → `problem` (Wyjatek)
|
||||||
|
- Kody z oficjalnej tabeli `ORDER_STATUS` w dokumentacji API polkurier v1.11 (marzec 2026).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Integrations
|
## Integrations
|
||||||
@@ -624,6 +656,21 @@ UNIQUE: `(integration_id)` - one global SMSPLANET settings row.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
**polkurier_integration_settings** — polkurier.pl broker account credentials (Phase 127; fixed 1 row)
|
||||||
|
| Column | Type | Nullable | Notes |
|
||||||
|
|--------|------|----------|-------|
|
||||||
|
| `id` | TINYINT UNSIGNED | NO | PK, always 1 |
|
||||||
|
| `integration_id` | INT UNSIGNED | YES | UNIQUE, FK -> integrations(id) CASCADE |
|
||||||
|
| `login` | VARCHAR(190) | YES | polkurier login (e-mail z Panel Klienta) — wymagany razem z Token API w body requestu |
|
||||||
|
| `api_token_encrypted` | TEXT | YES | AES-encrypted Token API via `IntegrationSecretCipher` (z Panel Klienta -> Ustawienia -> Token API) |
|
||||||
|
| `default_label_format` | VARCHAR(8) | NO | DEFAULT 'PDF' (PDF/ZPL/EPL) — wykorzystany przez przyszly `PolkurierShipmentService` |
|
||||||
|
| `created_at` | DATETIME | NO | |
|
||||||
|
| `updated_at` | DATETIME | NO | |
|
||||||
|
|
||||||
|
UNIQUE: `(integration_id)` - one global polkurier settings row. Token zapisywany jest rownolegle do `integrations.api_key_encrypted` (mirror patternu HostedSMS/SMSPLANET).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
**sms_messages** - SMSPLANET inbound/outbound conversation history (Phase 121): stores direction, provider, nullable `order_id BIGINT UNSIGNED`, original and normalized phone endpoints, SMS body, provider `message_id`, status, raw JSON payload, optional `created_by`, and timestamps. Indexes: `(order_id, created_at)`, normalized phone columns, and `(provider, message_id)`.
|
**sms_messages** - SMSPLANET inbound/outbound conversation history (Phase 121): stores direction, provider, nullable `order_id BIGINT UNSIGNED`, original and normalized phone endpoints, SMS body, provider `message_id`, status, raw JSON payload, optional `created_by`, and timestamps. Indexes: `(order_id, created_at)`, normalized phone columns, and `(provider, message_id)`.
|
||||||
|
|
||||||
**notifications** - Global notification center (Phase 121): stores type, title, body, target URL, related order/SMS references, `read_at`, and `created_at`. Indexes support unread polling by `(read_at, created_at)` and relation lookups.
|
**notifications** - Global notification center (Phase 121): stores type, title, body, target URL, related order/SMS references, `read_at`, and `created_at`. Indexes support unread polling by `(read_at, created_at)` and relation lookups.
|
||||||
|
|||||||
@@ -11,6 +11,117 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 2026-05-14 - Phase 130 Plan 01: polkurier delivery status mappings UI
|
||||||
|
|
||||||
|
**Co zrobiono:**
|
||||||
|
- `src/Modules/Shipments/DeliveryStatus.php` — nowe stałe `POLKURIER_MAP` i `POLKURIER_DESCRIPTIONS` z 7 oficjalnymi kodami ORDER_STATUS z dokumentacji polkurier API v1.11 (`O`→`created`, `P`→`confirmed`, `A`→`cancelled`, `WP`→`in_transit`, `D`→`delivered`, `Z`→`returned`, `W`→`problem`). Wartości identyczne z migracją Phase 128 (`20260514_000115_seed_polkurier_delivery_status_mappings.sql`).
|
||||||
|
- `src/Modules/Shipments/DeliveryStatus.php` — rejestracja `'polkurier' => self::POLKURIER_MAP` w `PROVIDER_MAPS` (po `'allegro_edge'`), analogicznie w `PROVIDER_DESCRIPTIONS`, oraz w match expressions `normalize()`/`description()`. `getDefaultMappings('polkurier')` zwraca 7 wpisów.
|
||||||
|
- `src/Modules/Settings/DeliveryStatusesController.php` + `DeliveryStatusMappingController.php` — stałe `PROVIDERS` rozszerzone z 3 do 4 wpisów: `'polkurier' => 'polkurier'` (lowercase, spójne z Phase 127).
|
||||||
|
- `src/Modules/Shipments/DeliveryStatusMappingRepository.php` — `countAllUnmappedForBadge()`: lista providerów rozszerzona z `['inpost', 'apaczka', 'allegro_wza']` do `['inpost', 'apaczka', 'allegro_wza', 'polkurier']`. Badge "niezmapowane statusy" w menu Ustawień reaguje teraz na nieznane raw statusy polkuriera.
|
||||||
|
- View `_delivery-status-mappings-content.php` automatycznie iteruje po `$providersList` z controllera — żadnych zmian w widoku nie trzeba.
|
||||||
|
|
||||||
|
**Dlaczego:**
|
||||||
|
- Phase 128 zaseed-owała DB override (`delivery_status_mappings` 7 wpisów) ale UI mapowania pozostał hardcoded na 3 providerów. Operator nie miał jak zmapować/podejrzeć statusów polkuriera w panelu.
|
||||||
|
- Defaultowe mapowania hardcoded w kodzie (nie tylko z DB) — spójność z InPost/Apaczka/Allegro (wszyscy mają hardcoded fallback). UI działa od razu, niezależnie czy operator uruchomił migrację Phase 128.
|
||||||
|
- Pattern `provider addition`: 5 punktów edycji w 4 plikach (1 const definition + 2 PROVIDER_* + 2 match arms + 2× PROVIDERS controller + 1 badge providers list) — checklist do reuse dla następnych przewoźników.
|
||||||
|
|
||||||
|
**Side-effects:**
|
||||||
|
- Migracja `20260514_000115_seed_polkurier_delivery_status_mappings.sql` (Phase 128) staje się no-op po wdrożeniu Phase 130 — DB override == hardcoded default → render `is_custom=true` ale ta sama wartość. Migracja może być uruchomiona lub nie.
|
||||||
|
|
||||||
|
**Files modified:**
|
||||||
|
- `src/Modules/Shipments/DeliveryStatus.php`
|
||||||
|
- `src/Modules/Settings/DeliveryStatusesController.php`
|
||||||
|
- `src/Modules/Settings/DeliveryStatusMappingController.php`
|
||||||
|
- `src/Modules/Shipments/DeliveryStatusMappingRepository.php`
|
||||||
|
- `.paul/codebase/tech_changelog.md` (this entry)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-05-14 - Phase 129 Plan 01: Order User Notes module
|
||||||
|
|
||||||
|
**Co zrobiono:**
|
||||||
|
- Migracja `database/migrations/20260514_000116_extend_order_notes_user_authored.sql` — `order_notes` rozszerzona o `user_id INT UNSIGNED NULL` (FK → `users(id)` ON DELETE SET NULL), `author_name VARCHAR(190) NULL`, oraz indeks `idx_order_notes_type_order (note_type, order_id)`. Wszystkie ADD owinięte w `INFORMATION_SCHEMA` guard z DDL no-op fallback (`ALTER TABLE COMMENT`) — pattern Phase 115/125.
|
||||||
|
- `src/Modules/Orders/OrderNotesService.php` — nowy serwis CRUD nad `order_notes` z `note_type='user'`. Metody: `listUserNotes`, `listImportedNotes`, `countUserNotes`, `findById`, `create`, `update`, `delete`. Autoryzacja przez `WHERE user_id = :user_id` w UPDATE/DELETE — rowCount=0 ⇒ rzut `RuntimeException(code=403)`. Walidacja `body`: trim, niepuste, ≤ 2000 znakow.
|
||||||
|
- `src/Modules/Orders/OrdersRepository.php` — dodany `userNotesCountSubquerySql($orderAlias)` (subquery `COUNT(*) FROM order_notes WHERE note_type='user'`) używany w `paginateSql()` jako kolumna `user_notes_count`. `loadOrderNotes()` zawężony do `note_type <> 'user'` (importowane ze źródła). `transformOrderRow()` ekspozuje `user_notes_count`.
|
||||||
|
- `src/Modules/Orders/OrdersController.php` — nowa opcjonalna zależność `OrderNotesService` w konstruktorze (na końcu, nullable, BC-safe). 3 metody: `storeNote`, `updateNote`, `deleteNote` (każda CSRF + sesja + try/catch `RuntimeException`/`InvalidArgumentException`; rejestruje `order_activity_log event_type='note'` przez `OrdersRepository::recordActivity`). `toTableRow()` renderuje `<a class="order-notes-badge" href="/orders/{id}#notes">[N]</a>` obok numeru zamówienia gdy `user_notes_count > 0`. `show()` pobiera `userNotes` + `currentUserId` i przekazuje do widoku.
|
||||||
|
- `routes/web.php` — 3 nowe route'y `POST /orders/{id}/notes`, `POST /orders/{id}/notes/{noteId}/update`, `POST /orders/{id}/notes/{noteId}/delete`. `OrderNotesService` instancjonowany przed `new OrdersController(...)` i przekazany ostatnim argumentem.
|
||||||
|
- `resources/views/orders/show.php` — sekcja "Wiadomości i załączniki" przebudowana na 3 bloki: (1) `<div class="order-user-notes" id="notes">` z listą notatek (data · autor) + akcjami edit/delete dla autora + inline formularz dodawania, (2) ukryty `order-note-edit-form` per notatka rozwijany przez JS, (3) opcjonalny blok "Wiadomości ze źródła" gdy `$notesList !== []` (importowane, bez akcji).
|
||||||
|
- `resources/lang/pl.php` — 10 nowych kluczy `orders.details.notes_user_*` / `notes_imported_title` (UI labels w PL).
|
||||||
|
- `resources/scss/modules/_order-notes.scss` (nowy) + `@use` w `app.scss` — `.order-notes-badge` (niebieskoszary `#eef2ff/#4338ca`), `.order-user-notes`, `.order-event--user` (lewa krawędź `#6366f1`), `.order-imported-notes` (opacity 0.75), `.btn-link`, `.order-note-form`, `.order-note-edit-form`. CSS przebudowany via `npm run build:css`.
|
||||||
|
- `public/assets/js/modules/order-notes.js` (nowy) + `<script>` w `layouts/app.php` — wanilijowy JS: klik "Edytuj" toggle'uje `js-order-note-body` ↔ `js-order-note-edit-form`, klik "Anuluj" wraca, submit formularza DELETE przechwycony i potwierdzany przez `OrderProAlerts.confirm({title, message, danger:true, onConfirm})` (options-object API z decyzji Phase 114). Idempotent guard `window.__orderNotesInit` + `dataset.bound`.
|
||||||
|
- `.paul/codebase/db_schema.md` — sekcja `order_notes` rozszerzona o pełne kolumny + notatkę Phase 129-01 (note_type='user' vs imported).
|
||||||
|
|
||||||
|
**Dlaczego:**
|
||||||
|
- Operator potrzebował miejsca na własne adnotacje per zamówienie (uzgodnienia z klientem, ustalenia wewnętrzne) niezależne od notatek importowanych z shopPRO/Allegro. Bez badge'a na liście trzeba by wchodzić w każde zamówienie żeby sprawdzić czy ma notatki.
|
||||||
|
- Reuse istniejącej tabeli `order_notes` (Plan clarification #1) zamiast nowej tabeli — mniej obiektów DB, jeden punkt zarządzania, semantyka rozróżniona przez `note_type`. UNIQUE `(order_id, source_note_id)` nadal działa bo MySQL traktuje wiele NULL jako unique.
|
||||||
|
- Brak admin override (Plan clarification #3 z dopiskiem): aplikacja nie ma systemu ról (`grep -rn "is_admin|role=" src/Modules/Auth` zwrócił 0 trafień). Autoryzacja przez `note.user_id = session.user_id` — operator który dodał notatkę edytuje/usuwa, inni widzą ale nie modyfikują. Pełen admin-override odłożony do osobnej fazy po wprowadzeniu ról.
|
||||||
|
- Indeks `idx_order_notes_type_order (note_type, order_id)` zapewnia że subquery `user_notes_count` w paginacji `/orders/list` nie degraduje performance przy rosnącej liczbie notatek (Phase 106 pattern dla `customer_returned_count`).
|
||||||
|
|
||||||
|
**BREAKING:** brak — wszystkie zmiany BC. `loadOrderNotes()` teraz zwraca tylko `note_type <> 'user'`, ale nikt poza `findDetails()` jej nie używa, a sekcja widoku zachowuje wstecznie kompatybilne `$notesList` z importowanych notatek (osobny blok pod nową sekcją "Notatki").
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-05-14 - Phase 128 Plan 01: polkurier ShipmentService + Tracking + UI prepare
|
||||||
|
|
||||||
|
**Co zrobiono:**
|
||||||
|
- `src/Modules/Settings/PolkurierApiClient.php` — pelen kontrakt API: `createShipment` (`apimetod=create_order`), `getLabel` (`get_label`), `getStatus` (`get_status`), `cancelOrder` (`cancel_order`), `getAvailableCarriers` (`available_carriers`), `getInpostParcelMachines` (`inpost_parcel_machines`), `getCourierPoints` (`get_courier_point`). Wspolny prywatny `call($apimetod, $data, $login, $token): mixed` parsuje envelope `{status, response}`; sukces -> zwraca `response`, blad -> rzuca `RuntimeException` z trescia z `response` (string albo zserializowany JSON dla tablic). Kontrakt zweryfikowany na oficjalnej dokumentacji PDF v1.11 (marzec 2026) — pobrana z `https://www.polkurier.pl/files/download/api_documentation_pdf`, zachowana w `.paul/phases/128-polkurier-shipment-service/polkurier-api-docs.txt`.
|
||||||
|
- `src/Modules/Shipments/PolkurierShipmentService.php` — `final class implements ShipmentProviderInterface` (`code()='polkurier'`). Pelen flow `createShipment($orderId, $formData)`: walidacja credentials/sender, `normalizeShipmentType()` mapuje `package_type` (PACKAGE/BOX/...) na zbior polkuriera `[box,envelope,palette,small_parcel,parcel_size_20]` (lowercase wymagane przez API — odkryte podczas live testu), `splitStreetAndNumber()` rozdziela ulice regexem na `street`/`housenumber`/`flatnumber`, `buildRecipient` z payload `order_addresses` + override z formularza, `buildPickup` z domyslnym `nextBusinessDay()` + 10:00-16:00, `COD` z bank account z `company_settings`. Po sukcesie API: `extractOrderNumber` (priorytet `number` z SDK Order entity), `extractTrackingNumber` (priorytet `waybills[0].number` z OrderWaybill entity), synchroniczna proba `downloadLabel`. Diagnostyka: gdy `orderno=''`, zapisuje fragment surowej odpowiedzi do `shipment_packages.error_message`.
|
||||||
|
- `src/Modules/Shipments/PolkurierTrackingService.php` — `final class implements ShipmentTrackingInterface`. `getDeliveryStatus($package)` woła `get_status`, parsuje `status_code`, mapuje przez `DeliveryStatus::normalizeWithOverrides('polkurier', ...)` z `delivery_status_mappings`. Graceful: null przy braku credentials/wyjatku API/braku `status_code`.
|
||||||
|
- `src/Modules/Shipments/DeliveryStatus.php` — fallback URL sledzenia dla `provider='polkurier'`: `https://polkurier.pl/sledz-paczke/<tracking>`. Carrier_id routing (DPD/UPS/GLS/InPost/Pocztex) dziala automatycznie przez istniejacy `matchCarrierByName()` (carrier_id ustawiany na servicecode z polkuriera, np. "INPOST" lub "DPD" — substring match).
|
||||||
|
- `routes/web.php` — `new PolkurierShipmentService(...)` w `ShipmentProviderRegistry`. Import `App\Modules\Settings\PolkurierApiClient` + `App\Modules\Shipments\PolkurierShipmentService`.
|
||||||
|
- `src/Modules/Cron/CronHandlerFactory.php` — `new PolkurierTrackingService(new PolkurierApiClient(), new PolkurierIntegrationRepository($this->db, $this->integrationSecret), new DeliveryStatusMappingRepository($this->db))` w `ShipmentTrackingRegistry` w handlerze `shipment_tracking_sync`.
|
||||||
|
- `src/Modules/Shipments/ShipmentController.php` — `prepare()` fetchuje `polkurierServices` przez registry i przekazuje do widoku. `create()` rozszerzony o pola `service_code`/`pickup_date`/`pickup_time_from`/`pickup_time_to` przekazywane do `createShipment()`.
|
||||||
|
- `resources/views/shipments/prepare.php` — opcja "polkurier" w `#shipment-carrier-select`, panel `#shipment-polkurier-panel` z `<select id="shipment-polkurier-select">` (lista uslug z `available_carriers`), hidden `name="service_code"`, JS toggle (`showPanel`, `syncPolkurierFields`). Brak dedykowanego selektora punktu odbioru — operator wpisuje `receiver_point_id` w istniejacym text inpucie w sekcji Adres odbiorcy (np. `POP-RZE54`).
|
||||||
|
- Nowa migracja `database/migrations/20260514_000115_seed_polkurier_delivery_status_mappings.sql` — 7 wpisow `provider='polkurier'`: `O`→`created`, `P`→`confirmed`, `A`→`cancelled`, `WP`→`in_transit`, `D`→`delivered`, `Z`→`returned`, `W`→`problem`. Idempotentne (`ON DUPLICATE KEY UPDATE`). Kody z oficjalnej tabeli ORDER_STATUS PDF v1.11.
|
||||||
|
- `.paul/codebase/architecture.md` + `db_schema.md` — opisy fazy 128.
|
||||||
|
|
||||||
|
**Dlaczego:**
|
||||||
|
- Phase 127 dostarczyl fundament (settings + test_auth_api). Bez ShipmentService polkurier byl tylko `wystawiony w hubie integracji ale niedzialajacy`. Operator chcial realnie nadawac paczki przez polkurier obok Apaczki — szczegolnie dla DPD/UPS/GLS/InPost gdzie polkurier oferuje lepsze ceny.
|
||||||
|
- Pelny zakres (ShipmentService + TrackingService + UI prepare + delivery_status_mappings) w jednej fazie zgodnie z decyzja operatora z planu (clarifications, `delegation: off`, `autonomous: false` z checkpointem live testu na #114/#115).
|
||||||
|
|
||||||
|
**Live test iteracje (zarejestrowane podczas APPLY):**
|
||||||
|
1. Pierwszy submit polkurier → "Blad tworzenia przesylki: Nie podano uslugi Apaczka." Przyczyna: `ReferenceError` na zmiennej `polkurierPointIdInput` (pozostalej po usunieciu duplikatu selektora punktu) w `clearHiddenFields()` → handler `carrierSelect.change` crashowal przed `showPanel()`, `provider_code` zostawal na PHP-renderowanej wartosci `apaczka`. Fix: usuniecie martwej referencji.
|
||||||
|
2. Drugi submit → "Blad tworzenia przesylki: polkurier create_order: Typ paczki musi przyjmowac jeden z parametrow ze zbioru [box, envelope, palette, small_parcel, parcel_size_20]". Przyczyna: wysylanie `BOX` uppercase. Fix: `normalizeShipmentType()` z lowercase + aliasami.
|
||||||
|
3. Trzeci submit → utworzona w polkurier, ale w orderPRO `status=pending` (brak orderno w parsing). Przyczyna: shape odpowiedzi `create_order` zwraca `Order` entity z polem `number` (nie `orderno`). Fix: `extractOrderNumber` z priorytetem `number` + fallback list + obsluga wrappera `{order:{...}}`. Etykieta poprawnie parsowana z pola `file` (response GetLabel.php).
|
||||||
|
4. Czwarty test → etykieta A4 zamiast A6. Iteracja w bogus parametry (`format`/`label_size`/`paper_size`) wyslane do `get_label` — bez efektu, bo API ignoruje. Pobranie oficjalnej dokumentacji PDF potwierdzilo: `get_label` przyjmuje WYLACZNIE `orderno`. Rozmiar A4 vs A6 sterowany jest w panelu klienta polkurier.pl (Ustawienia konta → Preferencje etykiet), NIE przez API. Operator zmienil w panelu — etykieta A6 OK.
|
||||||
|
|
||||||
|
**Deviation vs PLAN:**
|
||||||
|
- Plan deklarowal `delegation: off` — wykonane inline. Tasks 1-3 napisane przez orchestrator, checkpoint i Task 5/6 inline. Bez sub-agentow ze wzgledu na potrzebe szybkich iteracji po live test feedback (4 iteracje przed sukcesem) — spawn agentow zdublowalby koszt research z PDF.
|
||||||
|
- Plan deklarowal AJAX endpoint `/shipments/polkurier/points` + UI selector punktow paczkomatowych (Task 3 action). USUNIETY po feedback operatora — istnieje juz pole "Punkt odbioru" w sekcji Adres odbiorcy, operator wpisuje ID recznie (np. `POP-RZE54`). Usuniety: `PolkurierShipmentService::lookupPickupPoints()`, `ShipmentController::polkurierPoints()`, route, JS handler.
|
||||||
|
- AC-3 (TrackingService cron) dostarczony, ale niezweryfikowany na zywej bazie podczas APPLY (operator anulowal paczki w panelu polkurier po teście — cron tracking nie mial co pingowac). Dziala defensywnie (graceful null przy bledach), pierwszy realny passthrough nastapi przy nastepnej zywej paczce.
|
||||||
|
- Plan zakladal seedowanie mapowan po zaobserwowaniu realnych statusow w live tescie. Seed wykonany bazujac na oficjalnej tabeli ORDER_STATUS z PDF v1.11 (kody O/P/A/WP/D/Z/W) zamiast obserwacji — bezpieczniejsze i wyczerpujace.
|
||||||
|
- AC-1 wzmiankowal getAvailableCarriers/getParcelMachines/getPostOffices. `getInpostParcelMachines` i `getCourierPoints` zaimplementowane jako stuby na przyszle rozszerzenie UI, ale nie uzywane przez aktualny UI (operator wpisuje punkt recznie). `getPostOffices` POMINIETY — brak dedykowanej metody w SDK (jest tylko `inpost_parcel_machines` per courier, `pocztex_post_offices`, `kurier48_post_offices` — zlozenie tego w UI panel paczkomatow odlozone na kolejna faze).
|
||||||
|
|
||||||
|
**Follow-up:**
|
||||||
|
- Operator musi uruchomic migracje gdy XAMPP MySQL online: `php bin/migrate.php` (utworzy 7 wpisow `provider='polkurier'`).
|
||||||
|
- Po pierwszej zywej paczce w `in_transit` — weryfikacja crona `shipment_tracking_sync` (1x ping API polkuriera, zapis `delivery_status` + `delivery_status_raw` w `shipment_packages`).
|
||||||
|
- Kolejne fazy v3.7: paczkomaty UI panel (`InpostParcelMachines`/`PocztexPostOffices`/`Kurier48PostOffices`), presety przesylek z `provider_code='polkurier'`, `OrderValuationV2` (wycena przed nadaniem).
|
||||||
|
|
||||||
|
## 2026-05-14 - Phase 127 Plan 01: polkurier Integration Foundation
|
||||||
|
|
||||||
|
**Co zrobiono:**
|
||||||
|
- Nowa migracja `database/migrations/20260514_000114_create_polkurier_integration_settings.sql` — tabela `polkurier_integration_settings` (fixed `id=1`, FK do `integrations` CASCADE, kolumny: `login VARCHAR(190)`, `api_token_encrypted TEXT`, `default_label_format VARCHAR(8) DEFAULT 'PDF'`) + idempotentny seed rekordu `integrations.type='polkurier'`, `base_url='https://api.polkurier.pl/'`.
|
||||||
|
- `src/Modules/Settings/PolkurierIntegrationRepository.php` — single-instance repository (mirror `HostedSmsIntegrationRepository`): `getSettings()` zwraca `has_api_token: bool` zamiast plaintext, `saveSettings()` szyfruje Token API przez `IntegrationSecretCipher`, `getCredentials()` gatuje na `is_active=1`, `getIntegrationId()` jako single source of truth.
|
||||||
|
- `src/Modules/Settings/PolkurierApiClient.php` — POST do `https://api.polkurier.pl/` z JSON body `{authorization:{login,token}, apimetod, data}`. Endpoint test = `apimetod="test_auth_api"`. cURL z `SslCertificateResolver::resolve()`, PHP 8.5 compatible (brak `curl_close()`). Stuby createShipment/getLabel/getStatus/cancelOrder rzucaja RuntimeException — do implementacji w kolejnych fazach.
|
||||||
|
- `src/Modules/Settings/PolkurierIntegrationController.php` — endpointy `GET /settings/integrations/polkurier`, `POST .../save`, `POST .../test` (CSRF `_token`). `test` zapisuje wynik przez `IntegrationsRepository::updateTestResult()`.
|
||||||
|
- `resources/views/settings/polkurier.php` — formularz konfiguracji + przycisk realnego testu polaczenia. Wszystkie alerty przez komponent `resources/views/components/alert.php` (Phase 120 contract).
|
||||||
|
- `src/Modules/Settings/IntegrationsHubController.php` — dodany parametr `PolkurierIntegrationRepository $polkurier` i metoda `buildPolkurierRow()`; wiersz polkurier wstawiony zaraz po Apaczka.
|
||||||
|
- `routes/web.php` — DI wiring `PolkurierIntegrationRepository` + `PolkurierIntegrationController`, rozszerzony ctor `IntegrationsHubController`, 3 nowe routy `/settings/integrations/polkurier{,/save,/test}`.
|
||||||
|
- `resources/lang/pl.php` — sekcja `settings.polkurier.*` (title/description/fields/hints/token/status/actions/flash) + `settings.integrations_hub.providers.polkurier`.
|
||||||
|
- `.paul/codebase/db_schema.md` + `architecture.md` — opisy fazy 127.
|
||||||
|
|
||||||
|
**Dlaczego:**
|
||||||
|
- Operator dostaje drugiego brokera kurierskiego rownolegle z Apaczka (decyzja w `.paul/phases/127-polkurier-integration-foundation/127-01-PLAN.md`, clarifications).
|
||||||
|
- Single-instance bo polkurier to jedno konto operatora (mirror Apaczka/InPost/HostedSMS/SMSPLANET).
|
||||||
|
- Faza zamyka tylko warstwe ustawien + realny test (`apimetod=test_auth_api`); tworzenie przesylek, etykiety, tracking i mapowania metod dostawy beda w kolejnych fazach — analogicznie do tego jak Phase 116/117 zamknely tylko fundament HostedSMS/SMSPLANET.
|
||||||
|
|
||||||
|
**Deviation vs PLAN:**
|
||||||
|
- AC-1 wymagal kolumny `environment ENUM('production','sandbox')`. polkurier nie ma srodowiska sandbox (jeden produkcyjny endpoint `https://api.polkurier.pl/`), wiec kolumna `environment` zostala POMINIETA jako YAGNI.
|
||||||
|
- AC-1/AC-2 wymagaly tylko `api_token_encrypted`. polkurier API wymaga `login + token` razem w `authorization` (zweryfikowane w oficjalnym SDK https://github.com/Polkurier/polkurier-sdk — pliki `Auth.php`/`Request.php`/`Config.php`), wiec dodana kolumna `login VARCHAR(190)` z walidacja serwerowa.
|
||||||
|
- Plan deklarowal `delegation: auto` (sub-agents). Zadania wykonane inline z powodu swiezo zgromadzonego research o API polkuriera (Config/Auth/Request/Methods z SDK); spawn agentow powtorzylby ten research. Decyzja chroni kontekst i czas. Boundaries i acceptance criteria niezmienione.
|
||||||
|
|
||||||
|
**BREAKING:** brak.
|
||||||
|
|
||||||
## 2026-05-13 - Phase 126 Plan 01: Invoice GUS Field Mapping Fix (KRS heuristic)
|
## 2026-05-13 - Phase 126 Plan 01: Invoice GUS Field Mapping Fix (KRS heuristic)
|
||||||
|
|
||||||
**Co zrobiono:**
|
**Co zrobiono:**
|
||||||
|
|||||||
56
.paul/governance/governance_2026-05-13.jsonl
Normal file
56
.paul/governance/governance_2026-05-13.jsonl
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
{"ts":"2026-05-13T07:17:55Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro -e \"SELECT id, product_name_pat","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-13T07:18:08Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro -e \"\\nSELECT \\n oi.id AS i","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-13T07:18:38Z","tool":"Bash","cmd":"sed -n '80,120p' \"tools/generowanie/pudelko_komunia_kielich.py\"\",\"description\":\"Check remaining args of pudelko_komunia_kielich.py\"},\"tool_response\":{\"stdout\":\" parser.add_argument(\"--","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-13T07:19:38Z","tool":"Bash","cmd":"tasklist //FI \"IMAGENAME eq Photoshop.exe\" 2>/dev/null | grep -i photoshop\",\"description\":\"Check if Photoshop is running\"},\"tool_response\":{\"stdout\":\"Binary file (standard input) matches\"","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-13T07:19:52Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/orderPRO\" && PYTHONIOENCODING=utf-8 python tools/generowanie/pudelko_komunia_kielich.py --klient \"Dorota Mackiewicz\" --imie \"Karoliny\" --data \"17.05.2026\" --","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-13T07:20:00Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro -e \"UPDATE order_items SET proj","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-13T09:07:14Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro -e \"\\nSELECT \\n oi.id AS i","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-13T09:07:33Z","tool":"Bash","cmd":"python tools/generowanie/email_message_fetcher.py --email \"kingawisniewskafotografia@gmail.com\" --days 60 --json 2>&1\",\"description\":\"Check client emails for Kinga Wiśniewska\"},\"tool_response","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-13T09:08:22Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/orderPRO\" && PYTHONIOENCODING=utf-8 python tools/generowanie/pudelko_komunia_dziewczynka.py --klient \"Kinga Wiśniewska\" --imie \"Kornelki\" --data \"17.05.2026\","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-13T09:08:30Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro -e \"UPDATE order_items SET proj","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-13T09:14:07Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro -e \"\\nSELECT \\n oi.id AS i","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-13T09:15:03Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/orderPRO\" && PYTHONIOENCODING=utf-8 python tools/generowanie/pudelko_komunia_dziewczynka.py \\\\\\n --klient \"Marzena Socha\" \\\\\\n --imie \"Igi\" \\\\\\n --","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-13T09:15:12Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro -e \"UPDATE order_items SET proj","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-13T09:50:43Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro -e \"\\nSELECT \\n oi.id AS i","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-13T09:50:54Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro -e \"\\nSELECT \\n oi.id AS i","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-13T09:51:03Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro -e \"\\nSELECT o.internal_order_","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-13T09:51:12Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro -e \"\\nSELECT oi.id, oi.origina","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-13T10:10:41Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro -e \"\\nSELECT \\n oi.id AS i","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-13T10:11:42Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/orderPRO\" && PYTHONIOENCODING=utf-8 python tools/generowanie/pudelko_komunia_kielich.py \\\\\\n --klient \"Paulina Lewandowska\" \\\\\\n --imie \"Antosia\" \\\\\","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-13T10:11:50Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro -e \"UPDATE order_items SET proj","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-13T10:44:25Z","tool":"Bash","cmd":"tasklist /FI \"IMAGENAME eq Photoshop.exe\" 2>/dev/null | grep -i photoshop || echo \"NIE ZNALEZIONO\"\",\"description\":\"Sprawdź czy Photoshop jest uruchomiony\"},\"tool_response\":{\"stdout\":\"NI","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-13T10:45:49Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\tools\\\\generowanie\\\\_explore_layers.py","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-13T10:48:20Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\tools\\\\generowanie\\\\_explore_layers.py","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-13T10:51:14Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\tools\\\\generowanie\\\\_prep_akrylowa_statuetka_rodzice_wzor3.py","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-13T10:52:31Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\tools\\\\generowanie\\\\akrylowa_statuetka_podziekowanie_rodzice_wzor3.py","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-13T10:55:42Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\tools\\\\generowanie\\\\_insert_mapping_wzor3.py","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-13T10:56:02Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\tools\\\\generowanie\\\\_insert_mapping_wzor3.sql","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-13T11:09:04Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\tools\\\\generowanie\\\\akrylowa_statuetka_podziekowanie_rodzice_wzor5.py","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-13T11:10:17Z","tool":"Bash","cmd":"cd \"C:\\\\visual studio code\\\\projekty\\\\orderPRO\" && git ls-files | head -30\"},\"tool_response\":{\"stdout\":\".claude/agent_credentials.md\\n.claude/commands/wygeneruj-projekty.md\\n.claude/co","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-13T11:10:23Z","tool":"Bash","cmd":"cd \"C:\\\\visual studio code\\\\projekty\\\\orderPRO\" && find . -name \"*.php\" -o -name \"*.js\" -o -name \"*.json\" -o -name \"*.env\" | grep -E \"(config|model|controller|migration)\" | head -40\","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-13T11:10:49Z","tool":"Bash","cmd":"dir \"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\tools\\\\generowanie\"\"},\"tool_response\":{\"stdout\":\"__pycache__\\n_assets\\n_batch_log.txt\\n_batch_run.sh\\n_explore_buteleczki_klaudia","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-13T11:13:26Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro -e \"SELECT id, product_name_pat","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-13T11:13:41Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro -e \"\\nSELECT \\n oi.id AS i","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-13T11:13:57Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/orderPRO\" && python tools/generowanie/email_message_fetcher.py --email \"nattalia.skrzypek@gmail.com\" --days 60 --json 2>/dev/null\",\"description\":\"Sprawdź ma","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-13T11:14:01Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/orderPRO\" && python tools/generowanie/email_photo_fetcher.py --email \"nattalia.skrzypek@gmail.com\" --out \"C:/tmp/orderpro_photos\" --days 365 2>/dev/null\",\"de","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-13T11:14:57Z","tool":"Bash","cmd":"tasklist //FI \"IMAGENAME eq Photoshop.exe\" 2>/dev/null | grep -i photoshop || echo \"BRAK\"\",\"description\":\"Sprawdź czy Photoshop jest uruchomiony\"},\"tool_response\":{\"stdout\":\"Binary file","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-13T11:15:12Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/orderPRO\" && PYTHONIOENCODING=utf-8 python tools/generowanie/zaproszenie_slub_rodzice_zdjecie_w5.py --klient \"Natalia Skrzypek\" --imiona-pary \"Natalia i Mariusz","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-13T11:15:20Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro -e \"UPDATE order_items SET proj","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-13T11:49:47Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro -e \"\\nSELECT \\n oi.id AS i","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-13T11:49:57Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro -e \"\\nSELECT o.internal_order_","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-13T11:50:12Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro -e \"SELECT id, product_name_pat","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-13T11:50:24Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro -e \"UPDATE project_mappings SET","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-13T11:50:42Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro -e \"\\nSELECT \\n oi.id AS i","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-13T11:50:52Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro -e \"\\nSELECT oi.id, oi.origina","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-13T11:51:00Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro -e \"SELECT id, product_name_pat","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-13T11:51:13Z","tool":"Bash","cmd":"printf 'SET NAMES utf8mb4;\\\\nUPDATE project_mappings SET product_name_pattern = '\"'\"'Akrylowa statuetka podzi\\\\xc4\\\\x99kowanie dla rodzic\\\\xc3\\\\xb3w ze zdj\\\\xc4\\\\x99ciem - Wz\\\\xc3\\\","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-13T11:51:19Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro < /tmp/fix_mapping.sql 2>/dev/nu","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-13T11:51:26Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro -e \"SELECT id, product_name_pat","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-13T11:51:35Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro -e \"SELECT oa.email FROM order_","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-13T11:51:43Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/orderPRO\" && python tools/generowanie/email_message_fetcher.py --email \"haa1122@wp.pl\" --days 60 --json 2>/dev/null\",\"description\":\"Sprawdź maile od klientk","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-13T11:51:48Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/orderPRO\" && python tools/generowanie/email_photo_fetcher.py --email \"haa1122@wp.pl\" --out \"C:/tmp/orderpro_photos\" --days 365 2>/dev/null\",\"description\":\"","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-13T11:52:14Z","tool":"Bash","cmd":"tasklist //FI \"IMAGENAME eq Photoshop.exe\" 2>/dev/null | grep -i photoshop || echo \"BRAK\"\",\"description\":\"Sprawdź Photoshop\"},\"tool_response\":{\"stdout\":\"Binary file (standard input) mat","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-13T11:52:35Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/orderPRO\" && PYTHONIOENCODING=utf-8 python tools/generowanie/akrylowa_statuetka_podziekowanie_rodzice_wzor3.py --klient \"Hanna Kopp-Ostrowska\" --para \"Hania i P","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-13T11:52:48Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro -e \"UPDATE order_items SET proj","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-13T12:50:29Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro -e \"\\nSELECT \\n oi.id AS i","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-13T12:50:39Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro -e \"\\nSELECT o.internal_order_","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
249
.paul/governance/governance_2026-05-14.jsonl
Normal file
249
.paul/governance/governance_2026-05-14.jsonl
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
{"ts":"2026-05-14T07:16:16Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro -e \"SELECT id, product_name_pat","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T07:16:36Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro -e \"\\nSELECT \\n oi.id AS i","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T07:16:46Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro -e \"\\nSELECT o.internal_order_","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T07:17:10Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro -e \"\\nSELECT \\n oi.id AS i","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T07:17:20Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro -e \"\\nSELECT oa.address_type, ","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T07:17:30Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro -e \"\\nSELECT \\n oi.id AS i","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T07:17:57Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/orderPRO\" && python tools/generowanie/email_message_fetcher.py --email \"pene.lopa@o2.pl\" --days 60 --json 2>/dev/null | head -100\",\"description\":\"Pobierz mai","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T07:18:59Z","tool":"Bash","cmd":"tasklist //FI \"IMAGENAME eq Photoshop.exe\" 2>/dev/null | grep -i photoshop\",\"description\":\"Sprawdź czy Photoshop jest uruchomiony\"},\"tool_response\":{\"stdout\":\"\",\"stderr\":\"\",\"interru","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T07:19:48Z","tool":"Bash","cmd":"tasklist //FI \"IMAGENAME eq Photoshop.exe\" 2>/dev/null | grep -i photoshop\",\"description\":\"Weryfikuj czy Photoshop jest uruchomiony\"},\"tool_response\":{\"stdout\":\"Binary file (standard input","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T07:20:04Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/orderPRO\" && PYTHONIOENCODING=utf-8 python tools/generowanie/pudelko_komunia_kielich.py \\\\\\n --klient \"Paulina Klocek-Nizioł 01\" \\\\\\n --imie \"Kornelki\","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T07:20:13Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro -e \"UPDATE order_items SET proj","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T07:27:31Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro -e \"\\nSELECT \\n oi.id AS i","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T07:27:39Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro -e \"\\nSELECT o.internal_order_","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T07:27:51Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro -e \"\\nSELECT \\n oi.id AS i","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T07:28:12Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/orderPRO\" && PYTHONIOENCODING=utf-8 python tools/generowanie/email_message_fetcher.py --email \"nkozik@vp.pl\" --days 60 --json 2>/dev/null | head -100\",\"descrip","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T07:29:19Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/orderPRO\" && PYTHONIOENCODING=utf-8 python tools/generowanie/pudelko_komunia_kielich.py \\\\\\n --klient \"Natalia Pawłowska\" \\\\\\n --imie \"Dawida\" \\\\\\n","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T07:29:29Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/orderPRO\" && PYTHONIOENCODING=utf-8 python tools/generowanie/pudelko_komunia_dlonie.py \\\\\\n --klient \"Natalia Pawłowska\" \\\\\\n --imie \"Adama\" \\\\\\n ","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T07:29:37Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro -e \"UPDATE order_items SET proj","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T09:13:46Z","tool":"Bash","cmd":"mkdir -p \".paul/phases/127-polkurier-integration-foundation\"\",\"description\":\"Create phase directory\"},\"tool_response\":{\"stdout\":\"\",\"stderr\":\"\",\"interrupted\":false,\"isImage\":false,","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T09:15:59Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\phases\\\\127-polkurier-integration-foundation\\\\127-01-PLAN.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T09:16:13Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T09:16:21Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\ROADMAP.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T09:16:26Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\ROADMAP.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T09:23:45Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\database\\\\migrations\\\\20260514_000114_create_polkurier_integration_settings.sql","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T09:24:13Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Settings\\\\PolkurierIntegrationRepository.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T09:24:25Z","tool":"Bash","cmd":"\"C:/xampp/php/php.exe\" -l \"src/Modules/Settings/PolkurierIntegrationRepository.php\" 2>&1\",\"description\":\"Lint Repository with XAMPP PHP\"},\"tool_response\":{\"stdout\":\"No syntax errors dete","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T09:24:51Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Settings\\\\PolkurierApiClient.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T09:25:11Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Settings\\\\PolkurierIntegrationController.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T09:25:34Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\resources\\\\views\\\\settings\\\\polkurier.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T09:25:45Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\resources\\\\lang\\\\pl.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T09:25:58Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\resources\\\\lang\\\\pl.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T09:26:08Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\routes\\\\web.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T09:26:15Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\routes\\\\web.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T09:26:32Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\routes\\\\web.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T09:26:44Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Settings\\\\IntegrationsHubController.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T09:26:53Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Settings\\\\IntegrationsHubController.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T09:26:58Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\routes\\\\web.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T09:27:08Z","tool":"Bash","cmd":"\"C:/xampp/php/php.exe\" -l \"src/Modules/Settings/PolkurierApiClient.php\" 2>&1; \"C:/xampp/php/php.exe\" -l \"src/Modules/Settings/PolkurierIntegrationController.php\" 2>&1; \"C:/xampp/php/php.exe\"","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T09:27:16Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\codebase\\\\db_schema.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T09:27:26Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\codebase\\\\db_schema.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T09:27:54Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\codebase\\\\architecture.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T09:28:50Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\codebase\\\\tech_changelog.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T09:29:01Z","tool":"Bash","cmd":"git diff --stat 2>&1 | tail -25\",\"description\":\"Show modified files diff stat\"},\"tool_response\":{\"stdout\":\" .paul/ROADMAP.md | 3 +-\\n .paul/STATE.md ","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T09:29:08Z","tool":"Bash","cmd":"git status --porcelain 2>&1 | grep -E \"polkurier|127\" | head -10\",\"description\":\"Show new untracked files\"},\"tool_response\":{\"stdout\":\"?? .paul/phases/127-polkurier-integration-foundation/","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T09:29:16Z","tool":"Bash","cmd":"git status --porcelain 2>&1 | head -25\",\"description\":\"Full git status\"},\"tool_response\":{\"stdout\":\" M .paul/ROADMAP.md\\n M .paul/STATE.md\\n M .paul/codebase/architecture.md\\n M .paul/cod","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T09:29:31Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T09:33:14Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Settings\\\\PolkurierApiClient.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T09:33:21Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Settings\\\\PolkurierApiClient.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T09:33:28Z","tool":"Bash","cmd":"\"C:/xampp/php/php.exe\" -l \"src/Modules/Settings/PolkurierApiClient.php\" 2>&1\",\"description\":\"Lint updated ApiClient\"},\"tool_response\":{\"stdout\":\"No syntax errors detected in src/Modules/","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T09:37:20Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Settings\\\\PolkurierApiClient.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T09:39:27Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\phases\\\\127-polkurier-integration-foundation\\\\127-01-SUMMARY.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T09:39:51Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\changelog\\\\2026-05-14.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T09:40:07Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T09:40:21Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T09:40:28Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\ROADMAP.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T09:40:42Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\ROADMAP.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T09:40:47Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\ROADMAP.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T09:41:01Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\PROJECT.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T09:41:20Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\PROJECT.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T09:41:28Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\PROJECT.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T09:41:37Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\PROJECT.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T09:42:39Z","tool":"Bash","cmd":"git status --porcelain 2>&1\",\"description\":\"Final git status check\"},\"tool_response\":{\"stdout\":\" M .paul/PROJECT.md\\n M .paul/ROADMAP.md\\n M .paul/STATE.md\\n M .paul/codebase/architecture","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T09:42:48Z","tool":"Bash","cmd":"git log --oneline -3 2>&1\",\"description\":\"Recent commit style\"},\"tool_response\":{\"stdout\":\"541e61b update\\nafdbc67 upadte\\nc758ec7 feat(126): invoice GUS field mapping fix (JDG/KRS heurist","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T09:42:57Z","tool":"Bash","cmd":"git add .paul/PROJECT.md .paul/ROADMAP.md .paul/STATE.md .paul/codebase/architecture.md .paul/codebase/db_schema.md .paul/codebase/tech_changelog.md .paul/changelog/2026-05-14.md \".paul/phases/127-po","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T09:43:12Z","tool":"Bash","cmd":"git commit -m \"$(cat <<'EOF'\\nfeat(127): polkurier integration foundation\\n\\nSingle-instance globalna konfiguracja polkurier.pl jako alternatywa\\ndla Apaczki: szyfrowany login + Token API, karta ","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T09:43:19Z","tool":"Bash","cmd":"git status --porcelain 2>&1\",\"description\":\"Verify clean state post-commit\"},\"tool_response\":{\"stdout\":\"?? .paul/governance/governance_2026-05-14.jsonl\",\"stderr\":\"\",\"interrupted\":fals","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T09:43:28Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T09:52:02Z","tool":"Bash","cmd":"mkdir -p \"C:/visual studio code/projekty/orderPRO/.paul/phases/128-polkurier-shipment-service\" && ls \"C:/visual studio code/projekty/orderPRO/.paul/phases/\" | grep -i polkurier\",\"description\":\","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T09:54:49Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\phases\\\\128-polkurier-shipment-service\\\\128-01-PLAN.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T09:55:03Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T09:55:12Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\ROADMAP.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T09:57:40Z","tool":"Bash","cmd":"gh api repos/Polkurier/polkurier-sdk/contents/src/Api 2>&1 | head -50\",\"description\":\"List polkurier SDK Api directory\"},\"tool_response\":{\"stdout\":\"/usr/bin/bash: line 1: gh: command not fou","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T10:01:41Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Settings\\\\PolkurierApiClient.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T10:01:48Z","tool":"Bash","cmd":"\"C:/xampp/php/php.exe\" -l \"C:/visual studio code/projekty/orderPRO/src/Modules/Settings/PolkurierApiClient.php\"\",\"description\":\"Syntax check PolkurierApiClient\"},\"tool_response\":{\"stdout\"","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T10:03:45Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Shipments\\\\PolkurierShipmentService.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T10:04:02Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Shipments\\\\PolkurierTrackingService.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T10:04:17Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Shipments\\\\DeliveryStatus.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T10:04:25Z","tool":"Bash","cmd":"\"C:/xampp/php/php.exe\" -l \"C:/visual studio code/projekty/orderPRO/src/Modules/Shipments/PolkurierShipmentService.php\" && \"C:/xampp/php/php.exe\" -l \"C:/visual studio code/projekty/orderPRO/src/","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T10:05:11Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\routes\\\\web.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T10:05:16Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\routes\\\\web.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T10:05:29Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\routes\\\\web.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T10:05:39Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Cron\\\\CronHandlerFactory.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T10:05:45Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Cron\\\\CronHandlerFactory.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T10:05:52Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Cron\\\\CronHandlerFactory.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T10:06:01Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Shipments\\\\ShipmentController.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T10:06:06Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Shipments\\\\ShipmentController.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T10:06:15Z","tool":"Bash","cmd":"wc -l \"C:/visual studio code/projekty/orderPRO/src/Modules/Shipments/ShipmentController.php\"\",\"description\":\"Get ShipmentController line count\"},\"tool_response\":{\"stdout\":\"527 C:/visual st","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T10:06:29Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Shipments\\\\ShipmentController.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T10:06:44Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Shipments\\\\PolkurierShipmentService.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T10:06:59Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\routes\\\\web.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T10:07:20Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\resources\\\\views\\\\shipments\\\\prepare.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T10:07:26Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\resources\\\\views\\\\shipments\\\\prepare.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T10:07:44Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\resources\\\\views\\\\shipments\\\\prepare.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T10:07:53Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\resources\\\\views\\\\shipments\\\\prepare.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T10:08:01Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\resources\\\\views\\\\shipments\\\\prepare.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T10:08:07Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\resources\\\\views\\\\shipments\\\\prepare.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T10:08:17Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\resources\\\\views\\\\shipments\\\\prepare.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T10:08:25Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\resources\\\\views\\\\shipments\\\\prepare.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T10:08:46Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\resources\\\\views\\\\shipments\\\\prepare.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T10:09:16Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Shipments\\\\ShipmentController.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T10:09:38Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\routes\\\\web.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T10:09:46Z","tool":"Bash","cmd":"\"C:/xampp/php/php.exe\" -l \"C:/visual studio code/projekty/orderPRO/routes/web.php\" && \"C:/xampp/php/php.exe\" -l \"C:/visual studio code/projekty/orderPRO/src/Modules/Cron/CronHandlerFactory.php\","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T10:09:59Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T10:14:57Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\resources\\\\views\\\\shipments\\\\prepare.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T10:15:10Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\resources\\\\views\\\\shipments\\\\prepare.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T10:15:26Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\resources\\\\views\\\\shipments\\\\prepare.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T10:15:43Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\resources\\\\views\\\\shipments\\\\prepare.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T10:15:52Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\resources\\\\views\\\\shipments\\\\prepare.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T10:16:02Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Shipments\\\\ShipmentController.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T10:16:08Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\routes\\\\web.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T10:16:22Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Shipments\\\\PolkurierShipmentService.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T10:16:35Z","tool":"Bash","cmd":"\"C:/xampp/php/php.exe\" -l \"C:/visual studio code/projekty/orderPRO/resources/views/shipments/prepare.php\" && \"C:/xampp/php/php.exe\" -l \"C:/visual studio code/projekty/orderPRO/src/Modules/Shipm","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T10:25:01Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\resources\\\\views\\\\shipments\\\\prepare.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T10:25:09Z","tool":"Bash","cmd":"\"C:/xampp/php/php.exe\" -l \"C:/visual studio code/projekty/orderPRO/resources/views/shipments/prepare.php\"\",\"description\":\"Syntax recheck\"},\"tool_response\":{\"stdout\":\"No syntax errors det","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T10:26:01Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Shipments\\\\PolkurierShipmentService.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T10:26:10Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Shipments\\\\PolkurierShipmentService.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T10:26:17Z","tool":"Bash","cmd":"\"C:/xampp/php/php.exe\" -l \"C:/visual studio code/projekty/orderPRO/src/Modules/Shipments/PolkurierShipmentService.php\"\",\"description\":\"Syntax check\"},\"tool_response\":{\"stdout\":\"No syntax","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T10:28:38Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Shipments\\\\PolkurierShipmentService.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T10:28:58Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Shipments\\\\PolkurierShipmentService.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T10:29:05Z","tool":"Bash","cmd":"\"C:/xampp/php/php.exe\" -l \"C:/visual studio code/projekty/orderPRO/src/Modules/Shipments/PolkurierShipmentService.php\"\",\"description\":\"Syntax check\"},\"tool_response\":{\"stdout\":\"No syntax","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T10:34:12Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Settings\\\\PolkurierApiClient.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T10:34:19Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Shipments\\\\PolkurierShipmentService.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T10:34:26Z","tool":"Bash","cmd":"\"C:/xampp/php/php.exe\" -l \"C:/visual studio code/projekty/orderPRO/src/Modules/Settings/PolkurierApiClient.php\" && \"C:/xampp/php/php.exe\" -l \"C:/visual studio code/projekty/orderPRO/src/Modules","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T10:38:32Z","tool":"Bash","cmd":"where pdftoppm pdftotext 2>nul; \"C:/xampp/php/php.exe\" -r 'echo PHP_VERSION;' 2>&1 | head -3\",\"description\":\"Check for PDF tools\"},\"tool_response\":{\"stdout\":\"C:\\\\Program Files\\\\Git\\\\","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T10:38:43Z","tool":"Bash","cmd":"\"C:/Program Files/Git/mingw64/bin/pdftotext.exe\" -layout \"C:\\\\Users\\\\jacek\\\\.claude\\\\projects\\\\C--visual-studio-code-projekty-orderPRO\\\\ef283079-60e0-4bb1-94ab-bac2fb247aad\\\\tool-resu","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T10:39:22Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Settings\\\\PolkurierApiClient.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T10:39:29Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Shipments\\\\PolkurierShipmentService.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T10:39:36Z","tool":"Bash","cmd":"\"C:/xampp/php/php.exe\" -l \"C:/visual studio code/projekty/orderPRO/src/Modules/Settings/PolkurierApiClient.php\" && \"C:/xampp/php/php.exe\" -l \"C:/visual studio code/projekty/orderPRO/src/Modules","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T10:43:08Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\database\\\\migrations\\\\20260514_000115_seed_polkurier_delivery_status_mappings.sql","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T10:43:20Z","tool":"Bash","cmd":"\"C:/xampp/php/php.exe\" \"C:/visual studio code/projekty/orderPRO/bin/migrate.php\" 2>&1 | tail -20\",\"description\":\"Run migration\"},\"tool_response\":{\"stdout\":\"PHP Fatal error: Uncaught PDO","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T10:44:51Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\codebase\\\\architecture.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T10:45:01Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\codebase\\\\db_schema.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T10:46:06Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\codebase\\\\tech_changelog.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T10:46:16Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T10:46:27Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T10:49:07Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\phases\\\\128-polkurier-shipment-service\\\\128-01-SUMMARY.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T10:49:48Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\changelog\\\\2026-05-14.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T10:50:05Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T10:50:21Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\ROADMAP.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T10:50:28Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\ROADMAP.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T10:50:44Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\PROJECT.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T10:51:09Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\PROJECT.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T10:51:19Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\PROJECT.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T10:51:25Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\PROJECT.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T10:56:11Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/orderPRO\" && git status --short\",\"description\":\"Git status\"},\"tool_response\":{\"stdout\":\" M .paul/PROJECT.md\\n M .paul/ROADMAP.md\\n M .paul/STATE.md\\n ","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T10:56:21Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/orderPRO\" && git add .paul/PROJECT.md .paul/ROADMAP.md .paul/STATE.md .paul/changelog/2026-05-14.md .paul/codebase/architecture.md .paul/codebase/db_schema.md .pau","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T10:56:37Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/orderPRO\" && git commit -m \"$(cat <<'EOF'\\nfeat(128): polkurier shipment service + tracking + UI prepare\\n\\nPolkurierApiClient rozszerzony do pelnego kontraktu","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T10:56:45Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T10:59:56Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -u host700513_orderpro -p'hrDNtUBg9grwZ7syN77S' host700513_orderpro -e \"SELECT id, product_name_pattern, ","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T11:00:10Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -u host700513_orderpro -p'hrDNtUBg9grwZ7syN77S' host700513_orderpro -e \"SELECT oi.id AS item_id, oi.origi","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T11:00:19Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -u host700513_orderpro -p'hrDNtUBg9grwZ7syN77S' host700513_orderpro -e \"SELECT o.id, o.internal_order_num","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T11:00:27Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -u host700513_orderpro -p'hrDNtUBg9grwZ7syN77S' host700513_orderpro -e \"SELECT DISTINCT oa.name FROM orde","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T11:00:35Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -u host700513_orderpro -p'hrDNtUBg9grwZ7syN77S' host700513_orderpro -e \"SELECT o.id, o.internal_order_num","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T11:00:58Z","tool":"Write","file":"\\\\tmp\\\\q2.sql","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T11:02:40Z","tool":"Bash","cmd":"tasklist //FI \"IMAGENAME eq Photoshop.exe\" 2>/dev/null | grep -i photoshop\",\"description\":\"Check Photoshop running\"},\"tool_response\":{\"stdout\":\"Binary file (standard input) matches\",\"std","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T11:02:47Z","tool":"Bash","cmd":"python tools/generowanie/email_message_fetcher.py --email \"ania-biniecka@wp.pl\" --days 60 --json 2>&1 | head -50\",\"description\":\"Check customer emails\"},\"tool_response\":{\"stdout\":\"BRAK: ni","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T11:03:19Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/orderPRO\" && PYTHONIOENCODING=utf-8 python -c \"\\nimport subprocess\\nzyczenia = 'Kochany Antosiu.\\\\rDziś nastał niezwykle uroczysty dzień – dzień Twojej ","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T11:13:55Z","tool":"Bash","cmd":"python tools/generowanie/email_message_fetcher.py --email \"Julitach123@interia.pl\" --days 60 --json 2>&1 | head -30\",\"description\":\"Check client emails\"},\"tool_response\":{\"stdout\":\"BRAK: n","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T11:14:44Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/orderPRO\" && PYTHONIOENCODING=utf-8 python -c \"\\nimport subprocess\\nzyczenia = 'Z okazji przyjęcia do serca Bożej miłości\\\\rdumni z Ciebie i pełni radoś","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T11:18:05Z","tool":"Bash","cmd":"python tools/generowanie/email_message_fetcher.py --email \"oska8589@wp.pl\" --days 60 --json 2>&1 | head -50\",\"description\":\"Check client emails\"},\"tool_response\":{\"stdout\":\"BRAK: nie znale","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T11:19:01Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/orderPRO\" && PYTHONIOENCODING=utf-8 python -c \"\\nimport subprocess\\nzyczenia = (\\n 'Kochany Milanku\\\\r'\\n 'Dziś jest wyjątkowy dzień — pełen wzrusze","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T11:27:41Z","tool":"Bash","cmd":"python tools/generowanie/email_message_fetcher.py --email \"magda.luczkowska2@wp.pl\" --days 60 --json 2>&1 | head -30\",\"description\":\"Check client emails\"},\"tool_response\":{\"stdout\":\"BRAK: ","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T11:28:16Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/orderPRO\" && python tools/generowanie/prosba_chrzestny_zlota_plexi_uv.py --klient \"Magda Łuczkowska 01\" --imie-dziecka \"Maks\" --matka-wolacz \"Ciociu Natalio\","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T11:29:32Z","tool":"Bash","cmd":"python tools/generowanie/email_message_fetcher.py --email \"urszula-tworek@o2.pl\" --days 60 --json 2>&1 | head -30\\nls \"d:/pomysloweprezenty.pl/projekty/chrzest - prośby o bycie chrzestnym/złota ","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T11:29:50Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/orderPRO\" && python tools/generowanie/prosba_chrzestny_zlota_plexi_uv.py --klient \"Urszula Golonka\" --imie-dziecka \"Jan\" --matka-wolacz \"Ciociu Gosiu\" --ojci","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T11:30:55Z","tool":"Bash","cmd":"python tools/generowanie/email_message_fetcher.py --email \"ewaprzeradzka@onet.pl\" --days 60 --json 2>&1 | head -10\\ngrep -E \"add_argument|PROJEKT_DIR\" tools/generowanie/pudelko_chrzest_golabek_hd","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T11:31:26Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/orderPRO\" && PYTHONIOENCODING=utf-8 python -c \"\\nimport subprocess\\nzyczenia = 'Niech Wasza córeczka rośnie zdrowo i radośnie,\\\\rniech daje Wam pociechę,\","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T12:31:15Z","tool":"Bash","cmd":"python tools/generowanie/email_message_fetcher.py --email \"magdakozlowska@ymail.com\" --days 60 --json 2>&1 | head -10\\ngrep -E \"add_argument|PROJEKT_DIR\" tools/generowanie/pudelko_komunia_chlopie","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T12:32:24Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/orderPRO\" && PYTHONIOENCODING=utf-8 python -c \"\\nimport subprocess\\nzyczenia1 = (\\n 'Niech Pan Jezus, który zamieszkał dziś\\\\r'\\n 'w Twoim sercu zostan","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T12:32:44Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/orderPRO\" && PYTHONIOENCODING=utf-8 python -c \"\\nimport subprocess\\nzyczenia2 = (\\n 'Niech miłość,\\\\r'\\n 'która przepełnia dziś Twoje serce,\\\\r'\\","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T12:37:15Z","tool":"Bash","cmd":"python tools/generowanie/email_message_fetcher.py --email \"beata.malinowska03@gmail.com\" --days 60 --json 2>&1 | head -10\\nls \"d:/pomysloweprezenty.pl/projekty/komunia święta - pudełka na pieni","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T12:37:33Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/orderPRO\" && PYTHONIOENCODING=utf-8 python -c \"\\nimport subprocess\\nzyczenia = (\\n 'W dniu Twojej Pierwszej Komunii\\\\r'\\n 'życzę Ci dużo wiary i odwagi","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T12:42:54Z","tool":"Bash","cmd":"python tools/generowanie/email_message_fetcher.py --email \"annakusmierek26@gmail.com\" --days 60 --json 2>&1 | head -10\\nls \"d:/pomysloweprezenty.pl/projekty/komunia święta - pudełka na pieniąd","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T12:43:18Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/orderPRO\" && PYTHONIOENCODING=utf-8 python -c \"\\nimport subprocess\\nzyczenia = (\\n 'Idź drogą bez przeszkód\\\\r'\\n 'miej w sercu wiarę, uczciwość i m","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T12:55:48Z","tool":"Bash","cmd":"mkdir -p \"C:/visual studio code/projekty/orderPRO/.paul/phases/129-order-user-notes\"\",\"description\":\"Create phase 129 directory\"},\"tool_response\":{\"stdout\":\"\",\"stderr\":\"\",\"interrupte","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T12:58:24Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\phases\\\\129-order-user-notes\\\\129-01-PLAN.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T12:58:43Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T12:58:51Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\ROADMAP.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T12:59:46Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\database\\\\migrations\\\\20260514_000116_extend_order_notes_user_authored.sql","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T13:00:14Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\codebase\\\\db_schema.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T13:01:45Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Orders\\\\OrderNotesService.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T13:02:01Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Orders\\\\OrdersRepository.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T13:02:07Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Orders\\\\OrdersRepository.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T13:02:15Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Orders\\\\OrdersRepository.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T13:02:30Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Orders\\\\OrdersRepository.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T13:02:39Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Orders\\\\OrdersController.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T13:03:08Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Orders\\\\OrdersController.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T13:03:15Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Orders\\\\OrdersController.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T13:03:24Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Orders\\\\OrdersController.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T13:03:33Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Orders\\\\OrdersController.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T13:03:57Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Orders\\\\OrdersController.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T13:04:04Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Orders\\\\OrdersController.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T13:04:26Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\routes\\\\web.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T13:04:35Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\routes\\\\web.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T13:04:43Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\routes\\\\web.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T13:04:52Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/orderPRO\" && C:/xampp/php/php.exe -l src/Modules/Orders/OrderNotesService.php && C:/xampp/php/php.exe -l src/Modules/Orders/OrdersController.php && C:/xampp/php/ph","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T13:05:07Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\resources\\\\views\\\\orders\\\\show.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T13:05:30Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\resources\\\\views\\\\orders\\\\show.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T13:05:38Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\resources\\\\lang\\\\pl.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T13:06:02Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\resources\\\\scss\\\\modules\\\\_order-notes.scss","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T13:06:16Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\resources\\\\scss\\\\app.scss","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T13:06:39Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\public\\\\assets\\\\js\\\\modules\\\\order-notes.js","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T13:06:50Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\resources\\\\views\\\\layouts\\\\app.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T13:07:00Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/orderPRO\" && C:/xampp/php/php.exe -l resources/views/orders/show.php && C:/xampp/php/php.exe -l resources/views/layouts/app.php && C:/xampp/php/php.exe -l resource","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T13:07:08Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/orderPRO\" && which node 2>/dev/null && node --check public/assets/js/modules/order-notes.js && echo \"JS OK\"\",\"description\":\"Verify JS syntax with node\"},\"t","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T13:07:17Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/orderPRO\" && (test -f package.json && cat package.json | head -30; ls tools 2>/dev/null | head)\",\"description\":\"Check build pipeline\"},\"tool_response\":{\"st","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T13:07:27Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/orderPRO\" && npm run build:css 2>&1 | tail -10\",\"description\":\"Build SCSS\"},\"tool_response\":{\"stdout\":\"> orderpro@1.0.0 build:css\\n> sass --style=compre","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T13:08:12Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\codebase\\\\tech_changelog.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T13:08:31Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T13:08:43Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T13:08:51Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/orderPRO\" && git status --short && echo \"---\" && git diff --stat | tail -25\",\"description\":\"Git status overview\"},\"tool_response\":{\"stdout\":\" M .paul/R","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T13:15:38Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\phases\\\\129-order-user-notes\\\\129-01-SUMMARY.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T13:16:39Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T13:16:46Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\ROADMAP.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T13:16:51Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\ROADMAP.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T13:17:13Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\PROJECT.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T13:17:34Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\PROJECT.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T13:17:42Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\PROJECT.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T13:19:45Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/orderPRO\" && git status --short && echo \"---LOG---\" && git log --oneline -5\",\"description\":\"Pre-commit status check\"},\"tool_response\":{\"stdout\":\" M .pa","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T13:20:06Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/orderPRO\" && git add .paul/PROJECT.md .paul/ROADMAP.md .paul/STATE.md .paul/changelog/2026-05-14.md .paul/codebase/db_schema.md .paul/codebase/tech_changelog.md .p","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T13:36:00Z","tool":"Bash","cmd":"mkdir -p \".paul/phases/130-polkurier-delivery-status-mappings\"\",\"description\":\"Create phase directory\"},\"tool_response\":{\"stdout\":\"\",\"stderr\":\"\",\"interrupted\":false,\"isImage\":fals","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T13:37:16Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\phases\\\\130-polkurier-delivery-status-mappings\\\\130-01-PLAN.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T13:37:30Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T13:37:38Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\ROADMAP.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T13:47:07Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Shipments\\\\DeliveryStatus.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T13:47:18Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Shipments\\\\DeliveryStatus.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T13:47:24Z","tool":"Bash","cmd":"\"C:\\\\xampp\\\\php\\\\php.exe\" -l \"src/Modules/Shipments/DeliveryStatus.php\"\",\"description\":\"Lint DeliveryStatus.php\"},\"tool_response\":{\"stdout\":\"No syntax errors detected in src/Module","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T13:47:31Z","tool":"Bash","cmd":"\"C:\\\\xampp\\\\php\\\\php.exe\" -r \"require 'src/Modules/Shipments/DeliveryStatus.php'; \\\\$m = App\\\\Modules\\\\Shipments\\\\DeliveryStatus::getDefaultMappings('polkurier'); echo 'count=' . coun","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T13:47:39Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Settings\\\\DeliveryStatusesController.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T13:47:44Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Settings\\\\DeliveryStatusMappingController.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T13:47:49Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Shipments\\\\DeliveryStatusMappingRepository.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T13:47:57Z","tool":"Bash","cmd":"\"C:\\\\xampp\\\\php\\\\php.exe\" -l \"src/Modules/Settings/DeliveryStatusesController.php\" && \"C:\\\\xampp\\\\php\\\\php.exe\" -l \"src/Modules/Settings/DeliveryStatusMappingController.php\" && \"C","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T13:48:37Z","tool":"Bash","cmd":"git diff --stat\",\"description\":\"Show modified files\"},\"tool_response\":{\"stdout\":\" .paul/ROADMAP.md | 1 +\\n .paul/STATE.md ","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T13:49:00Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T15:14:51Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\phases\\\\130-polkurier-delivery-status-mappings\\\\130-01-SUMMARY.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T15:15:17Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\changelog\\\\2026-05-14.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T15:15:48Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\codebase\\\\tech_changelog.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T15:16:06Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T15:16:16Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T15:16:37Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\PROJECT.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T15:16:52Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\PROJECT.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T15:17:07Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\PROJECT.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T15:17:12Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\PROJECT.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T15:17:21Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\ROADMAP.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T15:17:27Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\ROADMAP.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T15:17:35Z","tool":"Bash","cmd":"git status --short\",\"description\":\"Check git status\"},\"tool_response\":{\"stdout\":\" M .paul/PROJECT.md\\n M .paul/ROADMAP.md\\n M .paul/STATE.md\\n M .paul/changelog/2026-05-14.md\\n M .paul/c","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T15:17:44Z","tool":"Bash","cmd":"git add .paul/PROJECT.md .paul/ROADMAP.md .paul/STATE.md .paul/changelog/2026-05-14.md .paul/codebase/tech_changelog.md .paul/phases/130-polkurier-delivery-status-mappings/ src/Modules/Settings/Delive","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T15:17:49Z","tool":"Bash","cmd":"git commit -m \"$(cat <<'EOF'\\nfeat(130): polkurier delivery status mappings UI\\n\\nPhase 130 complete (1 plan):\\n- POLKURIER_MAP + POLKURIER_DESCRIPTIONS w DeliveryStatus.php (7 wpisow O/P/A/WP/D/","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-14T15:17:58Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
33
.paul/governance/governance_2026-05-15.jsonl
Normal file
33
.paul/governance/governance_2026-05-15.jsonl
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
{"ts":"2026-05-15T07:40:46Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro -e \"SELECT id, product_name_pattern, sc","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-15T07:40:59Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro -e \"SELECT oi.id AS item_id, oi.origina","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-15T07:41:13Z","tool":"Bash","cmd":"python tools/generowanie/email_message_fetcher.py --email \"studio@kobido.com.pl\" --days 60 --json 2>&1; tasklist //FI \"IMAGENAME eq Photoshop.exe\" 2>/dev/null | grep -i photoshop\",\"description\"","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-15T07:43:02Z","tool":"Bash","cmd":"python tools/generowanie/pudelko_komunia_kielich.py --help 2>&1 | head -40\",\"description\":\"Check script params\"},\"tool_response\":{\"stdout\":\"usage: pudelko_komunia_kielich.py [-h] --klient KL","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-15T07:43:17Z","tool":"Bash","cmd":"PYTHONIOENCODING=utf-8 python -c \"import subprocess,sys; r=subprocess.run(['python','tools/generowanie/pudelko_komunia_kielich.py','--klient','Joanna Siedlecka','--imie','Julii','--data','30.05.2026'","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-15T07:43:26Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro -e \"UPDATE order_items SET project_gene","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-15T07:47:54Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro -e \"SELECT oi.id AS item_id, oi.origina","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-15T07:48:05Z","tool":"Bash","cmd":"python tools/generowanie/email_message_fetcher.py --email \"egutkowska@vp.pl\" --days 60 --json 2>&1; ls \"d:/pomysloweprezenty.pl/projekty/komunia Ĺ›wiÄ™ta - pudeĹ‚ka na pieniÄ…dze/PudeĹ‚ko na pieniÄ","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-15T07:48:30Z","tool":"Bash","cmd":"PYTHONIOENCODING=utf-8 python -c \"import subprocess,sys,os; r=subprocess.run(['python','tools/generowanie/pudelko_komunia_kielich.py','--klient','Ewelina Gutkowska','--imie','Franciszka','--data','31","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-15T07:48:39Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro -e \"UPDATE order_items SET project_gene","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-15T08:02:53Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro -e \"SELECT oi.id AS item_id, oi.origina","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-15T08:03:23Z","tool":"Bash","cmd":"PYTHONIOENCODING=utf-8 python -c \"import subprocess,sys,os; r=subprocess.run(['python','tools/generowanie/pudelko_komunia_chlopiec.py','--klient','Dorota Kuta','--imie','Franciszka','--data','30.05.2","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-15T08:03:35Z","tool":"Bash","cmd":"PYTHONIOENCODING=utf-8 python -c \"import subprocess,sys,os; r=subprocess.run(['python','tools/generowanie/pudelko_komunia_kielich.py','--klient','Dorota Kuta','--imie','Helenki','--data','30.05.2026'","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-15T08:03:45Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro -e \"UPDATE order_items SET project_gene","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-15T08:05:59Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro -e \"SELECT oi.id AS item_id, oi.origina","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-15T08:06:23Z","tool":"Bash","cmd":"PYTHONIOENCODING=utf-8 python -c \"import subprocess,sys,os; r=subprocess.run(['python','tools/generowanie/pudelko_komunia_dlonie.py','--klient','Agata Romanowska','--imie','Tymona','--data','23.05.20","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-15T08:06:33Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro -e \"UPDATE order_items SET project_gene","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-15T08:18:24Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro -e \"SELECT oi.id AS item_id, oi.origina","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-15T08:18:34Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro -e \"SELECT oi.id, oi.original_name, oi.","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-15T08:18:51Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro -e \"SELECT oi.id AS item_id, oi.origina","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-15T08:19:18Z","tool":"Bash","cmd":"PYTHONIOENCODING=utf-8 python -c \"import subprocess,sys,os; r=subprocess.run(['python','tools/generowanie/pudelko_komunia_kwiaty.py','--klient','Agnieszka WÓJCIK','--imie','Zuzi','--data','23 | 05 |","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-15T08:19:27Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro -e \"UPDATE order_items SET project_gene","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-15T08:29:25Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro -e \"SELECT oi.id AS item_id, oi.origina","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-15T08:29:57Z","tool":"Bash","cmd":"PYTHONIOENCODING=utf-8 python -c \"import subprocess,sys,os; r=subprocess.run(['python','tools/generowanie/pudelko_komunia_golabek_uv.py','--klient','KASIA FIERKA','--imie','Magdaleny','--data','06 | ","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-15T08:30:07Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro -e \"UPDATE order_items SET project_gene","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-15T08:42:10Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro -e \"SELECT oi.id AS item_id, oi.origina","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-15T08:42:35Z","tool":"Bash","cmd":"PYTHONIOENCODING=utf-8 python -c \"import subprocess,sys,os; r=subprocess.run(['python','tools/generowanie/pudelko_chrzest_dziewczynka_aniolek.py','--klient','Marta Katafiasz','--imie','Olgi','--data'","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-15T08:42:44Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro -e \"UPDATE order_items SET project_gene","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-15T09:06:46Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro -e \"SELECT oi.id AS item_id, oi.origina","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-15T09:06:54Z","tool":"Bash","cmd":"python tools/generowanie/email_message_fetcher.py --email \"natala.szumeluk@gmail.com\" --days 60 --json 2>&1; ls \"tools/generowanie/\" | grep prosba_chrzestny_zlota\",\"description\":\"Check emails ","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-15T09:07:05Z","tool":"Bash","cmd":"python tools/generowanie/prosba_chrzestny_zlota_plexi_uv.py --help 2>&1 | head -40\",\"description\":\"Check script params\"},\"tool_response\":{\"stdout\":\"usage: prosba_chrzestny_zlota_plexi_uv.py ","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-15T09:07:21Z","tool":"Bash","cmd":"PYTHONIOENCODING=utf-8 python -c \"import subprocess,sys,os; r=subprocess.run(['python','tools/generowanie/prosba_chrzestny_zlota_plexi_uv.py','--klient','Natalia Zacharczuk','--imie-dziecka','Milan',","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-05-15T09:07:32Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro -e \"UPDATE order_items SET project_gene","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
292
.paul/phases/127-polkurier-integration-foundation/127-01-PLAN.md
Normal file
292
.paul/phases/127-polkurier-integration-foundation/127-01-PLAN.md
Normal file
@@ -0,0 +1,292 @@
|
|||||||
|
---
|
||||||
|
phase: 127-polkurier-integration-foundation
|
||||||
|
plan: 01
|
||||||
|
type: execute
|
||||||
|
wave: 1
|
||||||
|
depends_on: []
|
||||||
|
files_modified:
|
||||||
|
- database/migrations/20260514_000114_create_polkurier_integration_settings.sql
|
||||||
|
- src/Modules/Settings/PolkurierIntegrationRepository.php
|
||||||
|
- src/Modules/Settings/PolkurierApiClient.php
|
||||||
|
- src/Modules/Settings/PolkurierIntegrationController.php
|
||||||
|
- src/Modules/Settings/IntegrationsHubController.php
|
||||||
|
- resources/views/settings/integrations/polkurier.php
|
||||||
|
- routes/web.php
|
||||||
|
- .paul/codebase/db_schema.md
|
||||||
|
- .paul/codebase/architecture.md
|
||||||
|
- .paul/codebase/tech_changelog.md
|
||||||
|
autonomous: true
|
||||||
|
delegation: auto
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
## Goal
|
||||||
|
Dodac fundament integracji z brokerem kurierskim polkurier.pl jako rownolegla alternatywe dla Apaczki: pojedyncza globalna konfiguracja w `/settings/integrations/polkurier` (szyfrowany Token API), karta w hubie integracji `/settings/integrations`, oraz realny test polaczenia z API polkuriera (wywolanie endpointu zwracajacego dane konta lub liste uslug).
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
Operator dostaje druga bramke kurierska oprocz Apaczki. Faza zamyka warstwe ustawien i testu polaczenia — tworzenie przesylek, etykiety, tracking i mapowania metod dostawy beda dolozone w kolejnych fazach (analogicznie do tego jak Phase 116/117 zamknely tylko ustawienia HostedSMS/SMSPLANET przed pelnym SMS-em). Apaczka i jej `ShipmentProviderInterface` zostaja niezmienione — polkurier dziala obok.
|
||||||
|
|
||||||
|
## Output
|
||||||
|
- Migracja DDL tworzaca `polkurier_integration_settings` (mirror `apaczka_integration_settings`).
|
||||||
|
- `PolkurierIntegrationRepository` szyfrujacy Token API przez `IntegrationSecretCipher` i zarzadzajacy pojedynczym rekordem `integrations.type='polkurier'` (id rekordu zalezne, nie wpisywane na sztywno).
|
||||||
|
- `PolkurierApiClient` realnie wywolujacy API polkuriera w trybie test (endpoint zwracajacy dane konta / liste uslug — wybor zgodnie z dokumentacja SDK ze strony bazy wiedzy polkuriera, decyzja na czas implementacji).
|
||||||
|
- `PolkurierIntegrationController` z routami `GET /settings/integrations/polkurier`, `POST .../save`, `POST .../test`.
|
||||||
|
- Wiersz "polkurier" w hubie `/settings/integrations` ze statusem konfiguracji, sekretu, aktywnosci i ostatniego testu.
|
||||||
|
- Aktualizacja dokumentow projektowych (db_schema, architecture, tech_changelog).
|
||||||
|
</objective>
|
||||||
|
|
||||||
|
<context>
|
||||||
|
## Project Context
|
||||||
|
@.paul/PROJECT.md
|
||||||
|
@.paul/ROADMAP.md
|
||||||
|
@.paul/STATE.md
|
||||||
|
|
||||||
|
## Prior Art (wzorzec do skopiowania)
|
||||||
|
@src/Modules/Settings/ApaczkaIntegrationRepository.php
|
||||||
|
@src/Modules/Settings/ApaczkaApiClient.php
|
||||||
|
@src/Modules/Settings/ApaczkaIntegrationController.php
|
||||||
|
@src/Modules/Settings/HostedSmsIntegrationRepository.php
|
||||||
|
@src/Modules/Settings/HostedSmsIntegrationController.php
|
||||||
|
@src/Modules/Settings/SmsplanetIntegrationRepository.php
|
||||||
|
@src/Modules/Settings/SmsplanetIntegrationController.php
|
||||||
|
@src/Modules/Settings/IntegrationsHubController.php
|
||||||
|
@src/Modules/Settings/IntegrationSecretCipher.php
|
||||||
|
@database/migrations/20260512_000109_consolidate_fakturownia_to_single_instance.sql
|
||||||
|
|
||||||
|
## Codebase docs
|
||||||
|
@.paul/codebase/architecture.md
|
||||||
|
@.paul/codebase/db_schema.md
|
||||||
|
|
||||||
|
## Routy i widok wzorcowe
|
||||||
|
@routes/web.php
|
||||||
|
@resources/views/settings/integrations/apaczka.php
|
||||||
|
|
||||||
|
<clarifications>
|
||||||
|
- **Zakres MVP** — Jaki zakres ma pokryc pierwsza faza integracji polkurier.pl?
|
||||||
|
- Odpowiedz: Tylko fundament + test polaczenia (wzorzec faz 116/117).
|
||||||
|
- **Model konta** — Pojedyncza globalna instancja czy wieloinstancyjna?
|
||||||
|
- Odpowiedz: Single instance (jak Apaczka/InPost) — fixed `polkurier_integration_settings.id=1`, jeden rekord `integrations.type='polkurier'`.
|
||||||
|
- **Apaczka vs polkurier** — Zastapienie czy rownoleglosc?
|
||||||
|
- Odpowiedz: Obok Apaczki — oba dostawcy dzialaja, Apaczka netknieta, operator wybiera w kolejnych fazach (gdy `PolkurierShipmentService` zostanie dodany).
|
||||||
|
- **Paczkomaty / punkty odbioru** — Czy w tej fazie?
|
||||||
|
- Odpowiedz: Poza zakresem tej fazy. Operator potwierdzil "ma dzialac jak Apaczka"; obsluga punktow odbioru pojawi sie razem z `PolkurierShipmentService` w nastepnej fazie (tak jak Apaczka — receiver_point_id w shipment_packages).
|
||||||
|
</clarifications>
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<acceptance_criteria>
|
||||||
|
|
||||||
|
## AC-1: Migracja tworzy single-instance tabele konfiguracji
|
||||||
|
```gherkin
|
||||||
|
Given XAMPP MySQL jest online i migracje sa zacommitowane
|
||||||
|
When operator uruchamia `php bin/migrate.php`
|
||||||
|
Then powstaje tabela `polkurier_integration_settings` z kolumnami: `id TINYINT UNSIGNED PK` (always 1), `integration_id INT UNSIGNED UNIQUE NULL FK -> integrations(id) CASCADE`, `api_token_encrypted TEXT NULL`, `environment ENUM('production','sandbox') NOT NULL DEFAULT 'production'`, `default_label_format VARCHAR(8) NOT NULL DEFAULT 'PDF'`, `created_at`, `updated_at`
|
||||||
|
And ponowne uruchomienie migracji jest no-op (`CREATE TABLE IF NOT EXISTS`)
|
||||||
|
```
|
||||||
|
|
||||||
|
## AC-2: Repozytorium szyfruje Token API i zarzadza pojedynczym rekordem integrations
|
||||||
|
```gherkin
|
||||||
|
Given migracja AC-1 wykonana
|
||||||
|
When operator zapisuje konfiguracje przez `PolkurierIntegrationRepository::saveSettings([api_token => 'XYZ', environment => 'production', is_active => 1])`
|
||||||
|
Then w `integrations` powstaje (lub zostaje zaktualizowany) jeden rekord `type='polkurier'`, `polkurier_integration_settings.id=1` ma uzupelnione `integration_id` i zaszyfrowane `api_token_encrypted`
|
||||||
|
And `getSettings()` zwraca rekord BEZ surowego tokena, jedynie z flaga `has_api_token: bool`
|
||||||
|
And `getCredentials()` zwraca odszyfrowany Token API tylko gdy konfiguracja jest kompletna i aktywna (`is_active=1`)
|
||||||
|
```
|
||||||
|
|
||||||
|
## AC-3: Endpoint testowy realnie wywoluje API polkuriera i zapisuje wynik
|
||||||
|
```gherkin
|
||||||
|
Given operator zapisal poprawny Token API
|
||||||
|
When operator klika "Testuj polaczenie" w `/settings/integrations/polkurier`
|
||||||
|
Then `PolkurierApiClient` wykonuje realne wywolanie HTTP do API polkuriera (endpoint nie pisany na sztywno w PLAN, wybierany przez implementatora z dokumentacji `Polkurier_WebService_API_1_1.pdf` — preferowany endpoint typu "lista uslug" / "konto" zwracajacy dane bez tworzenia przesylki)
|
||||||
|
And `integrations.last_test_status / last_test_http_code / last_test_message / last_test_at` zostaja zaktualizowane przez `IntegrationsRepository::updateTestResult()`
|
||||||
|
And UI pokazuje czytelny komunikat (sukces albo blad z opisem) — bez surowego dump-u JSON/XML
|
||||||
|
And brak zaszyfrowanego tokena w logach (`storage/logs/app.log` nie zawiera plaintext tokena nawet w przypadku bledu API)
|
||||||
|
```
|
||||||
|
|
||||||
|
## AC-4: Karta polkurier w hubie integracji
|
||||||
|
```gherkin
|
||||||
|
Given konfiguracja istnieje (kompletna albo niekompletna)
|
||||||
|
When operator otwiera `/settings/integrations`
|
||||||
|
Then widzi wiersz "polkurier" z statusem: skonfigurowana (tak/nie), token zapisany (tak/nie), aktywna (tak/nie), ostatni test (timestamp + ok/error)
|
||||||
|
And klikniecie wiersza prowadzi do `/settings/integrations/polkurier`
|
||||||
|
```
|
||||||
|
|
||||||
|
## AC-5: Apaczka i istniejace ShipmentProviderRegistry netkniete
|
||||||
|
```gherkin
|
||||||
|
Given Apaczka jest aktywna i zarejestrowana w `ShipmentProviderRegistry`
|
||||||
|
When polkurier zostaje dodany do hubu integracji
|
||||||
|
Then `ShipmentProviderRegistry` NIE rejestruje polkuriera (brak `PolkurierShipmentService` w tej fazie)
|
||||||
|
And tworzenie przesylek Apaczka dziala bez zmian
|
||||||
|
And `routes/web.php` nie modyfikuje wiring ApaczkaShipmentService/Tracking
|
||||||
|
```
|
||||||
|
|
||||||
|
## AC-6: Dokumentacja zaktualizowana
|
||||||
|
```gherkin
|
||||||
|
Given plan ukonczony
|
||||||
|
When operator otwiera `.paul/codebase/db_schema.md`
|
||||||
|
Then sekcja Integrations zawiera definicje `polkurier_integration_settings`
|
||||||
|
And `.paul/codebase/architecture.md` zawiera sekcje "Phase 127 - polkurier Integration Settings" z opisem repository, api client, controller, hub
|
||||||
|
And `.paul/codebase/tech_changelog.md` zawiera wpis chronologiczny z data i opisem co + dlaczego
|
||||||
|
```
|
||||||
|
|
||||||
|
</acceptance_criteria>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 1: Migracja DB + PolkurierIntegrationRepository</name>
|
||||||
|
<files>
|
||||||
|
database/migrations/20260514_000114_create_polkurier_integration_settings.sql,
|
||||||
|
src/Modules/Settings/PolkurierIntegrationRepository.php
|
||||||
|
</files>
|
||||||
|
<action>
|
||||||
|
1) Migracja DDL `CREATE TABLE IF NOT EXISTS polkurier_integration_settings` zgodna z AC-1 (mirror `apaczka_integration_settings` + analogia do `hostedsms_integration_settings`/`smsplanet_integration_settings` w zakresie ENUM environment). InnoDB, utf8mb4_unicode_ci. FK `integration_id REFERENCES integrations(id) ON DELETE CASCADE`. Migracja MUSI byc idempotentna (re-run = no-op zgodnie z decyzja projektu: nigdy `SELECT 1;`, tylko DDL — patrz decision 2026-05-10 w PROJECT.md).
|
||||||
|
2) `PolkurierIntegrationRepository final class` w `src/Modules/Settings/`:
|
||||||
|
- konstruktor `(Medoo $db, IntegrationSecretCipher $cipher, IntegrationsRepository $integrations)`,
|
||||||
|
- `getSettings(): array` — JOIN `integrations` z `polkurier_integration_settings` po `integration_id`, zwraca `has_api_token: bool` (NIE plaintext), `environment`, `default_label_format`, `is_active`, `last_test_*`,
|
||||||
|
- `saveSettings(array $payload): void` — upsert: gdy brak rekordu `integrations.type='polkurier'`, twórz przez `IntegrationsRepository::ensureIntegration('polkurier', $name)`; gdy token jest pustym stringiem -> nie nadpisuj (BC z patternem fakturowni); inaczej zaszyfruj przez `IntegrationSecretCipher::encrypt()`. Walidacja serwerowa wymaganych pol.
|
||||||
|
- `getCredentials(): ?array` — zwraca `['api_token' => string, 'environment' => string, 'integration_id' => int]` TYLKO gdy `is_active=1` AND `api_token_encrypted IS NOT NULL`; inaczej `null`. Uzywany przez `PolkurierApiClient` i przyszly `PolkurierShipmentService`.
|
||||||
|
- `getIntegrationId(): ?int` — single source of truth dla przyszlych integracji (analogicznie do `FakturowniaIntegrationRepository`).
|
||||||
|
|
||||||
|
Avoid: tworzenia drugiego rekordu `integrations.type='polkurier'` (analogicznie do migracji konsolidacyjnej Fakturowni 20260512_000109 — single instance jest twardym kontraktem); pisania tokenu plaintext do logow; sklejania SQL stringiem (Medoo + prepared statements only).
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
`php bin/migrate.php` -> brak bledow, `SHOW CREATE TABLE polkurier_integration_settings;` -> kolumny zgodne z AC-1.
|
||||||
|
`php -r "require 'bootstrap/app.php'; $r = $app->make(PolkurierIntegrationRepository::class); var_dump($r->getSettings());"` -> array z `has_api_token=false`.
|
||||||
|
</verify>
|
||||||
|
<done>AC-1 satisfied (migracja DDL idempotentna), AC-2 satisfied (repozytorium szyfruje token, zwraca has_api_token bool, getCredentials gating na is_active).</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 2: PolkurierApiClient + Controller + widok formularza</name>
|
||||||
|
<files>
|
||||||
|
src/Modules/Settings/PolkurierApiClient.php,
|
||||||
|
src/Modules/Settings/PolkurierIntegrationController.php,
|
||||||
|
resources/views/settings/integrations/polkurier.php,
|
||||||
|
routes/web.php
|
||||||
|
</files>
|
||||||
|
<action>
|
||||||
|
1) `PolkurierApiClient final class` w `src/Modules/Settings/`:
|
||||||
|
- cURL klient z `SslCertificateResolver::resolve()` (zgodnie z patternem `FakturowniaApiClient`, `HostedSmsApiClient`, `SmsplanetApiClient`),
|
||||||
|
- PHP 8.5: ZAKAZ `curl_close()` (decision 2026-05-10 — wycieka `Deprecated` HTML przed JSON response),
|
||||||
|
- metoda `testConnection(string $apiToken, string $environment): array` zwracajaca `['ok' => bool, 'http_code' => int, 'message' => string]`,
|
||||||
|
- WYBOR endpointu testowego: implementator MUSI sprawdzic `Polkurier_WebService_API_1_1.pdf` (link w opisie API polkuriera, baza wiedzy artykul "interfejs api do pobrania") i wybrac endpoint nie tworzacy przesylki (preferencja: "lista uslug" / "dane konta" / "wycena testowa"). Wybor udokumentowac w naglowku klasy.
|
||||||
|
- przyszle stuby `createShipment()`, `downloadLabel()`, `trackShipment()`, `cancelShipment()` — rzucajace `RuntimeException("Not implemented in Phase 127")`; dolozone w kolejnych fazach.
|
||||||
|
|
||||||
|
2) `PolkurierIntegrationController final class` (mirror `HostedSmsIntegrationController` 1:1):
|
||||||
|
- routes: `GET /settings/integrations/polkurier` (`edit`), `POST /settings/integrations/polkurier/save` (CSRF `_token`), `POST /settings/integrations/polkurier/test` (CSRF `_token`),
|
||||||
|
- `test()` -> walidacja zapisanej konfiguracji -> `PolkurierApiClient::testConnection()` -> `IntegrationsRepository::updateTestResult()` -> Flash `Flash::push('success'|'danger', ...)` (Phase 120 pattern, NIE `Flash::set('polkurier.test')`),
|
||||||
|
- redirect przez `RedirectPathResolver`.
|
||||||
|
|
||||||
|
3) Widok `resources/views/settings/integrations/polkurier.php`:
|
||||||
|
- dziedziczy z `layouts/app.php`,
|
||||||
|
- formularz: token API (password input z placeholderem "Pozostaw puste aby nie zmieniac" gdy `has_api_token=true`), environment (select production/sandbox), domyslny format etykiety (PDF/ZPL/EPL), checkbox `is_active`, przycisk "Zapisz" i osobny "Testuj polaczenie",
|
||||||
|
- `_token` na obu formularzach (CSRF, nie `_csrf_token` — decision 2026-03-13),
|
||||||
|
- alerty wylacznie przez komponent `resources/views/components/alert.php` (Phase 120 contract — NIE inline `<div class="alert alert--*">`),
|
||||||
|
- potwierdzenia akcji destrukcyjnych (na przyszlosc) przez `window.OrderProAlerts.confirm({...})` options-object API (decision Phase 114/120),
|
||||||
|
- bez inline `<style>` — style przez `resources/scss/modules/_integrations.scss` jezeli czegokolwiek brakuje (CLAUDE.md: zero CSS w widokach).
|
||||||
|
|
||||||
|
4) `routes/web.php` — wpiac controller w sekcji Settings/Integrations (po Apaczka, przed HostedSMS dla porzadku alfabetycznego). DI: `new PolkurierIntegrationController($template, $translator, $auth, new PolkurierIntegrationRepository($app->db(), $cipher, $integrationsRepository), new PolkurierApiClient($timeoutSeconds = 15))`.
|
||||||
|
|
||||||
|
Avoid: kopiowania `apaczka.php` bez przegladu (Apaczka ma duzy formularz z domyslnymi wymiarami — niepotrzebne w MVP polkuriera); dodawania `PolkurierShipmentService` do `ShipmentProviderRegistry` (out of scope, AC-5 wymaga netknietego registry); modyfikacji `apaczka_integration_settings` lub `IntegrationsRepository` poza `updateTestResult()` (reuse, nie refactor).
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
Build PHP `php -l src/Modules/Settings/PolkurierApiClient.php` i `php -l src/Modules/Settings/PolkurierIntegrationController.php` -> No syntax errors.
|
||||||
|
Recznie: zaloguj sie, otworz `/settings/integrations/polkurier` -> formularz renderuje sie bez bledow, alerty stylowane.
|
||||||
|
Zapis pustego tokenu -> blad walidacji. Zapis prawdziwego tokenu (dev konto polkurier jezeli operator ma) -> rekord w DB ma niepuste `api_token_encrypted`.
|
||||||
|
Klik "Testuj polaczenie" z prawdziwym tokenem -> `integrations.last_test_status='ok'` w DB; bledny token -> `last_test_status='error'` + zrozumialy komunikat w UI.
|
||||||
|
</verify>
|
||||||
|
<done>AC-3 satisfied (realne wywolanie API, zapis wyniku, brak plaintext tokena w logach); fundament UI/wiring na miejscu.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 3: Hub integracji + aktualizacja dokumentow projektowych</name>
|
||||||
|
<files>
|
||||||
|
src/Modules/Settings/IntegrationsHubController.php,
|
||||||
|
resources/views/settings/integrations/index.php,
|
||||||
|
.paul/codebase/db_schema.md,
|
||||||
|
.paul/codebase/architecture.md,
|
||||||
|
.paul/codebase/tech_changelog.md
|
||||||
|
</files>
|
||||||
|
<action>
|
||||||
|
1) `IntegrationsHubController`:
|
||||||
|
- dodaj parametr konstruktora `PolkurierIntegrationRepository $polkurier`,
|
||||||
|
- dodaj metode `buildPolkurierRow(): array` zwracajaca te same klucze co `buildApaczkaRow()` (`name`, `configured`, `has_secret`, `is_active`, `last_test_status`, `last_test_at`, `last_test_message`, `link`),
|
||||||
|
- wcisniecie wiersza do listy w `index()` (kolejnosc: po Apaczka, przed Allegro/inni — sprawdz aktualny porzadek w `index.php`),
|
||||||
|
- `routes/web.php` - rozszerzyc wiring kontrolera o instancje `PolkurierIntegrationRepository` (kompatybilnie z istniejacymi 5+ params).
|
||||||
|
|
||||||
|
2) `resources/views/settings/integrations/index.php`:
|
||||||
|
- jezeli widok generuje wiersze z tablicy zwracanej przez controller, ZADNA zmiana widoku nie jest potrzebna,
|
||||||
|
- jezeli widok ma hardkodowana liste rzedow (sprawdz przed edycja) — dodaj wiersz polkurier w tym samym wzorcu co Apaczka.
|
||||||
|
|
||||||
|
3) `.paul/codebase/db_schema.md` — sekcja Integrations, po `apaczka_integration_settings`:
|
||||||
|
- dodac pelna tabele kolumn `polkurier_integration_settings` (zgodnie z AC-1),
|
||||||
|
- oznaczenie `(Phase 127; fixed 1 row)` w naglowku tabeli,
|
||||||
|
- update licznika `Total tables: 62` i `Updated: 2026-05-14`.
|
||||||
|
|
||||||
|
4) `.paul/codebase/architecture.md`:
|
||||||
|
- dodac sekcje `## Phase 127 — polkurier Integration Settings` po `## Phase 117 - SMSPLANET Integration Settings`,
|
||||||
|
- opis `PolkurierIntegrationRepository`, `PolkurierApiClient`, `PolkurierIntegrationController`, integracja z `IntegrationsHubController`,
|
||||||
|
- zaznaczyc ze `ShipmentProviderRegistry` nie zostal zmodyfikowany (deferred do osobnej fazy).
|
||||||
|
|
||||||
|
5) `.paul/codebase/tech_changelog.md` — dopisac wpis chronologiczny z data 2026-05-14:
|
||||||
|
- co: fundament polkurier integration (settings + test polaczenia),
|
||||||
|
- dlaczego: alternatywa dla Apaczki na zyczenie operatora,
|
||||||
|
- referencja do Phase 127.
|
||||||
|
|
||||||
|
Avoid: dotykania PROJECT.md (Decisions / Validated Requirements) — to robi UNIFY, nie APPLY; modyfikacji ROADMAP.md (robi to /paul:plan w step update_state); zmiany schematu innych tabel.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
`/settings/integrations` -> widac wiersz polkurier z prawidlowym statusem konfiguracji.
|
||||||
|
`grep -c "polkurier_integration_settings" .paul/codebase/db_schema.md` -> co najmniej 1 trafienie.
|
||||||
|
`grep -c "Phase 127" .paul/codebase/architecture.md` -> co najmniej 1 trafienie.
|
||||||
|
Apaczka wiersz w hubie nadal sie renderuje (regresja zero — AC-5).
|
||||||
|
</verify>
|
||||||
|
<done>AC-4 satisfied (karta polkurier w hubie), AC-5 satisfied (Apaczka netknieta), AC-6 satisfied (dokumenty zaktualizowane).</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<boundaries>
|
||||||
|
|
||||||
|
## DO NOT CHANGE
|
||||||
|
- `src/Modules/Shipments/*` — caly modul Shipments (registries, ApaczkaShipmentService, InpostShipmentService, ShipmentController). `PolkurierShipmentService` to osobna faza.
|
||||||
|
- `src/Modules/Settings/Apaczka*.php` — Apaczka netknieta.
|
||||||
|
- `database/migrations/*` istniejace pliki — tylko nowa migracja `20260514_000114_*`.
|
||||||
|
- `src/Modules/Settings/IntegrationSecretCipher.php` — reuse, zero refaktoru.
|
||||||
|
- `routes/web.php` istniejace routy — tylko dodajemy 3 nowe (polkurier GET/save/test).
|
||||||
|
- `resources/views/components/alert.php` — Phase 120 contract, zero modyfikacji.
|
||||||
|
|
||||||
|
## SCOPE LIMITS
|
||||||
|
- Brak `PolkurierShipmentService` (tworzenie przesylki) — kolejna faza.
|
||||||
|
- Brak `PolkurierTrackingService` (delivery polling) — kolejna faza po Shipment.
|
||||||
|
- Brak wpisow w `delivery_status_mappings` (provider='polkurier') — wymagaja realnego API tracking, do osobnej fazy.
|
||||||
|
- Brak mapowan metod dostawy w UI (`order_delivery_method -> polkurier service`) — wymagaja modelowania w osobnej fazie po analizie listy uslug API polkuriera.
|
||||||
|
- Brak zmian w `shipment_presets` schemacie ani UI presetow — presety beda potem.
|
||||||
|
- Brak migracji konsolidujacych z Apaczka — oba dostawcy zyja niezaleznie.
|
||||||
|
- Brak `ShipmentProviderRegistry::register('polkurier', ...)` — out of scope.
|
||||||
|
- Brak `.env` / `app_settings` flag globalnych — token siedzi tylko w `polkurier_integration_settings` (jak Apaczka/HostedSMS/SMSPLANET).
|
||||||
|
|
||||||
|
</boundaries>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
Before declaring plan complete:
|
||||||
|
- [ ] `php bin/migrate.php` przeszla bez bledow (operator manualnie po wdrozeniu — XAMPP online).
|
||||||
|
- [ ] `php -l` przeszedl dla wszystkich nowych plikow PHP (bez syntax errors).
|
||||||
|
- [ ] `/settings/integrations` renderuje wiersz polkurier obok Apaczki.
|
||||||
|
- [ ] `/settings/integrations/polkurier` formularz dziala: zapis, ponowne wczytanie, "Testuj polaczenie" zwraca rzeczywista odpowiedz API (operator wpisuje prawdziwy token).
|
||||||
|
- [ ] Apaczka konfiguracja `/settings/integrations/apaczka` dziala bez regresji.
|
||||||
|
- [ ] `ShipmentProviderRegistry` nie zna polkuriera (grep brak `polkurier` w `src/Modules/Shipments/ShipmentProviderRegistry.php`).
|
||||||
|
- [ ] Wszystkie acceptance criteria spelnione.
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- Single-instance globalna konfiguracja polkurier zapisuje sie i odczytuje (Token zaszyfrowany, `has_api_token` flag w UI).
|
||||||
|
- Realne wywolanie API polkuriera w trybie test zwraca status (ok/error) i jest widoczne w hubie i panelu integracji.
|
||||||
|
- Apaczka dziala bez regresji obok polkuriera.
|
||||||
|
- Dokumentacja codebase (`db_schema.md`, `architecture.md`, `tech_changelog.md`) zaktualizowana.
|
||||||
|
- Zaden plik z `boundaries.DO NOT CHANGE` nie zostal zmodyfikowany.
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create `.paul/phases/127-polkurier-integration-foundation/127-01-SUMMARY.md`.
|
||||||
|
</output>
|
||||||
@@ -0,0 +1,201 @@
|
|||||||
|
---
|
||||||
|
phase: 127-polkurier-integration-foundation
|
||||||
|
plan: 01
|
||||||
|
subsystem: integrations
|
||||||
|
tags: [polkurier, courier, shipment-broker, settings, integration-hub, php]
|
||||||
|
|
||||||
|
requires:
|
||||||
|
- phase: 116-hostedsms-integration-settings
|
||||||
|
provides: single-instance integration repository pattern (IntegrationsRepository::ensureIntegration + updateTestResult + IntegrationSecretCipher)
|
||||||
|
- phase: 120-alert-component-unification
|
||||||
|
provides: resources/views/components/alert.php contract for all settings views
|
||||||
|
provides:
|
||||||
|
- polkurier_integration_settings DB table (single-instance, fixed id=1)
|
||||||
|
- PolkurierIntegrationRepository (login + Token API, AES-encrypted)
|
||||||
|
- PolkurierApiClient with verified live test connection against apimetod=test_auth_api
|
||||||
|
- /settings/integrations/polkurier UI (form + Testuj polaczenie) + hub row
|
||||||
|
- Foundation for future PolkurierShipmentService / PolkurierTrackingService
|
||||||
|
affects:
|
||||||
|
- future polkurier-shipment-service phase (uses getCredentials + verified API client contract)
|
||||||
|
- future polkurier-tracking-service phase (delivery_status_mappings provider='polkurier')
|
||||||
|
|
||||||
|
tech-stack:
|
||||||
|
added: []
|
||||||
|
patterns:
|
||||||
|
- single-instance integration ctor (PDO $pdo, string $secret) mirror HostedSMS/SMSPLANET
|
||||||
|
- polkurier API contract: POST https://api.polkurier.pl/, JSON body {authorization:{login,token}, apimetod, data:{platform, platform_version}}, success when top-level status='success'
|
||||||
|
- error path: payload from "response" field of envelope (string or struct) — mirror SDK ErrorException($response->get('response'))
|
||||||
|
- strict Content-Type: application/json (no charset suffix — polkurier rejects)
|
||||||
|
|
||||||
|
key-files:
|
||||||
|
created:
|
||||||
|
- database/migrations/20260514_000114_create_polkurier_integration_settings.sql
|
||||||
|
- src/Modules/Settings/PolkurierIntegrationRepository.php
|
||||||
|
- src/Modules/Settings/PolkurierApiClient.php
|
||||||
|
- src/Modules/Settings/PolkurierIntegrationController.php
|
||||||
|
- resources/views/settings/polkurier.php
|
||||||
|
modified:
|
||||||
|
- routes/web.php
|
||||||
|
- src/Modules/Settings/IntegrationsHubController.php
|
||||||
|
- resources/lang/pl.php
|
||||||
|
- .paul/codebase/db_schema.md
|
||||||
|
- .paul/codebase/architecture.md
|
||||||
|
- .paul/codebase/tech_changelog.md
|
||||||
|
|
||||||
|
key-decisions:
|
||||||
|
- "polkurier startuje jako single-instance globalna konfiguracja (mirror Apaczka/HostedSMS/SMSPLANET) — operator ma jedno konto polkurier"
|
||||||
|
- "polkurier dziala obok Apaczki — ShipmentProviderRegistry netkniety; oba dostawcy zyja niezaleznie"
|
||||||
|
- "API polkuriera wymaga login + token w body authorization (zweryfikowane w SDK polkurier-sdk); kolumna login dodana mimo ze PLAN AC-1 jej nie wymagal"
|
||||||
|
- "Brak kolumny environment ENUM — polkurier ma jeden produkcyjny endpoint, sandbox nie istnieje"
|
||||||
|
- "Test polaczenia uzywa apimetod=test_auth_api (nie tworzy przesylki, nie kosztuje); sukces gdy top-level status='success'"
|
||||||
|
- "Content-Type MUSI byc dokladnie 'application/json' — polkurier odrzuca '; charset=UTF-8' suffix"
|
||||||
|
|
||||||
|
patterns-established:
|
||||||
|
- "polkurier API client: jeden POST endpoint, ResponseStatus::SUCCESS='success', tresc bledu w polu 'response' envelope'a — wzorzec dla wszystkich przyszlych metod (createShipment, getLabel, getStatus, cancelOrder, AvailableCarriers, etc.)"
|
||||||
|
- "Strict Content-Type bez charset suffix — pattern do reuse w innych integracjach jezeli odrzucaja parametry"
|
||||||
|
|
||||||
|
duration: ~45min
|
||||||
|
started: 2026-05-14T19:00:00Z
|
||||||
|
completed: 2026-05-14T19:45:00Z
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 127 Plan 01: polkurier Integration Foundation — Summary
|
||||||
|
|
||||||
|
**polkurier.pl broker kurierski dostepny jako alternatywa dla Apaczki: pojedyncza globalna konfiguracja w `/settings/integrations/polkurier` z zaszyfrowanym Token API + loginem, realny test polaczenia przez `apimetod=test_auth_api` zweryfikowany na produkcyjnym koncie operatora (`Autoryzacja: 1`).**
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
| Metric | Value |
|
||||||
|
|--------|-------|
|
||||||
|
| Duration | ~45min (incl. live API debugging) |
|
||||||
|
| Started | 2026-05-14T19:00:00Z |
|
||||||
|
| Completed | 2026-05-14T19:45:00Z |
|
||||||
|
| Tasks | 3 of 3 completed |
|
||||||
|
| Files created | 5 |
|
||||||
|
| Files modified | 6 |
|
||||||
|
|
||||||
|
## Acceptance Criteria Results
|
||||||
|
|
||||||
|
| Criterion | Status | Notes |
|
||||||
|
|-----------|--------|-------|
|
||||||
|
| AC-1: Migracja tworzy single-instance tabele konfiguracji | Pass (modified) | DDL idempotentny. **Modyfikacja:** kolumna `environment ENUM` pominieta (polkurier nie ma sandbox); dodana kolumna `login VARCHAR(190)` (polkurier wymaga login+token, nie samego tokena). |
|
||||||
|
| AC-2: Repozytorium szyfruje Token API i zarzadza pojedynczym rekordem integrations | Pass | `getSettings()` zwraca `has_api_token: bool`, `saveSettings()` szyfruje przez `IntegrationSecretCipher`, `getCredentials()` gates na `is_active=1`. |
|
||||||
|
| AC-3: Endpoint testowy realnie wywoluje API polkuriera i zapisuje wynik | Pass (live verified) | Operator potwierdzil: `Polaczenie z polkurier dziala. Autoryzacja: 1` (response z `apimetod=test_auth_api`). `IntegrationsRepository::updateTestResult()` zapisuje wynik. |
|
||||||
|
| AC-4: Karta polkurier w hubie integracji | Pass | `buildPolkurierRow()` w `IntegrationsHubController` wstawia wiersz po Apaczce (semantycznie sasiednie). |
|
||||||
|
| AC-5: Apaczka i istniejace ShipmentProviderRegistry netkniete | Pass | Zerowe modyfikacje w `src/Modules/Shipments/*` i `Apaczka*`. Grep `polkurier` w `ShipmentProviderRegistry.php` -> 0 trafien. |
|
||||||
|
| AC-6: Dokumentacja zaktualizowana | Pass | `db_schema.md` +1 tabela (62 total), `architecture.md` +sekcja Phase 127, `tech_changelog.md` +wpis z deviation. |
|
||||||
|
|
||||||
|
## Accomplishments
|
||||||
|
|
||||||
|
- polkurier.pl wpiety jako drugi broker kurierski (obok Apaczki) — fundament gotowy i zweryfikowany na zywym API operatora.
|
||||||
|
- Kontrakt API polkuriera zweryfikowany i udokumentowany w `architecture.md`: POST `https://api.polkurier.pl/`, JSON `{authorization:{login,token}, apimetod, data:{platform, platform_version}}`, sukces gdy `status='success'`, tresc bledu w polu `response` envelope'a.
|
||||||
|
- 4 buggi z pierwszego draftu naprawione live (3 podczas testow operatora) — finalna implementacja sprawdzona na realnym Token API.
|
||||||
|
|
||||||
|
## Task Commits
|
||||||
|
|
||||||
|
Commits jeszcze nie utworzone (czekaja na transition step). Calosc fazy 127 zostanie zacommitowana jako jeden `feat(127):` commit.
|
||||||
|
|
||||||
|
| Task | Commit | Type | Description |
|
||||||
|
|------|--------|------|-------------|
|
||||||
|
| Task 1: Migracja + Repository | (pending) | feat | DDL + PolkurierIntegrationRepository |
|
||||||
|
| Task 2: ApiClient + Controller + widok + routy | (pending) | feat | PolkurierApiClient + Controller + view + i18n + DI |
|
||||||
|
| Task 3: Hub + dokumentacja codebase | (pending) | feat | IntegrationsHubController buildPolkurierRow + db_schema/architecture/tech_changelog |
|
||||||
|
|
||||||
|
## Files Created/Modified
|
||||||
|
|
||||||
|
| File | Change | Purpose |
|
||||||
|
|------|--------|---------|
|
||||||
|
| `database/migrations/20260514_000114_create_polkurier_integration_settings.sql` | Created | DDL tabeli + seed `integrations.type='polkurier'` (idempotentny) |
|
||||||
|
| `src/Modules/Settings/PolkurierIntegrationRepository.php` | Created | Single-instance repo, szyfrowanie tokena, getCredentials z gating na is_active |
|
||||||
|
| `src/Modules/Settings/PolkurierApiClient.php` | Created | POST do api.polkurier.pl, testConnection z apimetod=test_auth_api, stuby createShipment/getLabel/getStatus/cancelOrder |
|
||||||
|
| `src/Modules/Settings/PolkurierIntegrationController.php` | Created | GET/save/test endpointy z CSRF, flash, RedirectPathResolver |
|
||||||
|
| `resources/views/settings/polkurier.php` | Created | Formularz konfiguracji + Test polaczenia, alerty przez komponent alert.php |
|
||||||
|
| `routes/web.php` | Modified | DI wiring (Repo+Controller) + 3 routy + ctor IntegrationsHubController |
|
||||||
|
| `src/Modules/Settings/IntegrationsHubController.php` | Modified | +param polkurier + buildPolkurierRow() + wstawienie wiersza po Apaczce |
|
||||||
|
| `resources/lang/pl.php` | Modified | settings.polkurier.* + providers.polkurier |
|
||||||
|
| `.paul/codebase/db_schema.md` | Modified | +tabela polkurier_integration_settings, 61->62 |
|
||||||
|
| `.paul/codebase/architecture.md` | Modified | +sekcja Phase 127 |
|
||||||
|
| `.paul/codebase/tech_changelog.md` | Modified | +wpis 2026-05-14 z deviation |
|
||||||
|
|
||||||
|
## Decisions Made
|
||||||
|
|
||||||
|
| Decision | Rationale | Impact |
|
||||||
|
|----------|-----------|--------|
|
||||||
|
| Kolumna `login VARCHAR(190)` w tabeli zamiast samego `api_token` | API polkuriera (zweryfikowane w SDK Auth.php/Request.php) wymaga login+token w body authorization, nie samego tokena | Wszystkie przyszle wywolania API musza miec login z `getCredentials()['login']` |
|
||||||
|
| Pominieta kolumna `environment ENUM('production','sandbox')` z PLAN AC-1 | polkurier nie ma osobnego srodowiska sandbox (jeden URL: https://api.polkurier.pl/) | YAGNI; jezeli polkurier doda sandbox, dolozymy migracja `ALTER TABLE ... ADD COLUMN` |
|
||||||
|
| Wykonanie planu inline zamiast delegated:auto z planu | Swiezy kontekst API research (Config/Auth/Methods z polkurier-sdk) — agent musialby ten research powtorzyc | Brak; boundaries i AC niezmienione, deviation udokumentowana |
|
||||||
|
| `Content-Type: application/json` (bez `; charset=UTF-8` suffix) | polkurier API zwraca `Content type must be: application/json` gdy header ma charset suffix | Pattern do reuse jezeli inne integracje sa rownie strict |
|
||||||
|
| `ResponseStatus::SUCCESS = 'success'` (nie `'ok'`) | Zweryfikowane w `src/Type/ResponseStatus.php` SDK polkuriera | Wszystkie przyszle metody API musza sprawdzac `status === 'success'` |
|
||||||
|
| Tresc bledu z pola `response` envelope'a (nie `error_message`) | SDK polkuriera rzuca `ErrorException($response->get('response'))` gdy status != success | Wzorzec parser bledu dla wszystkich przyszlych metod API |
|
||||||
|
|
||||||
|
## Deviations from Plan
|
||||||
|
|
||||||
|
### Summary
|
||||||
|
|
||||||
|
| Type | Count | Impact |
|
||||||
|
|------|-------|--------|
|
||||||
|
| Auto-fixed | 3 | Krytyczne — bez tych poprawek test polaczenia nie zwracalby `success` |
|
||||||
|
| Scope additions | 1 | Kolumna `login` w schemacie (poza zakresem AC-1 — wymagana przez kontrakt API) |
|
||||||
|
| Scope removals | 1 | Kolumna `environment` z AC-1 pominieta (YAGNI) |
|
||||||
|
| Execution mode | 1 | Plan: `delegation:auto`. Faktycznie: inline. Boundaries i AC niezmienione. |
|
||||||
|
| Deferred | 0 | Brak |
|
||||||
|
|
||||||
|
**Total impact:** Wszystkie deviacje wymuszone realnym kontraktem API polkuriera. Plan z chwili pisania bazowal na publicznym opisie API; szczegoly (login, status='success', strict Content-Type) wyplynely dopiero przy weryfikacji SDK i testach na zywym koncie operatora.
|
||||||
|
|
||||||
|
### Auto-fixed Issues
|
||||||
|
|
||||||
|
**1. [API contract] `status === 'ok'` -> `status === 'success'`**
|
||||||
|
- **Found during:** Live test po Task 2 (operator zglosil `Status: error`)
|
||||||
|
- **Issue:** Kod sprawdzal `$status === 'ok'`, ale `ResponseStatus::SUCCESS` w SDK polkuriera = `'success'`
|
||||||
|
- **Fix:** Zmiana porownania na `$status === 'success'`; parser bledu zaktualizowany do pobierania tresci z pola `response` envelope'a (mirror SDK ErrorException)
|
||||||
|
- **Files:** `src/Modules/Settings/PolkurierApiClient.php`
|
||||||
|
- **Verification:** Drugi test operatora — komunikat `Content type must be: application/json` (faktyczna tresc z polkuriera, nie generyczne `Status: error`)
|
||||||
|
- **Commit:** TBD (przy transition)
|
||||||
|
|
||||||
|
**2. [HTTP headers] Content-Type strict**
|
||||||
|
- **Found during:** Live test po fix #1 (operator zglosil `Content type must be: application/json`)
|
||||||
|
- **Issue:** Header `Content-Type: application/json; charset=UTF-8` — polkurier robi strict equality check i odrzuca suffix `; charset=UTF-8`
|
||||||
|
- **Fix:** Zmiana na `Content-Type: application/json` (sam mime, bez parametrow)
|
||||||
|
- **Files:** `src/Modules/Settings/PolkurierApiClient.php`
|
||||||
|
- **Verification:** Trzeci test operatora — `Polaczenie z polkurier dziala. Autoryzacja: 1` (sukces)
|
||||||
|
- **Commit:** TBD (przy transition)
|
||||||
|
|
||||||
|
**3. [Error reporting] Brak tresci bledu w komunikacie UI**
|
||||||
|
- **Found during:** Live test po Task 2 (`Status: error` bez detali)
|
||||||
|
- **Issue:** Komunikat fallback `'Status: ' . $status` byl nieczytelny; tresc bledu z polkuriera siedzi w polu `response` envelope'a, nie `error_message` top-level
|
||||||
|
- **Fix:** Parser bledu czyta `response` field (string albo zagniezdzona struktura `error_message/errorMessage/message/error`), z fallbackiem na top-level `error_message/message/error` i finalnie `Status: X (HTTP Y)`
|
||||||
|
- **Files:** `src/Modules/Settings/PolkurierApiClient.php`
|
||||||
|
- **Verification:** Fix #2 mozliwy tylko dzieki temu (operator zobaczyl `Content type must be: application/json` zamiast `Status: error`)
|
||||||
|
- **Commit:** TBD (przy transition)
|
||||||
|
|
||||||
|
### Deferred Items
|
||||||
|
|
||||||
|
Brak — kontrakt API operatora zweryfikowany, fundament zamkniety. Kolejne fazy (PolkurierShipmentService, PolkurierTrackingService) sa zaplanowane jako oddzielne, swiadomie poza zakresem 127 (PLAN boundaries `SCOPE LIMITS`).
|
||||||
|
|
||||||
|
## Issues Encountered
|
||||||
|
|
||||||
|
| Issue | Resolution |
|
||||||
|
|-------|------------|
|
||||||
|
| Operator: `Status: error` po pierwszym smoke | Fix #1 (status='success') + #3 (parser bledu) — operator widzi teraz realny komunikat polkuriera |
|
||||||
|
| Operator: `Content type must be: application/json` po fix #1 | Fix #2 (strict Content-Type bez charset suffix) |
|
||||||
|
| API research nieobecny przed planem | Pre-APPLY fetche SDK polkurier-sdk (Auth/Request/Methods/Config/ResponseStatus) — kontrakt zrekonstruowany przed implementacja |
|
||||||
|
|
||||||
|
## Next Phase Readiness
|
||||||
|
|
||||||
|
**Ready:**
|
||||||
|
- `PolkurierIntegrationRepository::getCredentials()` zwraca odszyfrowany `login + api_token + default_label_format` — gotowe do uzycia w `PolkurierShipmentService`.
|
||||||
|
- `PolkurierApiClient` ma zweryfikowany kontrakt POST (single endpoint, JSON body, status='success', error w `response`) + stuby `createShipment/getLabel/getStatus/cancelOrder` z `RuntimeException("Not implemented in Phase 127")` jako placeholder dla nastepnej fazy.
|
||||||
|
- 36 metod SDK polkuriera zidentyfikowanych: `AvailableCarriers`, `OrderValuationV2`, `CreateOrder`, `GetLabel`, `GetStatus`, `CancelOrder`, `InpostParcelMachines`, `PocztexPostOffices`, `Kurier48PostOffices`, `GetCourierPoint`, `Heartbeat`, etc. — gotowe do mapowania w kolejnych planach.
|
||||||
|
- Hub integracji pokazuje stan polkuriera obok Apaczki — operator widzi obie integracje rownolegle.
|
||||||
|
|
||||||
|
**Concerns:**
|
||||||
|
- Brak `PolkurierShipmentService` — operator nie moze jeszcze nadawac przesylek przez polkuriera. Zgodne z PLAN scope (`SCOPE LIMITS`).
|
||||||
|
- Brak mapowan metod dostawy `order_delivery_method -> polkurier service` — wymaga analizy listy uslug z `AvailableCarriers` API.
|
||||||
|
- Brak `delivery_status_mappings` dla `provider='polkurier'` — tracking polling rowniez deferred.
|
||||||
|
|
||||||
|
**Blockers:**
|
||||||
|
- Operator musi uruchomic `php bin/migrate.php` na zywej bazie (XAMPP) zeby zalozyc tabele i seed rekord `integrations.type='polkurier'`. AKTUALNIE migracja juz uruchomiona (test polaczenia dzialal, wiec rekord `integrations` istnieje).
|
||||||
|
|
||||||
|
---
|
||||||
|
*Phase: 127-polkurier-integration-foundation, Plan: 01*
|
||||||
|
*Completed: 2026-05-14*
|
||||||
392
.paul/phases/128-polkurier-shipment-service/128-01-PLAN.md
Normal file
392
.paul/phases/128-polkurier-shipment-service/128-01-PLAN.md
Normal file
@@ -0,0 +1,392 @@
|
|||||||
|
---
|
||||||
|
phase: 128-polkurier-shipment-service
|
||||||
|
plan: 01
|
||||||
|
type: execute
|
||||||
|
wave: 1
|
||||||
|
depends_on: []
|
||||||
|
files_modified:
|
||||||
|
- src/Modules/Settings/PolkurierApiClient.php
|
||||||
|
- src/Modules/Shipments/PolkurierShipmentService.php
|
||||||
|
- src/Modules/Shipments/PolkurierTrackingService.php
|
||||||
|
- src/Modules/Shipments/ShipmentController.php
|
||||||
|
- src/Modules/Shipments/DeliveryStatus.php
|
||||||
|
- src/Modules/Cron/CronHandlerFactory.php
|
||||||
|
- routes/web.php
|
||||||
|
- resources/views/shipments/prepare.php
|
||||||
|
- database/migrations/20260514_000115_seed_polkurier_delivery_status_mappings.sql
|
||||||
|
- .paul/codebase/architecture.md
|
||||||
|
- .paul/codebase/db_schema.md
|
||||||
|
- .paul/codebase/tech_changelog.md
|
||||||
|
autonomous: false
|
||||||
|
delegation: off
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
## Goal
|
||||||
|
Dostarczyc pelna integracje wysylkowa polkurier.pl: tworzenie paczek (createOrder), pobieranie etykiet (getLabel), tracking statusow przesylek (getStatus) oraz UI do nadawania paczek w `/orders/{id}/shipment/prepare` z dynamiczna lista przewoznikow z polkuriera i obsluga punktow odbioru (Paczkomaty InPost, ORLEN, Pocztex, Kurier48). Weryfikacja na zywych zamowieniach #114 i #115 z manualnym anulowaniem po teście.
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
Phase 127 dostarczyl fundament (settings + test_auth_api). Bez ShipmentService polkurier jest "wystawiony w hubie integracji ale niedzialajacy". Operator chce realnie nadawac paczki przez polkurier obok Apaczki — wspolny use case (DPD, UPS, GLS, InPost) z lepszymi cenami.
|
||||||
|
|
||||||
|
## Output
|
||||||
|
- 2 nowe klasy w `src/Modules/Shipments/` (`PolkurierShipmentService`, `PolkurierTrackingService`) implementujace odpowiednio `ShipmentProviderInterface` i `ShipmentTrackingInterface`
|
||||||
|
- Rozszerzony `PolkurierApiClient` z 6 nowymi metodami API
|
||||||
|
- Nowy panel "polkurier" w `prepare.php` + przelacznik JS
|
||||||
|
- Migracja seedujaca `delivery_status_mappings` (provider='polkurier')
|
||||||
|
- Architektura/schema/changelog zaktualizowane
|
||||||
|
- 2 paczki utworzone na zywym koncie polkurier (#114 i #115), zweryfikowane, recznie anulowane przez operatora
|
||||||
|
</objective>
|
||||||
|
|
||||||
|
<context>
|
||||||
|
|
||||||
|
<clarifications>
|
||||||
|
- **Zakres fazy** — Co dostarczamy w Phase 128?
|
||||||
|
→ Odpowiedz: Pelny zakres: ShipmentService + TrackingService + UI prepare + delivery_status_mappings (jedna duza faza zamiast dwoch).
|
||||||
|
- **Uslugi UI** — Jak prezentujemy uslugi polkurier w UI prepare?
|
||||||
|
→ Odpowiedz: Dynamiczna lista z API polkuriera (`get_available_carriers` lub odpowiednia metoda) — analog `ApaczkaApiClient::getServiceStructure`.
|
||||||
|
- **Paczkomaty** — Czy obslugujemy paczkomaty/punkty odbioru w Phase 128?
|
||||||
|
→ Odpowiedz: Pelne wsparcie wszystkich punktow (InpostParcelMachines, PocztexPostOffices, Kurier48PostOffices, ewentualnie ORLEN).
|
||||||
|
- **Tryb testu** — Jak testujemy na #114/#115?
|
||||||
|
→ Odpowiedz: Realny createOrder na zywym koncie + manualny cancelOrder po weryfikacji przez operatora w panelu polkurier (live test, ale bez wysylki).
|
||||||
|
</clarifications>
|
||||||
|
|
||||||
|
## Project Context
|
||||||
|
@.paul/PROJECT.md
|
||||||
|
@.paul/ROADMAP.md
|
||||||
|
@.paul/STATE.md
|
||||||
|
@.paul/codebase/architecture.md
|
||||||
|
@.paul/codebase/db_schema.md
|
||||||
|
|
||||||
|
## Prior Work
|
||||||
|
@.paul/phases/127-polkurier-integration-foundation/127-01-SUMMARY.md
|
||||||
|
|
||||||
|
## Source Files (wzorzec Apaczka)
|
||||||
|
@src/Modules/Shipments/ApaczkaShipmentService.php
|
||||||
|
@src/Modules/Shipments/ApaczkaTrackingService.php
|
||||||
|
@src/Modules/Settings/ApaczkaApiClient.php
|
||||||
|
@src/Modules/Shipments/ShipmentProviderInterface.php
|
||||||
|
@src/Modules/Shipments/ShipmentTrackingInterface.php
|
||||||
|
@src/Modules/Shipments/ShipmentProviderRegistry.php
|
||||||
|
@src/Modules/Settings/PolkurierApiClient.php
|
||||||
|
@src/Modules/Settings/PolkurierIntegrationRepository.php
|
||||||
|
@src/Modules/Shipments/ShipmentController.php
|
||||||
|
@src/Modules/Cron/CronHandlerFactory.php
|
||||||
|
@resources/views/shipments/prepare.php
|
||||||
|
@routes/web.php
|
||||||
|
|
||||||
|
## External Reference
|
||||||
|
- Oficjalne polkurier SDK: https://github.com/Polkurier/polkurier-sdk (zweryfikowany kontrakt API w Phase 127)
|
||||||
|
- Klucze apimetod w SDK: `test_auth_api`, `new_order`, `get_label`, `get_status`, `cancel_order`, `get_available_carriers`, `get_parcel_machines`, `get_post_offices`, `get_carrier_info` (do potwierdzenia per SDK)
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<acceptance_criteria>
|
||||||
|
|
||||||
|
## AC-1: PolkurierApiClient — pelny kontrakt API
|
||||||
|
```gherkin
|
||||||
|
Given globalna konfiguracja polkurier jest aktywna i token zwery (Phase 127)
|
||||||
|
When backend wywoluje `PolkurierApiClient::getAvailableCarriers()` z poprawnymi credentials
|
||||||
|
Then klient zwraca tablice przewoznikow (DPD/UPS/GLS/InPost/Pocztex…) z polami: `carrier_id`, `name`, `service_code`, `supports_pickup_point` (bool), `weight_limits`, `cod_supported`
|
||||||
|
And `createShipment($payload)` zwraca tablice `{order_id: string, tracking_number: string, label_url: ?string, raw: array}` przy `status='success'`
|
||||||
|
And `getLabel($orderId, $format)` zwraca binarna zawartosc etykiety (PDF/ZPL/EPL zaleznie od `default_label_format`)
|
||||||
|
And `getStatus($orderId)` zwraca tablice `{status_code, status_name, status_date}`
|
||||||
|
And `cancelOrder($orderId)` zwraca `{ok: bool, message: string}`
|
||||||
|
And kazda metoda przy `status != 'success'` rzuca `RuntimeException` z trescia z pola `response`
|
||||||
|
```
|
||||||
|
|
||||||
|
## AC-2: PolkurierShipmentService implementuje ShipmentProviderInterface
|
||||||
|
```gherkin
|
||||||
|
Given `PolkurierShipmentService` jest zarejestrowany w `ShipmentProviderRegistry` jako `code()='polkurier'`
|
||||||
|
When `createShipment(int $orderId, array $formData)` jest wywolane z danymi zamowienia #114 lub #115
|
||||||
|
Then service buduje payload polkurier (sender z `company_settings`, receiver z order_addresses, paczka z formData), woła `PolkurierApiClient::createShipment()`, zapisuje wynik do `shipment_packages` (provider='polkurier', tracking_number, label_path po pobraniu, payload_json)
|
||||||
|
And `downloadLabel($packageId, $storagePath)` pobiera plik etykiety i aktualizuje `shipment_packages.label_path`
|
||||||
|
And `checkCreationStatus($packageId)` zwraca aktualny stan z `shipment_packages` (sync vs `getStatus()` API)
|
||||||
|
And `getDeliveryServices()` zwraca cache'owana liste przewoznikow z `getAvailableCarriers()` (per-request)
|
||||||
|
```
|
||||||
|
|
||||||
|
## AC-3: PolkurierTrackingService cron tracking
|
||||||
|
```gherkin
|
||||||
|
Given paczka z `provider='polkurier'` i `tracking_number` istnieje w `shipment_packages`
|
||||||
|
When `ShipmentTrackingHandler` (cron) wywoluje `PolkurierTrackingService::getDeliveryStatus($package)`
|
||||||
|
Then service woła `PolkurierApiClient::getStatus()`, parsuje surowy status i mapuje przez `delivery_status_mappings(provider='polkurier')` na znormalizowany status z `delivery_statuses`
|
||||||
|
And zwraca `{status: <normalized>, status_raw: <polkurier_code>, description: <polkurier_label>}`
|
||||||
|
And `supports('polkurier')` zwraca true
|
||||||
|
```
|
||||||
|
|
||||||
|
## AC-4: UI prepare.php panel polkurier
|
||||||
|
```gherkin
|
||||||
|
Given operator wszedl na `/orders/115/shipment/prepare`
|
||||||
|
When wybiera "polkurier" z dropdowna przewoznika
|
||||||
|
Then JS pokazuje panel `#shipment-polkurier-panel` z dynamicznym selectem uslug (zaladowanym z `getAvailableCarriers`)
|
||||||
|
And dla uslug `supports_pickup_point=true` pojawia sie selektor punktu odbioru z listą punktów odpowiedniego typu (InPost/Pocztex/Kurier48/ORLEN)
|
||||||
|
And ukryty input `provider_code` ustawia sie na `polkurier`
|
||||||
|
And submit formularza tworzy paczke przez `ShipmentController::store()` -> `ShipmentProviderRegistry::get('polkurier')->createShipment()`
|
||||||
|
```
|
||||||
|
|
||||||
|
## AC-5: delivery_status_mappings + /settings/delivery-statuses
|
||||||
|
```gherkin
|
||||||
|
Given migracja `20260514_000115_seed_polkurier_delivery_status_mappings.sql` wykonana
|
||||||
|
When operator otwiera `/settings/delivery-statuses` (tab 'mapping')
|
||||||
|
Then widoczne sa wpisy `provider='polkurier'` z surowymi statusami polkuriera mapowanymi na znormalizowane statusy z `delivery_statuses` (np. `nowa_paczka` -> `registered`, `w_doreczeniu` -> `out_for_delivery`, `doreczone` -> `delivered`, `anulowane` -> `cancelled`)
|
||||||
|
And `DeliveryStatus::trackingUrl('polkurier', $tracking, $carrierId)` zwraca poprawny link sledzenia (URL polkuriera lub bezposrednio przewoznika)
|
||||||
|
```
|
||||||
|
|
||||||
|
## AC-6: Live test na zamowieniach #114 i #115
|
||||||
|
```gherkin
|
||||||
|
Given operator ma aktywne konto polkurier (Phase 127 test "Autoryzacja: 1")
|
||||||
|
When operator nadaje paczki na #114 i #115 przez nowy panel UI
|
||||||
|
Then dla obu zamowien `shipment_packages` zawiera wiersz `provider='polkurier'`, `status='created'`, niepusty `tracking_number`, sciezke `label_path` do pobranej etykiety
|
||||||
|
And operator widzi etykiety jako PDF w `/orders/{id}` zakladka Przesylki
|
||||||
|
And operator recznie anuluje obie paczki w panelu polkurier.pl po weryfikacji (poza zakresem kodu — manualna akcja w UI polkuriera)
|
||||||
|
```
|
||||||
|
|
||||||
|
</acceptance_criteria>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 1: PolkurierApiClient — implementacja pelnego kontraktu API</name>
|
||||||
|
<files>src/Modules/Settings/PolkurierApiClient.php</files>
|
||||||
|
<action>
|
||||||
|
Zastapic stuby `createShipment/getLabel/getStatus/cancelOrder` realnymi implementacjami i dodac `getAvailableCarriers`, `getParcelMachines`, `getPostOffices`.
|
||||||
|
|
||||||
|
Wspolny helper `private function call(string $apimetod, array $data, string $login, string $apiToken): array`:
|
||||||
|
- Buduje payload `{authorization: {login, token}, apimetod, data}`.
|
||||||
|
- Wykorzystuje istniejacy `postJson()` (Phase 127).
|
||||||
|
- Parsuje envelope: jezeli `status === 'success'` zwraca `$decoded['response']` (array). W przeciwnym razie rzuca `RuntimeException` z trescia `response` (string albo zserializowany JSON).
|
||||||
|
- PHP 8.5: NIE wywolywac `curl_close()`.
|
||||||
|
|
||||||
|
Metody publiczne:
|
||||||
|
- `createShipment(string $login, string $apiToken, array $payload): array` — apimetod `new_order` (sprawdzic dokladna nazwe w SDK polkurier-sdk/src/Api). Zwraca `{order_id, tracking_number, raw}`.
|
||||||
|
- `getLabel(string $login, string $apiToken, string $orderId, string $format): string` — apimetod `get_label`, format=PDF/ZPL/EPL. Zwraca surowa zawartosc base64-decoded jezeli polkurier zwraca base64; w przeciwnym razie binarny stream. Sprawdzic odpowiedz API.
|
||||||
|
- `getStatus(string $login, string $apiToken, string $orderId): array` — apimetod `get_status`. Zwraca `{status_code, status_name, status_date, raw}`.
|
||||||
|
- `cancelOrder(string $login, string $apiToken, string $orderId): array` — apimetod `cancel_order`. (NIE bedzie wywolywana w Phase 128 — operator anuluje w UI polkuriera, ale metoda dostepna dla przyszlych planow.)
|
||||||
|
- `getAvailableCarriers(string $login, string $apiToken): array` — apimetod `get_available_carriers` (potwierdzic w SDK; mozliwe alternatywy: `get_carriers`, `get_services`). Zwraca liste przewoznikow.
|
||||||
|
- `getParcelMachines(string $login, string $apiToken, string $type, ?string $postalCode = null): array` — apimetod `get_parcel_machines`. type=InPost/Pocztex/Kurier48/ORLEN.
|
||||||
|
- `getPostOffices(string $login, string $apiToken, string $type): array` — apimetod `get_post_offices` (jezeli polkurier rozdziela).
|
||||||
|
|
||||||
|
UWAGA: dokladne nazwy `apimetod` zweryfikowac wzgledem `polkurier-sdk` (https://github.com/Polkurier/polkurier-sdk) — pliki `src/Api/*Api.php`. Jezeli nazwa rozna od zakladanej, dostosowac stale prywatne (`private const APIMETOD_NEW_ORDER = '...'` itp.).
|
||||||
|
|
||||||
|
Avoid: hardcodowane mapowanie statusow (zostawiamy `DeliveryStatusMappingRepository`), zmiany w `testConnection()` (dziala od Phase 127), wprowadzania `Content-Type: application/json; charset=UTF-8` (polkurier odrzuca — zachowac dokladnie `application/json`).
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
1. `php -l src/Modules/Settings/PolkurierApiClient.php` (no syntax errors).
|
||||||
|
2. Operator wywoluje przyszly smoke test: `php bin/smoke-polkurier.php` (skrypt z Task 4) — pierwszy crash wskaze brakujace pole.
|
||||||
|
3. Rzut `RuntimeException` zawiera tresc z pola `response` (nie `status: error`).
|
||||||
|
</verify>
|
||||||
|
<done>AC-1 satisfied: wszystkie 7 metod publicznych zaimplementowane wzorem `testConnection()`; envelope `{status, response}` parsowany jednolicie; bledy rzucane z trescia z `response`.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 2: PolkurierShipmentService + PolkurierTrackingService</name>
|
||||||
|
<files>src/Modules/Shipments/PolkurierShipmentService.php, src/Modules/Shipments/PolkurierTrackingService.php, src/Modules/Shipments/DeliveryStatus.php</files>
|
||||||
|
<action>
|
||||||
|
**PolkurierShipmentService** — `final class implements ShipmentProviderInterface`, wzorzec `ApaczkaShipmentService` (1044 LOC, ale polkurier prostszy — ~500-700 LOC):
|
||||||
|
|
||||||
|
Konstruktor (manualny DI, mirror Apaczki):
|
||||||
|
- `PolkurierIntegrationRepository $integrationRepository`
|
||||||
|
- `PolkurierApiClient $apiClient`
|
||||||
|
- `ShipmentPackageRepository $packages`
|
||||||
|
- `CompanySettingsRepository $companySettings`
|
||||||
|
- `OrdersRepository $ordersRepository`
|
||||||
|
|
||||||
|
Metody:
|
||||||
|
- `code(): string` -> `'polkurier'`.
|
||||||
|
- `getDeliveryServices(): array` — `getCredentials()` -> `$apiClient->getAvailableCarriers()`. Cache per-request (`private ?array $servicesCache = null`).
|
||||||
|
- `createShipment(int $orderId, array $formData): array`:
|
||||||
|
1. `findDetails($orderId)` z OrdersRepository; throw `ShipmentException` gdy null.
|
||||||
|
2. `requireCredentials()` -> `[$login, $token]` z `PolkurierIntegrationRepository::getCredentials()`; throw `IntegrationConfigException` gdy null.
|
||||||
|
3. Sender z `CompanySettingsRepository::getSenderAddress()` + walidacja (`validateSenderAddress`).
|
||||||
|
4. Receiver z `order_addresses` (delivery type), normalizacja telefonu/postal_code.
|
||||||
|
5. Wybor uslugi z `$formData['service_code']` lub `$formData['carrier_id']`.
|
||||||
|
6. Wymiary/waga z formData (z domyslnymi z `company_settings.default_package_*`).
|
||||||
|
7. Punkt odbioru: jezeli `$formData['receiver_point_id']` niepuste -> wstawiamy w payload polkuriera (klucz zaleznie od SDK).
|
||||||
|
8. COD/insurance z formData.
|
||||||
|
9. `$apiClient->createShipment($login, $token, $payload)` -> `{order_id, tracking_number, ...}`.
|
||||||
|
10. `$packages->insert([...])` z `provider='polkurier'`, `command_id=order_id`, `tracking_number`, `status='created'`, `payload_json=zserializowany_payload`.
|
||||||
|
11. Synchroniczne pobranie etykiety przez `downloadLabel($packageId, $storagePath)` — analog Apaczki, ktora pobiera label do `storage/labels/`.
|
||||||
|
12. Zwroc `['package_id' => ..., 'tracking_number' => ..., 'label_path' => ...]`.
|
||||||
|
- `checkCreationStatus(int $packageId): array` — fetch z `shipment_packages`; jezeli `status='draft'` -> pingnij `getStatus()` API i zaktualizuj.
|
||||||
|
- `downloadLabel(int $packageId, string $storagePath): array` — `$apiClient->getLabel($login, $token, $orderId, $package['label_format'] ?? 'PDF')`; zapisz do `$storagePath/polkurier_{packageId}.pdf` (lub `.zpl`); update `shipment_packages.label_path`.
|
||||||
|
|
||||||
|
**PolkurierTrackingService** — `final class implements ShipmentTrackingInterface`:
|
||||||
|
- `supports(string $provider): bool` -> `strtolower($provider) === 'polkurier'`.
|
||||||
|
- `getDeliveryStatus(array $package): ?array`:
|
||||||
|
1. `requireCredentials()`; zwroc null gdy konfiguracja nieaktywna (cron nie powinien rzucac, tylko skipowac).
|
||||||
|
2. `$apiClient->getStatus($login, $token, $package['command_id'])`.
|
||||||
|
3. Mapuj `status_code` przez `DeliveryStatusMappingRepository::findNormalized('polkurier', $rawStatus)`.
|
||||||
|
4. Fallback `unknown` gdy brak mapowania (analog Apaczki).
|
||||||
|
5. Zwroc `['status' => $normalized, 'status_raw' => $rawStatus, 'description' => $statusName]`.
|
||||||
|
|
||||||
|
**DeliveryStatus.php** — dolozyc obsluge providera `polkurier` w `trackingUrl(string $provider, string $tracking, string $carrierId): string`:
|
||||||
|
- Polkurier deleguje do przewoznika docelowego — `carrierId` mowi nam ktory. Fallback URL `https://polkurier.pl/sledzenie/<tracking>` (jezeli polkurier ma taki) lub URL przewoznika docelowego z istniejacych branch-ow (`inpost`, `dpd`, etc.). MVP: zwroc URL bazowy polkurier + tracking_number.
|
||||||
|
|
||||||
|
Avoid:
|
||||||
|
- Hardcodowania URL tracking polkuriera bez weryfikacji (sprawdzic w panelu polkurier lub SDK).
|
||||||
|
- Pobierania etykiety w osobnym requeście jezeli polkurier zwraca `label_base64` w odpowiedzi createShipment (parsowac i zapisywac od razu).
|
||||||
|
- Throwowania w `PolkurierTrackingService::getDeliveryStatus` przy braku credentials — cron musi byc odporny.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
1. `php -l src/Modules/Shipments/PolkurierShipmentService.php` i `PolkurierTrackingService.php`.
|
||||||
|
2. Manualnie: po Task 3 wejdz na `/orders/114/shipment/prepare`, wybierz polkurier — dropdown zwraca przewoznikow z API.
|
||||||
|
</verify>
|
||||||
|
<done>AC-2 i AC-3 satisfied: oba serwisy implementuja interfejsy, integruja z `PolkurierApiClient`, `ShipmentPackageRepository` i `DeliveryStatusMappingRepository`. Kontrakt Phase 127 (single global config, `is_active=1` guard) zachowany.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 3: Wiring + UI prepare.php panel polkurier</name>
|
||||||
|
<files>routes/web.php, src/Modules/Shipments/ShipmentController.php, src/Modules/Cron/CronHandlerFactory.php, resources/views/shipments/prepare.php</files>
|
||||||
|
<action>
|
||||||
|
**routes/web.php**:
|
||||||
|
- W `$shipmentProviderRegistry = new ShipmentProviderRegistry([...])` (linia ~474) dolozyc `new PolkurierShipmentService($polkurierIntegrationRepository, new PolkurierApiClient(), $shipmentPackageRepository, $companySettingsRepository, new OrdersRepository(...))`.
|
||||||
|
- Dolozyc `use App\Modules\Shipments\PolkurierShipmentService;` w nagłowku.
|
||||||
|
|
||||||
|
**CronHandlerFactory.php** (linia ~166-169):
|
||||||
|
- W tablicy trackerow dolozyc `new PolkurierTrackingService($polkurierIntegrationRepository, $polkurierApiClient, $deliveryStatusMappingRepository)`.
|
||||||
|
- Dolozyc `use App\Modules\Shipments\PolkurierTrackingService;` i `use App\Modules\Settings\PolkurierIntegrationRepository;`/`PolkurierApiClient;`.
|
||||||
|
|
||||||
|
**ShipmentController.php**:
|
||||||
|
- Wstrzyknac `PolkurierShipmentService $polkurierService` (lub uzywac z registry).
|
||||||
|
- W `prepare()` przekazac do widoku `$polkurierServices = $polkurierService->getDeliveryServices()` (try/catch — empty array on failure).
|
||||||
|
|
||||||
|
**resources/views/shipments/prepare.php** — wzorzec panelu Apaczki (linie 172-216):
|
||||||
|
- Dolozyc `<option value="polkurier">polkurier</option>` do `#carrierSelect` (linia ~103).
|
||||||
|
- Dolozyc panel `<div id="shipment-polkurier-panel">` z:
|
||||||
|
- `<select id="shipment-polkurier-service-select">` wypelniony `$polkurierServices` (carrier_id, name).
|
||||||
|
- Conditional pickup point picker (`<select id="shipment-polkurier-point-select">`) widoczny gdy wybrana usluga ma `supports_pickup_point=true`. Lista punktow ladowana AJAX-em z nowego endpointu `/shipments/polkurier/points?type=InPost&postal=XX-XXX` (dodac route + metoda w `ShipmentController`).
|
||||||
|
- Hidden input `name="service_code"` aktualizowany przez JS na podstawie selecta.
|
||||||
|
- JS (linie ~580-650): dodac `polkurierSelect`/`polkurierPanel`/`polkurierPointSelect`; toggle widocznosci paneli; ustaw `providerInput.value = 'polkurier'` gdy carrier='polkurier'.
|
||||||
|
|
||||||
|
Avoid:
|
||||||
|
- Powielania logiki Apaczki — uzyc tych samych helperow JS gdzie sie da.
|
||||||
|
- Hardcodowania listy punktow w widoku — wszystko z API polkuriera przez AJAX.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
1. `/orders/114/shipment/prepare` — dropdown przewoznika ma "polkurier"; po wyborze pokazuje panel.
|
||||||
|
2. Select uslug ma realne wartosci z polkuriera.
|
||||||
|
3. `php -l routes/web.php` i `CronHandlerFactory.php`.
|
||||||
|
</verify>
|
||||||
|
<done>AC-4 satisfied: panel polkurier widoczny w prepare.php, integracja z registry, cron tracker zarejestrowany.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="checkpoint:human-verify" gate="blocking">
|
||||||
|
<what-built>Live test polkurier ShipmentService na zamowieniach #114 i #115. Operator nadaje paczki przez nowy panel UI, weryfikuje wynik, recznie anuluje w panelu polkurier po weryfikacji.</what-built>
|
||||||
|
<how-to-verify>
|
||||||
|
1. XAMPP MySQL online + cron disabled (zeby tracking nie zaczal pingowac przed weryfikacja).
|
||||||
|
2. Otworz `/orders/114/shipment/prepare`:
|
||||||
|
- Wybierz "polkurier" w dropdownie.
|
||||||
|
- Wybierz usluge kuriera (np. DPD Standard albo InPost Kurier).
|
||||||
|
- Uzupelnij wymiary/wage; potwierdz adres odbiorcy.
|
||||||
|
- Submit.
|
||||||
|
3. Sprawdz redirect na `/orders/114` -> zakladka Przesylki:
|
||||||
|
- Wiersz `provider=polkurier`, niepusty `tracking_number`, link "Pobierz etykiete" otwiera PDF.
|
||||||
|
4. Powtorz dla zamowienia #115 — ten raz z usluga paczkomatowa (InPost Paczkomat); selektor punktu pokazuje liste paczkomatow z API.
|
||||||
|
5. Otworz panel polkurier.pl manualnie -> zobacz utworzone paczki.
|
||||||
|
6. Anuluj obie paczki w panelu polkurier (manualna akcja).
|
||||||
|
7. Zglos wynik: "OK" jezeli paczki utworzone i etykiety pobrane, "issues" z opisem co nie dziala.
|
||||||
|
</how-to-verify>
|
||||||
|
<resume-signal>Type "approved" to continue with delivery_status_mappings seed, or describe issues to fix</resume-signal>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 4: Migracja seed delivery_status_mappings + weryfikacja /settings/delivery-statuses</name>
|
||||||
|
<files>database/migrations/20260514_000115_seed_polkurier_delivery_status_mappings.sql</files>
|
||||||
|
<action>
|
||||||
|
Stworzyc migracje seedujaca `delivery_status_mappings` (provider='polkurier') na podstawie REALNYCH statusow zwracanych przez API polkurier w Task 1 (operator po live tescie zna konkretne `status_code` z `getStatus`).
|
||||||
|
|
||||||
|
Baseline mapowan (do dostrojenia po live tescie):
|
||||||
|
- `nowa` / `przyjete` / `oczekuje` -> `registered`
|
||||||
|
- `wydrukowane` / `przygotowanie` -> `label_printed` (jezeli istnieje w `delivery_statuses` — sprawdzic w Phase 108)
|
||||||
|
- `nadane` / `w_dostawie` / `w_doreczeniu` -> `in_transit` lub `out_for_delivery`
|
||||||
|
- `doreczone` / `odebrane` -> `delivered`
|
||||||
|
- `zwrocone` -> `returned`
|
||||||
|
- `anulowane` -> `cancelled`
|
||||||
|
- `niedoreczone` / `blad_doreczenia` -> `delivery_failed` lub `unknown` (zaleznie od `delivery_statuses`)
|
||||||
|
|
||||||
|
Migracja musi byc idempotentna: `INSERT INTO delivery_status_mappings (provider, raw_status, normalized_status, description) VALUES (...) ON DUPLICATE KEY UPDATE normalized_status = VALUES(normalized_status), description = VALUES(description);`.
|
||||||
|
|
||||||
|
Uruchom `php bin/migrate.php` po zatwierdzeniu mapowan.
|
||||||
|
|
||||||
|
Otworz `/settings/delivery-statuses?tab=mapping` -> potwierdz widocznosc wpisow provider='polkurier'.
|
||||||
|
|
||||||
|
Avoid:
|
||||||
|
- Seedu na bazie przypuszczen — uzyj statusow ZAOBSERWOWANYCH w live tescie z Task checkpoint.
|
||||||
|
- Tworzenia nowych wpisow w `delivery_statuses` (jezeli polkurier zwraca status ktorego nie ma — dodaj rownolegly INSERT do tej migracji albo osobna migracje).
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
1. `php bin/migrate.php` zwraca success.
|
||||||
|
2. `SELECT COUNT(*) FROM delivery_status_mappings WHERE provider='polkurier'` >= 6.
|
||||||
|
3. `/settings/delivery-statuses?tab=mapping` pokazuje wiersze polkurier.
|
||||||
|
4. Ponowne uruchomienie migracji = no-op (idempotencja).
|
||||||
|
</verify>
|
||||||
|
<done>AC-5 satisfied: mapowania wpisane do DB, widoczne w UI, idempotentne.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 5: Aktualizacja dokumentacji codebase</name>
|
||||||
|
<files>.paul/codebase/architecture.md, .paul/codebase/db_schema.md, .paul/codebase/tech_changelog.md</files>
|
||||||
|
<action>
|
||||||
|
**architecture.md** — dolozyc sekcje "Phase 128 — polkurier ShipmentService + Tracking" pod sekcja Phase 127:
|
||||||
|
- PolkurierApiClient — pelna lista 7 publicznych metod z apimetod-ami.
|
||||||
|
- PolkurierShipmentService — kontrakt ShipmentProviderInterface, lista pol payloadu, integracja z `getAvailableCarriers`.
|
||||||
|
- PolkurierTrackingService — kontrakt ShipmentTrackingInterface, mapowanie statusow.
|
||||||
|
- Wiring w `ShipmentProviderRegistry` i `CronHandlerFactory`.
|
||||||
|
- UI prepare.php panel polkurier — selector uslug + pickup point ajax.
|
||||||
|
|
||||||
|
**db_schema.md** — dolozyc seed mapping rows w sekcji "delivery_status_mappings":
|
||||||
|
- Wymienic mapowania `provider='polkurier'` z migracji.
|
||||||
|
|
||||||
|
**tech_changelog.md** — wpis z data 2026-05-14 (lub data wdrozenia):
|
||||||
|
- "Phase 128 (polkurier ShipmentService): pelna implementacja API client (createShipment/getLabel/getStatus/cancelOrder/getAvailableCarriers/getParcelMachines/getPostOffices), PolkurierShipmentService implementing ShipmentProviderInterface, PolkurierTrackingService, UI panel w prepare.php, seed delivery_status_mappings."
|
||||||
|
|
||||||
|
Avoid: kopiowania kodu do dokumentacji (zostawic referencje sciezek + 1-2 zdania kontraktu).
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
Manualnie przegladnac diff `.paul/codebase/*.md` — wpisy obecne, formatowanie spojne z Phase 127.
|
||||||
|
</verify>
|
||||||
|
<done>Dokumentacja zaktualizowana zgodnie z CLAUDE.md (sekcja "Utrwalanie stalych wymagan").</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<boundaries>
|
||||||
|
|
||||||
|
## DO NOT CHANGE
|
||||||
|
- `src/Modules/Settings/PolkurierIntegrationRepository.php` — Phase 127, kontrakt stabilny.
|
||||||
|
- `src/Modules/Settings/PolkurierApiClient::testConnection()` — Phase 127, zweryfikowany na zywym koncie ("Autoryzacja: 1").
|
||||||
|
- `apaczka_integration_settings`, `ApaczkaShipmentService`, `ApaczkaTrackingService` — Apaczka dziala niezaleznie obok polkuriera.
|
||||||
|
- `database/migrations/20260514_000114_create_polkurier_integration_settings.sql` (Phase 127).
|
||||||
|
- `ShipmentProviderInterface` i `ShipmentTrackingInterface` — kontrakty stabilne (nie dolozyc/zmienic metod).
|
||||||
|
- Reszta paneli w `prepare.php` (allegro/inpost/apaczka) — tylko dolozenie nowego panelu polkurier obok.
|
||||||
|
|
||||||
|
## SCOPE LIMITS
|
||||||
|
- BEZ implementacji `OrderValuationV2` (wycena przed nadaniem) — odlozone na osobna faze.
|
||||||
|
- BEZ presetow przesylek dla polkuriera (`shipment_presets.provider_code='polkurier'`) — operator moze ich uzywac dopiero jak panel polkurier dziala; presety w osobnej fazie.
|
||||||
|
- BEZ widoku CLI smoke test scriptu (`bin/smoke-polkurier.php`) — testujemy w realnym UI na #114/#115.
|
||||||
|
- BEZ event automatyzacji `shipment.created` zmian — to zdarzenie juz emitowane jednolicie z `ShipmentController::store()` dla wszystkich providerow.
|
||||||
|
- BEZ idempotencji createShipment (double-POST guard) — jak w Apaczce, brak retry guard w MVP.
|
||||||
|
- BEZ refaktoringu wspolnego kodu Apaczka/polkurier (`buildReceiverAddress`, `validateSenderAddress` itp.) — kopiujemy wzorzec, deduplikacja w osobnym planie.
|
||||||
|
|
||||||
|
</boundaries>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
Before declaring plan complete:
|
||||||
|
- [ ] `php -l` przechodzi dla wszystkich zmienionych plikow PHP
|
||||||
|
- [ ] Migracja `20260514_000115_*.sql` wykonana, ponowne uruchomienie = no-op
|
||||||
|
- [ ] Operator potwierdzil checkpoint: 2 paczki utworzone na #114 i #115, etykiety pobrane, manualnie anulowane w panelu polkurier
|
||||||
|
- [ ] `/settings/delivery-statuses?tab=mapping` pokazuje wpisy provider='polkurier'
|
||||||
|
- [ ] `/orders/{id}/shipment/prepare` panel polkurier widoczny i funkcjonalny
|
||||||
|
- [ ] Cron tracking nie crashuje (sprawdzic `storage/logs/app.log` po jednym przebiegu)
|
||||||
|
- [ ] Dokumentacja `.paul/codebase/*.md` zaktualizowana
|
||||||
|
- [ ] Wszystkie AC-1..AC-6 spelnione
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- Operator moze nadawac paczki przez polkurier z poziomu `/orders/{id}/shipment/prepare` w 4 trybach: kurier door-to-door, paczkomat InPost, punkt Pocztex/Kurier48, ORLEN.
|
||||||
|
- Tracking polkurier dziala w cronie i aktualizuje `shipment_packages.delivery_status` przez `delivery_status_mappings`.
|
||||||
|
- Live test na #114 i #115 zakonczony sukcesem (paczki utworzone, etykiety pobrane, recznie anulowane).
|
||||||
|
- Zaden istniejacy provider (Apaczka/InPost/Allegro WZA) nie regresuje.
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create `.paul/phases/128-polkurier-shipment-service/128-01-SUMMARY.md`
|
||||||
|
</output>
|
||||||
221
.paul/phases/128-polkurier-shipment-service/128-01-SUMMARY.md
Normal file
221
.paul/phases/128-polkurier-shipment-service/128-01-SUMMARY.md
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
---
|
||||||
|
phase: 128-polkurier-shipment-service
|
||||||
|
plan: 01
|
||||||
|
subsystem: shipments
|
||||||
|
tags: [polkurier, courier, broker, shipment, tracking, ui-prepare, delivery-status-mappings]
|
||||||
|
|
||||||
|
requires:
|
||||||
|
- phase: 127-polkurier-integration-foundation
|
||||||
|
provides: PolkurierIntegrationRepository (login + Token API + getCredentials), PolkurierApiClient.testConnection, integration row in `integrations` + `polkurier_integration_settings`.
|
||||||
|
|
||||||
|
provides:
|
||||||
|
- PolkurierApiClient z pelnym kontraktem (createShipment/getLabel/getStatus/cancelOrder/getAvailableCarriers/getInpostParcelMachines/getCourierPoints).
|
||||||
|
- PolkurierShipmentService implementujacy ShipmentProviderInterface — operator tworzy paczki polkurier z `/orders/{id}/shipment/prepare`.
|
||||||
|
- PolkurierTrackingService implementujacy ShipmentTrackingInterface — cron `shipment_tracking_sync` pinguje get_status.
|
||||||
|
- DeliveryStatus::trackingUrl fallback `https://polkurier.pl/sledz-paczke/<tracking>` + carrier_id routing.
|
||||||
|
- UI panel "polkurier" w `prepare.php` z dynamiczna lista uslug z available_carriers.
|
||||||
|
- Seed migracja `delivery_status_mappings(provider='polkurier')` z 7 wpisami O/P/A/WP/D/Z/W → znormalizowane statusy.
|
||||||
|
|
||||||
|
affects: [paczkomaty UI (InpostParcelMachines/PocztexPostOffices/Kurier48PostOffices), shipment_presets (provider_code='polkurier'), OrderValuationV2 (wycena przed nadaniem)]
|
||||||
|
|
||||||
|
tech-stack:
|
||||||
|
added: []
|
||||||
|
patterns:
|
||||||
|
- "Wspolny prywatny `call($apimetod, $data, $login, $token): mixed` w API client parsuje envelope `{status, response}`; sukces -> tresc `response`, blad -> RuntimeException z trescia z `response`. Reuse dla wszystkich apimetod."
|
||||||
|
- "polkurier SDK Order entity zwraca `number` (nie `orderno`) i `waybills[0].number` — `extractOrderNumber`/`extractTrackingNumber` priorytetuja SDK shape, fallback na top-level klucze."
|
||||||
|
- "polkurier API nie udostepnia parametru rozmiaru etykiety (A4/A6) — sterowane wylacznie w panelu klienta polkurier.pl. `polkurier_integration_settings.default_label_format` (PDF/ZPL/EPL) odnosi sie do typu pliku, NIE rozmiaru."
|
||||||
|
|
||||||
|
key-files:
|
||||||
|
created:
|
||||||
|
- src/Modules/Shipments/PolkurierShipmentService.php
|
||||||
|
- src/Modules/Shipments/PolkurierTrackingService.php
|
||||||
|
- database/migrations/20260514_000115_seed_polkurier_delivery_status_mappings.sql
|
||||||
|
modified:
|
||||||
|
- src/Modules/Settings/PolkurierApiClient.php
|
||||||
|
- src/Modules/Shipments/DeliveryStatus.php
|
||||||
|
- src/Modules/Shipments/ShipmentController.php
|
||||||
|
- src/Modules/Cron/CronHandlerFactory.php
|
||||||
|
- routes/web.php
|
||||||
|
- resources/views/shipments/prepare.php
|
||||||
|
|
||||||
|
key-decisions:
|
||||||
|
- "polkurier `shipmenttype` wymaga lowercase z zbioru [box, envelope, palette, small_parcel, parcel_size_20] — `normalizeShipmentType()` mapuje legacy PACKAGE/BOX/PARCEL/PACZKA/KOPERTA/PALETA na format polkuriera."
|
||||||
|
- "Rozmiar etykiety A4/A6 sterowany w panelu klienta polkurier.pl, NIE przez API (zweryfikowane na PDF v1.11) — kod nie wysyla zadnego parametru rozmiaru."
|
||||||
|
- "Brak dedykowanego selektora punktu odbioru w UI — operator wpisuje `receiver_point_id` w istniejacy text input w sekcji Adres odbiorcy (np. `POP-RZE54`); usuniety AJAX endpoint i lookupPickupPoints."
|
||||||
|
- "Seed `delivery_status_mappings` bazuje na oficjalnej tabeli ORDER_STATUS z PDF v1.11 (kody O/P/A/WP/D/Z/W), nie na obserwacji w live tescie — bezpieczniejsze i wyczerpujace."
|
||||||
|
- "polkurier dziala obok Apaczki (decyzja z Phase 127 zachowana); `ShipmentProviderRegistry` rejestruje oba; brak migracji shipment_presets."
|
||||||
|
|
||||||
|
patterns-established:
|
||||||
|
- "Pattern: dla nowych metod polkurier API uzywaj wspolnego `call($apimetod, $data, $login, $token)`. Status `success` zwraca tresc `response`. Status inny rzuca `RuntimeException` z trescia `response` (string albo zserializowany JSON dla tablic)."
|
||||||
|
- "Pattern: dla parsowania odpowiedzi polkurier SDK entity, najpierw priorytetuj klucze entity (`number`, `waybills[].number`, `file`), potem fallback na top-level/snake_case klucze, potem obsluga wrapperow `{order:{...}}` i list."
|
||||||
|
- "Pattern: diagnostyka silent-fail w ShipmentService — gdy parsing API odpowiedzi zwraca pusty wynik mimo `status=success`, zapisuj fragment surowej odpowiedzi do `shipment_packages.error_message` zeby operator/dev zobaczyl shape."
|
||||||
|
|
||||||
|
duration: ~120min (incl. 4 live test iteracje)
|
||||||
|
started: 2026-05-14T20:00:00Z
|
||||||
|
completed: 2026-05-14T22:00:00Z
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 128 Plan 01: polkurier ShipmentService + Tracking + UI prepare
|
||||||
|
|
||||||
|
**polkurier zarejestrowany jako pelnoprawny przewoznik obok Apaczki — operator tworzy paczki przez UI `/orders/{id}/shipment/prepare`, etykieta A6 generowana, cron tracking gotowy do mapowania statusow O/P/A/WP/D/Z/W na znormalizowane created/confirmed/cancelled/in_transit/delivered/returned/problem.**
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
| Metric | Value |
|
||||||
|
|--------|-------|
|
||||||
|
| Duration | ~120 min |
|
||||||
|
| Started | 2026-05-14T20:00:00Z |
|
||||||
|
| Completed | 2026-05-14T22:00:00Z |
|
||||||
|
| Tasks | 6/6 completed (5 auto + 1 checkpoint) |
|
||||||
|
| Files modified | 10 |
|
||||||
|
| Live test iteracje | 4 (ReferenceError → uppercase shipmenttype → orderno parsing → A6 panel) |
|
||||||
|
|
||||||
|
## Acceptance Criteria Results
|
||||||
|
|
||||||
|
| Criterion | Status | Notes |
|
||||||
|
|-----------|--------|-------|
|
||||||
|
| AC-1: PolkurierApiClient pelny kontrakt API | Pass | 7 metod (createShipment/getLabel/getStatus/cancelOrder/getAvailableCarriers/getInpostParcelMachines/getCourierPoints) zweryfikowane na PDF v1.11. `call()` wspolny wrapper envelope `{status, response}`. |
|
||||||
|
| AC-2: PolkurierShipmentService implementuje ShipmentProviderInterface | Pass | `code()='polkurier'`, `getDeliveryServices` cache per-request, `createShipment` orchestruje pelny flow z normalizacja shipmenttype i splitem ulicy, `downloadLabel` z base64 decode na klucz `file`. Verified on #114/#115. |
|
||||||
|
| AC-3: PolkurierTrackingService cron tracking | Pass (kod) | Implementacja kompletna, ale niezweryfikowane na zywej bazie podczas APPLY (operator anulowal paczki w panelu polkurier po teście — cron nie mial co pingowac). Graceful null przy bledach. Pierwszy passthrough nastapi przy nastepnej zywej paczce. |
|
||||||
|
| AC-4: UI prepare.php panel polkurier | Pass | Opcja "polkurier" w dropdownie, panel z dynamiczna lista uslug, hidden `service_code`. Bez dedykowanego selektora punktu — operator wpisuje w istniejacy input w sekcji Adres odbiorcy. |
|
||||||
|
| AC-5: delivery_status_mappings + /settings/delivery-statuses | Pass (kod) | Migracja idempotentna z 7 wpisami O/P/A/WP/D/Z/W. Operator uruchomi `php bin/migrate.php` gdy MySQL online. Widocznosc w `/settings/delivery-statuses` po migracji. |
|
||||||
|
| AC-6: Live test na #114 i #115 | Pass | 4 iteracje, ostatecznie obie paczki utworzone w polkurier, etykiety pobrane (A6 po zmianie w panelu klienta), operator anulowal w panelu polkuriera po weryfikacji. |
|
||||||
|
|
||||||
|
## Accomplishments
|
||||||
|
|
||||||
|
- polkurier zarejestrowany jako 4. provider w `ShipmentProviderRegistry` (obok allegro_wza, apaczka, inpost) — operator nadaje paczki z UI bez przelaczania platform.
|
||||||
|
- Kontrakt API zweryfikowany na oficjalnej dokumentacji PDF v1.11 (pobranej i zachowanej w `.paul/phases/128-polkurier-shipment-service/polkurier-api-docs.txt` — referencyjne zrodlo dla przyszlych faz).
|
||||||
|
- Mapowanie statusow `O/P/A/WP/D/Z/W` na znormalizowane statusy `created/confirmed/cancelled/in_transit/delivered/returned/problem` z idempotentna migracja — cron tracking gotowy do dzialania.
|
||||||
|
- Diagnostyka silent-fail patternem (zapis fragmentu surowej odpowiedzi do `error_message` przy nieudanym parsingu) — uratowala 3. iteracje live testu (parsing `number` vs `orderno`).
|
||||||
|
|
||||||
|
## Task Commits
|
||||||
|
|
||||||
|
Wszystkie zmiany w jednym stanie WIP — commit zostanie wykonany w transition (`feat(128): polkurier shipment service + tracking + UI prepare`).
|
||||||
|
|
||||||
|
| Task | Status | Description |
|
||||||
|
|------|--------|-------------|
|
||||||
|
| Task 1: PolkurierApiClient pelen kontrakt API | done | 7 metod, wspolny `call()` wrapper, parsowanie envelope |
|
||||||
|
| Task 2: PolkurierShipmentService + PolkurierTrackingService | done | ~520 + ~110 LOC, oba implementuja swoje interfejsy |
|
||||||
|
| Task 3: Wiring + UI prepare.php panel | done | Registry, CronHandlerFactory, ShipmentController.prepare/create, panel + JS |
|
||||||
|
| Task 4: Live test checkpoint na #114/#115 | done | Operator approved po 4 iteracjach, etykieta A6 po zmianie w panelu klienta polkurier |
|
||||||
|
| Task 5: Migracja seed delivery_status_mappings | done (kod) | 7 wpisow z PDF v1.11, idempotentna; operator uruchomi gdy MySQL online |
|
||||||
|
| Task 6: Aktualizacja `.paul/codebase/*.md` | done | architecture.md (Phase 128 sekcja), db_schema.md (seed mappings), tech_changelog.md (Phase 128 entry z 4 deviationami i iteracjami live testu) |
|
||||||
|
|
||||||
|
## Files Created/Modified
|
||||||
|
|
||||||
|
| File | Change | Purpose |
|
||||||
|
|------|--------|---------|
|
||||||
|
| `src/Modules/Settings/PolkurierApiClient.php` | Modified | Stuby z Phase 127 zastapione 7 metodami: createShipment/getLabel/getStatus/cancelOrder/getAvailableCarriers/getInpostParcelMachines/getCourierPoints. Wspolny `call()` parser envelope. |
|
||||||
|
| `src/Modules/Shipments/PolkurierShipmentService.php` | Created | `implements ShipmentProviderInterface`, ~520 LOC. createShipment orchestracja, normalizeShipmentType, splitStreetAndNumber, buildRecipient/buildSender/buildPickup, downloadLabel z base64 decode, extractOrderNumber/extractTrackingNumber priorytetujace SDK shape. |
|
||||||
|
| `src/Modules/Shipments/PolkurierTrackingService.php` | Created | `implements ShipmentTrackingInterface`, ~110 LOC. getDeliveryStatus z graceful null + normalizacja przez DeliveryStatusMappingRepository. |
|
||||||
|
| `src/Modules/Shipments/DeliveryStatus.php` | Modified | +4 LOC: fallback URL `https://polkurier.pl/sledz-paczke/<tracking>`. Carrier_id routing przez `matchCarrierByName` automatyczny. |
|
||||||
|
| `src/Modules/Shipments/ShipmentController.php` | Modified | prepare() fetchuje polkurierServices, create() rozszerzony o service_code/pickup_date/pickup_time_from/pickup_time_to. |
|
||||||
|
| `src/Modules/Cron/CronHandlerFactory.php` | Modified | PolkurierTrackingService dodany do ShipmentTrackingRegistry. |
|
||||||
|
| `routes/web.php` | Modified | use PolkurierApiClient + PolkurierShipmentService, registry zarejestrowany. |
|
||||||
|
| `resources/views/shipments/prepare.php` | Modified | Opcja "polkurier" w carrier select, panel z select uslug, hidden service_code, JS handler. |
|
||||||
|
| `database/migrations/20260514_000115_seed_polkurier_delivery_status_mappings.sql` | Created | 7 wpisow O/P/A/WP/D/Z/W → normalized. Idempotentne. |
|
||||||
|
| `.paul/codebase/architecture.md` | Modified | Sekcja Phase 128 (PolkurierApiClient/ShipmentService/TrackingService/UI/wiring/seed/boundaries). |
|
||||||
|
| `.paul/codebase/db_schema.md` | Modified | Seedowane mapowania `provider='polkurier'` w sekcji `delivery_status_mappings`. |
|
||||||
|
| `.paul/codebase/tech_changelog.md` | Modified | Entry Phase 128 z opisem zmian + 4 iteracje live testu + deviations. |
|
||||||
|
| `.paul/phases/128-polkurier-shipment-service/polkurier-api-docs.txt` | Created | Tekst PDF v1.11 (pdftotext extract) — referencyjne zrodlo dla przyszlych faz polkuriera. |
|
||||||
|
|
||||||
|
## Decisions Made
|
||||||
|
|
||||||
|
| Decision | Rationale | Impact |
|
||||||
|
|----------|-----------|--------|
|
||||||
|
| `shipmenttype` lowercase + `normalizeShipmentType()` mapping | polkurier API odrzuca uppercase `BOX` — wymaga lowercase z zbioru `[box, envelope, palette, small_parcel, parcel_size_20]` (komunikat bledu w live tescie). Aliasy dla PACKAGE/PARCEL/PACZKA/KOPERTA/PALETA pozwalaja reuse istniejacych wartosci formularza. | Wszystkie kolejne paczki polkurier maja poprawny shipmenttype bez zmian w formularzu/preset. |
|
||||||
|
| `extractOrderNumber` priorytetuje pole `number` (SDK Order entity) nad `orderno` | polkurier `create_order` zwraca Order entity z polem `number` (zweryfikowane w SDK Order.php — setNumber/getNumber). `orderno` to nazwa parametru INPUT w innych metodach (get_label, get_status, cancel_order). | Parsing dziala dla aktualnej wersji SDK + odporne na stary shape (`orderno` fallback). |
|
||||||
|
| Brak dedykowanego selektora punktu odbioru w UI | Operator zglosil ze `Punkt odbioru` jest juz polem w sekcji Adres odbiorcy z auto-fillem `parcel_external_id` z importu zamowienia. Dodatkowy selektor byl duplikatem. | Usuniete: `lookupPickupPoints`, `ShipmentController::polkurierPoints`, route, JS handler. Operator wpisuje czysty ID (np. `POP-RZE54`) w istniejacy input. |
|
||||||
|
| Rozmiar etykiety A4/A6 sterowany w panelu klienta polkurier.pl | API polkurier nie udostepnia parametru rozmiaru w `get_label` ani `create_order` (zweryfikowane na PDF v1.11). Operator zmienia preferencje konta jednorazowo. | Brak dodatkowego pola w `polkurier_integration_settings` ani formularzu; `default_label_format` (PDF/ZPL/EPL) odnosi sie tylko do typu pliku. |
|
||||||
|
| Seed `delivery_status_mappings` z PDF v1.11 (nie z obserwacji live test) | Live test obejmowal tylko status `P` (Potwierdzone) bezposrednio po `create_order`. Seedowanie bazujace na obserwacji wymagaloby kolejnych miesiecy zywych paczek. PDF ma kompletna tabele ORDER_STATUS. | 7 wpisow O/P/A/WP/D/Z/W ready od pierwszego dnia. |
|
||||||
|
| Diagnostyka silent-fail patternem (zapis surowej odpowiedzi do `error_message`) | 3. iteracja live testu (parsing `number` vs `orderno`) byla niemozliwa do debugowania bez podgladu surowej odpowiedzi — `payload_json` w `shipment_packages.update()` jest poza whitelist. Zapis fragmentu (400 znakow) do `error_message` jest tani i widoczny operatorowi w UI. | Pattern do reuse dla nowych integracji API z nieznanym shape odpowiedzi. |
|
||||||
|
|
||||||
|
## Deviations from Plan
|
||||||
|
|
||||||
|
### Summary
|
||||||
|
|
||||||
|
| Type | Count | Impact |
|
||||||
|
|------|-------|--------|
|
||||||
|
| Auto-fixed | 4 | Live test iteracje — wszystkie naprawione w tej samej sesji APPLY |
|
||||||
|
| Scope removals | 1 | UI selektor punktow paczkomatowych usuniety na zyczenie operatora |
|
||||||
|
| Scope additions | 1 | Pole `service_code` i `pickup_*` w `ShipmentController::create()` (potrzebne dla polkurier payload) |
|
||||||
|
| Deferred | 3 | Cron tracking weryfikacja, migracja MySQL, paczkomaty UI (kolejna faza) |
|
||||||
|
|
||||||
|
**Total impact:** Essential fixes (live test feedback), no scope creep — operator manual confirmation poszerzyl o jedno usuniecie (selektor punktu) i jedno dodanie (`service_code` przekazywany do service).
|
||||||
|
|
||||||
|
### Auto-fixed Issues
|
||||||
|
|
||||||
|
**1. [JS ReferenceError] `polkurierPointIdInput is not defined` w `clearHiddenFields()`**
|
||||||
|
- **Found during:** Task 4 (live test, pierwszy submit polkurier)
|
||||||
|
- **Issue:** Po usunieciu duplikatu selektora punktu odbioru (po feedback operatora w Task 3 iteracji) zostala martwa referencja do zmiennej `polkurierPointIdInput` w `clearHiddenFields()`. JS rzucal ReferenceError, handler `carrierSelect.change` przerywal przed wywolaniem `showPanel()`, `provider_code` zostawal na PHP-renderowanej wartosci `apaczka` (gdy `$preselectedCarrier === 'apaczka'`). Submit szedl do ApaczkaShipmentService → blad "Nie podano uslugi Apaczka."
|
||||||
|
- **Fix:** Usuniecie linii `if (polkurierPointIdInput) polkurierPointIdInput.value = '';` z `clearHiddenFields()`.
|
||||||
|
- **Files:** `resources/views/shipments/prepare.php`
|
||||||
|
- **Verification:** Drugi submit polkurier → routing do PolkurierShipmentService.
|
||||||
|
|
||||||
|
**2. [Polkurier API validation] `shipmenttype` musi byc lowercase**
|
||||||
|
- **Found during:** Task 4 (live test, drugi submit po napraweniu #1)
|
||||||
|
- **Issue:** Wysylanie `BOX` uppercase → API odrzucalo: "Typ paczki musi przyjmowac jeden z parametrow ze zbioru [box, envelope, palette, small_parcel, parcel_size_20]".
|
||||||
|
- **Fix:** Nowa metoda `normalizeShipmentType()` z lowercase + aliasami (PACKAGE→box, PARCEL→box, PACZKA→box, KOPERTA→envelope, PALETA→palette, MALA_PACZKA/SMALL→small_parcel). Default `box`.
|
||||||
|
- **Files:** `src/Modules/Shipments/PolkurierShipmentService.php`
|
||||||
|
- **Verification:** Trzeci submit → paczka utworzona w polkurier.
|
||||||
|
|
||||||
|
**3. [Response shape mismatch] `extractOrderNumber` nie znajdowal pola `number`**
|
||||||
|
- **Found during:** Task 4 (live test, trzeci submit — paczka utworzona w polkurier ale w orderPRO `status=pending`)
|
||||||
|
- **Issue:** Pierwotny parsing szukal kluczy `orderno`/`order_no` w odpowiedzi. polkurier zwraca SDK Order entity z polem `number` + tablica `waybills[]` z `OrderWaybill` entity (zweryfikowane w `Order.php` setterach `setNumber()`, `addWaybill()`).
|
||||||
|
- **Fix:** Nowe metody `extractOrderNumber()` (priorytet `number`, fallback `orderno`/`order_no`/`order_number`/`order_id`/`id`, obsluga wrappera `{order:{...}}` i list) + `extractTrackingNumber()` (priorytet `waybills[0].number`, fallback top-level klucze). Dodatkowo diagnostyka: gdy `orderno=''`, zapis fragmentu surowej odpowiedzi do `error_message`.
|
||||||
|
- **Files:** `src/Modules/Shipments/PolkurierShipmentService.php`
|
||||||
|
- **Verification:** Czwarty submit → `status=created`, `tracking_number` ustawiony, etykieta pobrana z pola `file`.
|
||||||
|
|
||||||
|
**4. [API misunderstanding] Bogus parametry rozmiaru etykiety**
|
||||||
|
- **Found during:** Task 4 (live test, czwarty submit — etykieta A4 zamiast A6)
|
||||||
|
- **Issue:** Iteracja w 3 bogus parametry (`format`/`label_size`/`paper_size`) wyslanych do `get_label` — bez efektu, bo API ignoruje nieznane pola. Operator zglosil ze etykieta nadal A4.
|
||||||
|
- **Fix:** Pobranie i przeczytanie oficjalnej dokumentacji PDF v1.11 potwierdzilo: `get_label` przyjmuje WYLACZNIE `orderno`. Rozmiar A4/A6 sterowany jest w panelu klienta polkurier.pl. Usuniete bogus parametry, `getLabel($login, $token, $orderno)` ma tylko 3 argumenty. Operator zmienil ustawienie w panelu polkurier — etykieta A6 OK.
|
||||||
|
- **Files:** `src/Modules/Settings/PolkurierApiClient.php`, `src/Modules/Shipments/PolkurierShipmentService.php`
|
||||||
|
- **Verification:** Operator nadal kolejna paczke → etykieta A6.
|
||||||
|
|
||||||
|
### Scope Removals
|
||||||
|
|
||||||
|
**UI selektor punktow paczkomatowych (AJAX endpoint + dropdown)**
|
||||||
|
- **Removed during:** Task 3 iteracje (po feedback operatora)
|
||||||
|
- **Reason:** Istnieje juz pole `name="receiver_point_id"` w sekcji Adres odbiorcy z auto-fillem `parcel_external_id` z importu zamowienia. Dodatkowy selektor byl duplikatem. Operator wpisuje czysty ID recznie (np. `POP-RZE54`).
|
||||||
|
- **Files removed:** `PolkurierShipmentService::lookupPickupPoints()`, `ShipmentController::polkurierPoints()`, route `/shipments/polkurier/points`, JS handler `loadPolkurierPoints/renderPolkurierPoints`.
|
||||||
|
- **Zachowane:** `PolkurierApiClient::getInpostParcelMachines()` i `getCourierPoints()` — gotowe stuby na przyszle rozszerzenie (kolejna faza paczkomatow UI).
|
||||||
|
|
||||||
|
### Scope Additions
|
||||||
|
|
||||||
|
**`service_code` + `pickup_*` w `ShipmentController::create()`**
|
||||||
|
- **Reason:** PolkurierShipmentService potrzebuje servicecode z available_carriers (osobne pole niz `delivery_method_id` zeby JS mogl wstawic czysta wartosc) + optional pickup override.
|
||||||
|
- **Impact:** Backward compatible — Apaczka/InPost/AllegroWZA ignoruja te pola w swoich createShipment.
|
||||||
|
|
||||||
|
### Deferred Items
|
||||||
|
|
||||||
|
- **Phase 128 follow-up:** Operator uruchomi `php bin/migrate.php` gdy XAMPP MySQL online (utworzy 7 wpisow `provider='polkurier'` w `delivery_status_mappings`).
|
||||||
|
- **Phase 128 follow-up:** Cron `shipment_tracking_sync` weryfikacja przy pierwszej zywej paczce polkurier w `in_transit` — pierwszy realny passthrough TrackingService dopiero przy nastepnej niezanulowanej paczce.
|
||||||
|
- **Kolejna faza:** Paczkomaty UI panel (`InpostParcelMachines`/`PocztexPostOffices`/`Kurier48PostOffices` selectory w `prepare.php`), presety przesylek z `provider_code='polkurier'`, `OrderValuationV2` (wycena przed nadaniem).
|
||||||
|
|
||||||
|
## Issues Encountered
|
||||||
|
|
||||||
|
| Issue | Resolution |
|
||||||
|
|-------|------------|
|
||||||
|
| Migracja `20260514_000115` nie uruchomiona — MySQL offline z poziomu agenta (Bash) | Operator uruchomi recznie `php bin/migrate.php` gdy XAMPP MySQL online. Migracja jest idempotentna. |
|
||||||
|
| AC-3 (cron tracking) nie zweryfikowane na zywej bazie | Operator anulowal obie paczki w panelu polkurier po teście — cron tracking nie mial co pingowac. Implementacja kompletna i defensywna (graceful null). Weryfikacja przy nastepnej zywej paczce. |
|
||||||
|
| PDF v1.11 polkurier API niedostepny przez WebFetch (binary content) | Pobrane przez WebFetch jako binarny PDF + `pdftotext.exe` (Git Bash bundle) → tekst w `.paul/phases/128-polkurier-shipment-service/polkurier-api-docs.txt`. Pattern dla przyszlych fetchy binary docs. |
|
||||||
|
|
||||||
|
## Next Phase Readiness
|
||||||
|
|
||||||
|
**Ready:**
|
||||||
|
- polkurier dziala end-to-end w UI (tworzenie + etykieta + tracking gotowy).
|
||||||
|
- Kontrakt API zweryfikowany na oficjalnej dokumentacji (PDF v1.11) — przyszle fazy maja stale referencyjne zrodlo.
|
||||||
|
- Diagnostyka silent-fail pattern do reuse dla nowych integracji.
|
||||||
|
- `getInpostParcelMachines`/`getCourierPoints` stuby gotowe dla kolejnej fazy paczkomaty UI.
|
||||||
|
|
||||||
|
**Concerns:**
|
||||||
|
- AC-3 (cron tracking) nie zweryfikowane na zywej bazie — pierwszy passthrough wymaga niezanulowanej paczki polkurier. Defensywne kodowanie (graceful null) chroni przed crashem crona, ale realne dzialanie testowalne dopiero na zywej paczce.
|
||||||
|
- `extractOrderNumber`/`extractTrackingNumber` fallback chain moze nie pokryc 100% wariantow shape odpowiedzi (np. order zlecone z dodatkowymi opcjami). Pattern z `error_message` dump pomoze w iteracji.
|
||||||
|
|
||||||
|
**Blockers:**
|
||||||
|
- None.
|
||||||
|
|
||||||
|
---
|
||||||
|
*Phase: 128-polkurier-shipment-service, Plan: 01*
|
||||||
|
*Completed: 2026-05-14*
|
||||||
2963
.paul/phases/128-polkurier-shipment-service/polkurier-api-docs.txt
Normal file
2963
.paul/phases/128-polkurier-shipment-service/polkurier-api-docs.txt
Normal file
File diff suppressed because it is too large
Load Diff
295
.paul/phases/129-order-user-notes/129-01-PLAN.md
Normal file
295
.paul/phases/129-order-user-notes/129-01-PLAN.md
Normal file
@@ -0,0 +1,295 @@
|
|||||||
|
---
|
||||||
|
phase: 129-order-user-notes
|
||||||
|
plan: 01
|
||||||
|
type: execute
|
||||||
|
wave: 1
|
||||||
|
depends_on: []
|
||||||
|
files_modified:
|
||||||
|
- database/migrations/20260514_000116_extend_order_notes_user_authored.sql
|
||||||
|
- src/Modules/Orders/OrdersRepository.php
|
||||||
|
- src/Modules/Orders/OrdersController.php
|
||||||
|
- src/Modules/Orders/OrderNotesService.php
|
||||||
|
- routes/web.php
|
||||||
|
- resources/views/orders/show.php
|
||||||
|
- resources/views/orders/list.php
|
||||||
|
- resources/lang/pl.php
|
||||||
|
- resources/scss/modules/_order-notes.scss
|
||||||
|
- resources/scss/app.scss
|
||||||
|
- public/assets/js/modules/order-notes.js
|
||||||
|
- resources/views/layouts/app.php
|
||||||
|
autonomous: true
|
||||||
|
delegation: auto
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
## Goal
|
||||||
|
Wprowadzic moduł notatek użytkownika w zamówieniach: pełen CRUD (add/edit/delete tylko dla autora) w sekcji "Wiadomosci i zalaczniki" w szczegółach zamówienia (`/orders/{id}`), oraz licznik notatek `[N]` jako mały badge przy numerze zamówienia na liście (`/orders/list`).
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
Operator potrzebuje miejsca na własne adnotacje per zamówienie (uzgodnienia z klientem, ustalenia wewnętrzne, flagi do dalszej obsługi), niezależne od zaimportowanych notatek ze źródła. Badge na liście pozwala szybko zobaczyć które zamówienia mają adnotacje bez wchodzenia w szczegóły — analogicznie do licznika zwrotów klienta (Phase 106).
|
||||||
|
|
||||||
|
## Output
|
||||||
|
- Migracja rozszerzająca `order_notes` o `user_id` (FK→users SET NULL) + `author_name` (snapshot) + `body` (czytelny alias do TEXT) — z reuse istniejącej kolumny `comment` jako body i nowymi kolumnami; nowy `note_type='user'`.
|
||||||
|
- `OrderNotesService` z metodami `create/update/delete/listUserNotes/countUserNotesForOrders`.
|
||||||
|
- 3 routes: `POST /orders/{id}/notes`, `POST /orders/{id}/notes/{noteId}/update`, `POST /orders/{id}/notes/{noteId}/delete`.
|
||||||
|
- W sekcji "Wiadomosci i zalaczniki" w `show.php`: lista notatek użytkownika (data + autor + tresc + akcje edit/delete dla autora) + formularz dodawania; importowane notatki zachowane jako osobny blok wyżej (filtr po `note_type`).
|
||||||
|
- Badge `[N]` w komórce `order_ref` listy zamówień (neutralna kolorystyka, klasa `order-notes-badge`, link do `#notes` w szczegółach).
|
||||||
|
</objective>
|
||||||
|
|
||||||
|
<context>
|
||||||
|
## Project Context
|
||||||
|
@.paul/PROJECT.md
|
||||||
|
@.paul/ROADMAP.md
|
||||||
|
@.paul/STATE.md
|
||||||
|
@.paul/codebase/architecture.md
|
||||||
|
@.paul/codebase/db_schema.md
|
||||||
|
|
||||||
|
## Source Files (key spots)
|
||||||
|
@src/Modules/Orders/OrdersRepository.php
|
||||||
|
@src/Modules/Orders/OrdersController.php
|
||||||
|
@routes/web.php
|
||||||
|
@resources/views/orders/show.php
|
||||||
|
@resources/views/orders/list.php
|
||||||
|
@resources/lang/pl.php
|
||||||
|
|
||||||
|
## Reference Patterns
|
||||||
|
- Phase 106 Customer Return Badge — `customerReturnedCountSubquerySql()` w `OrdersRepository`, render `risk-return-badge` w `OrdersController::formatOrderRow()` (linia ~656–659).
|
||||||
|
- Phase 124 SMS Templates Service — `SmsTemplatesService` jako wzorzec serwisu CRUD nad pojedynczą tabelą.
|
||||||
|
- Phase 113-115 toggle pattern — `invoice-requested-toggle.js` jako wzorzec wanilijowego JS POST-em z CSRF.
|
||||||
|
|
||||||
|
## Existing `order_notes` schema (draft 20260302_orders_schema_v1.sql)
|
||||||
|
Tabela już istnieje: `id`, `order_id`, `source_note_id`, `note_type`, `created_at_external`, `comment`, `payload_json`, `created_at`, `updated_at`, UNIQUE `(order_id, source_note_id)`. Obecnie używana tylko do notatek importowanych ze źródła (shopPRO/Allegro mappers; loadOrderNotes w OrdersRepository.php:596).
|
||||||
|
|
||||||
|
<clarifications>
|
||||||
|
- **Schemat DB** — Gdzie przechowywać notatki użytkownika?
|
||||||
|
→ Odpowiedź: Rozszerz `order_notes` o `user_id`+`author_name`+nowy `note_type='user'`.
|
||||||
|
- **Badge UI** — Jak ma wyglądać label z liczbą notatek na liście zamówień?
|
||||||
|
→ Odpowiedź: Mały badge `[N]` przy nr zamówienia (neutralna kolorystyka, klik scrolluje do sekcji notatek w szczegółach).
|
||||||
|
- **CRUD scope** — Co operator może robić z własnymi notatkami?
|
||||||
|
→ Odpowiedź: Pełny CRUD (add/edit/delete) — autor lub admin może edytować/usuwać. Brak systemu ról w aplikacji → implementacja: edit/delete dozwolone tylko gdy `note.user_id === session.user_id` (sam autor). Jeżeli operator chce uprawnienia globalne, odłożyć do osobnej fazy po wprowadzeniu ról.
|
||||||
|
- **Umiejscowienie** — Gdzie umieścić UI notatek w szczegółach zamówienia?
|
||||||
|
→ Odpowiedź: W sekcji "Wiadomosci i zalaczniki" (już istnieje w details panel, `resources/views/orders/show.php` linia ~449–463). Tam dorzucamy listę notatek użytkownika + formularz dodawania. Importowane notatki ze źródła zachowujemy jako osobny mniejszy blok.
|
||||||
|
</clarifications>
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<acceptance_criteria>
|
||||||
|
|
||||||
|
## AC-1: Migracja DB — kolumny user notes
|
||||||
|
```gherkin
|
||||||
|
Given baza zawiera tabelę `order_notes` ze starymi importowanymi rekordami (note_type IN ('shoppro','allegro','message'))
|
||||||
|
When uruchomię `php bin/migrate.php`
|
||||||
|
Then tabela `order_notes` ma nowe kolumny `user_id INT UNSIGNED NULL` (FK→users(id) ON DELETE SET NULL), `author_name VARCHAR(190) NULL`, oraz indeks `idx_order_notes_type_order (note_type, order_id)`. Istniejące rekordy mają `user_id=NULL`, `author_name=NULL`, `note_type` niezmieniony. Migracja jest idempotentna (re-run = no-op via `INFORMATION_SCHEMA` guard lub `IF NOT EXISTS`).
|
||||||
|
```
|
||||||
|
|
||||||
|
## AC-2: Tworzenie notatki użytkownika
|
||||||
|
```gherkin
|
||||||
|
Given zalogowany user (id=5, name="Jacek Pyziak") otwiera `/orders/1090`
|
||||||
|
When wpisuje treść w textarea formularza "Dodaj notatkę" i klika "Zapisz"
|
||||||
|
Then POST `/orders/1090/notes` z `_token` i `body` zapisuje wiersz `order_notes(order_id=1090, note_type='user', user_id=5, author_name='Jacek Pyziak', comment=<treść>, created_at=NOW())`, dodaje wpis `order_activity_log(event_type='note', summary='Dodano notatkę', actor_type='user', actor_name='Jacek Pyziak')`, flashuje sukces i przekierowuje do `/orders/1090#notes`.
|
||||||
|
```
|
||||||
|
|
||||||
|
## AC-3: Edycja i usuwanie tylko przez autora
|
||||||
|
```gherkin
|
||||||
|
Given notatka #42 ma user_id=5 i jest renderowana na `/orders/1090`
|
||||||
|
When zalogowany user id=5 klika "Edytuj" → zmienia treść → "Zapisz"
|
||||||
|
Then POST `/orders/1090/notes/42/update` aktualizuje `comment` i `updated_at`, lista re-renderuje się z nową treścią
|
||||||
|
|
||||||
|
When ten sam user id=5 klika "Usuń" → potwierdza w `OrderProAlerts.confirm` z `danger:true`
|
||||||
|
Then POST `/orders/1090/notes/42/delete` usuwa rekord (DELETE WHERE id=42 AND user_id=5), flashuje sukces
|
||||||
|
|
||||||
|
When zalogowany user id=8 (inny niż autor) próbuje POST `/orders/1090/notes/42/update` lub `/delete`
|
||||||
|
Then odpowiedź HTTP 403 z komunikatem "Brak uprawnień — tylko autor może edytować/usuwać notatkę" (flash danger), wiersz pozostaje nienaruszony
|
||||||
|
```
|
||||||
|
|
||||||
|
## AC-4: Lista notatek w sekcji "Wiadomosci i zalaczniki"
|
||||||
|
```gherkin
|
||||||
|
Given zamówienie 1090 ma 2 notatki użytkownika (autor=Jacek, daty 2026-05-14 10:00 i 2026-05-14 12:30) oraz 1 zaimportowaną z shopPRO (`note_type='shoppro'`)
|
||||||
|
When otwieram `/orders/1090` i scrolluję do "Wiadomosci i zalaczniki"
|
||||||
|
Then widzę:
|
||||||
|
1. Blok "Notatki" (id="notes"): 2 wpisy w kolejności desc po `created_at`, każdy z `data | autor` w nagłówku i treścią poniżej, oraz przyciskami "Edytuj"/"Usuń" tylko dla wpisów, których user_id == session.user_id
|
||||||
|
2. Inline formularz dodawania notatki (textarea + przycisk "Zapisz") z CSRF tokenem
|
||||||
|
3. Blok "Wiadomości ze źródła" (subtelny styl, mniejszy): 1 wpis shopPRO bez akcji edit/delete
|
||||||
|
```
|
||||||
|
|
||||||
|
## AC-5: Badge `[N]` na liście zamówień
|
||||||
|
```gherkin
|
||||||
|
Given zamówienie 1090 ma 2 user-notes, zamówienie 1091 ma 0
|
||||||
|
When otwieram `/orders/list`
|
||||||
|
Then przy nr zamówienia 1090 widzę mały badge `<span class="order-notes-badge" title="2 notatki">[2]</span>` jako link do `/orders/1090#notes` (neutralna kolorystyka — niebieskoszary tekst na jasnym tle, mniejszy niż badge zwrotów), badge przy 1091 jest ukryty (count=0 ⇒ pusty string).
|
||||||
|
```
|
||||||
|
|
||||||
|
## AC-6: Subquery licznika nie psuje paginacji/sortowania
|
||||||
|
```gherkin
|
||||||
|
Given lista `/orders/list` z 1000 zamówieniami filtrowana po statusie i sortowana
|
||||||
|
When wykonam paginację, filtrowanie i sortowanie
|
||||||
|
Then licznik `user_notes_count` jest wyliczany subquery (`SELECT COUNT(*) FROM order_notes WHERE order_id = o.id AND note_type = 'user'`) jako kolumna SELECT — bez wpływu na WHERE/GROUP BY/ORDER. Czas wykonania zapytania pozostaje rozsądny dzięki indeksowi `idx_order_notes_type_order`.
|
||||||
|
```
|
||||||
|
|
||||||
|
</acceptance_criteria>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 1: Migracja DB + extend `order_notes` o pola user-authored</name>
|
||||||
|
<files>database/migrations/20260514_000116_extend_order_notes_user_authored.sql, .paul/codebase/db_schema.md</files>
|
||||||
|
<action>
|
||||||
|
Utwórz migrację `20260514_000116_extend_order_notes_user_authored.sql`:
|
||||||
|
- `ALTER TABLE order_notes ADD COLUMN user_id INT UNSIGNED NULL AFTER note_type;`
|
||||||
|
- `ALTER TABLE order_notes ADD COLUMN author_name VARCHAR(190) NULL AFTER user_id;`
|
||||||
|
- `ALTER TABLE order_notes ADD CONSTRAINT order_notes_user_fk FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE;`
|
||||||
|
- `ALTER TABLE order_notes ADD INDEX idx_order_notes_type_order (note_type, order_id);`
|
||||||
|
- Każdy ADD owijaj w `INFORMATION_SCHEMA` guard (`SET @x = (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE ...); SET @sql = IF(@x=0, 'ALTER TABLE...', 'SELECT 1'); PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s;`) — wzorzec z istniejących migracji. UWAGA: ostatni guard musi być DDL no-op (`ALTER TABLE order_notes COMMENT='phase-129 idempotent'`) NIE `SELECT 1` (decyzja z Phase 115).
|
||||||
|
Następnie zaktualizuj `.paul/codebase/db_schema.md` (sekcja Orders → order_notes): dopisz tabelę z nowymi kolumnami i indeksem, opisz że `note_type='user'` oznacza notatki autorskie z `user_id`/`author_name`, a stare `note_type IN ('shoppro','allegro','message')` to importowane.
|
||||||
|
Avoid: zmiany w `comment`/`payload_json`/`source_note_id` (ochrona istniejących importów). UNIQUE `(order_id, source_note_id)` zostaje — user notes mają source_note_id=NULL, więc MySQL traktuje każdy NULL jako unique row.
|
||||||
|
</action>
|
||||||
|
<verify>php bin/migrate.php → migration logged; `DESCRIBE order_notes;` pokazuje nowe kolumny i FK; re-run migracji = no-op (idempotent guard).</verify>
|
||||||
|
<done>AC-1 satisfied: kolumny dodane, FK aktywny, indeks utworzony, schema doc zaktualizowany.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 2: OrderNotesService + repository extension + routes + Controller actions</name>
|
||||||
|
<files>src/Modules/Orders/OrderNotesService.php, src/Modules/Orders/OrdersRepository.php, src/Modules/Orders/OrdersController.php, routes/web.php</files>
|
||||||
|
<action>
|
||||||
|
1) Utwórz `src/Modules/Orders/OrderNotesService.php` (final class):
|
||||||
|
- `__construct(\PDO $pdo)`
|
||||||
|
- `listUserNotes(int $orderId): array` — `SELECT id, user_id, author_name, comment AS body, created_at, updated_at FROM order_notes WHERE order_id = :order_id AND note_type = 'user' ORDER BY created_at DESC, id DESC`
|
||||||
|
- `listImportedNotes(int $orderId): array` — stary `loadOrderNotes` logic, ale z filtrem `note_type != 'user'`
|
||||||
|
- `create(int $orderId, int $userId, string $authorName, string $body): int` — INSERT + zwraca lastInsertId; po INSERT wywołaj `OrderActivityLogService::log(orderId, 'note', 'Dodano notatkę', actorName=$authorName)` jeśli serwis istnieje (jeśli nie — INSERT do `order_activity_log` bezpośrednio przez PDO; pattern z Phase 56 OrderPaymentsService).
|
||||||
|
- `update(int $noteId, int $userId, string $body): bool` — UPDATE WHERE id=:id AND user_id=:user_id, zwraca `$stmt->rowCount() > 0`. Rzut `RuntimeException` z kodem 403 gdy rowCount=0 (nieautoryzowany lub nie istnieje).
|
||||||
|
- `delete(int $noteId, int $userId): bool` — DELETE WHERE id=:id AND user_id=:user_id; analogiczna obsługa autoryzacji.
|
||||||
|
- Walidacja `body`: trim, nie pusty (min 1 znak), max 2000 znaków (TEXT). Throw `InvalidArgumentException` gdy pusty.
|
||||||
|
|
||||||
|
2) `OrdersRepository.php`:
|
||||||
|
- Dodaj prywatną metodę `userNotesCountSubquerySql(string $orderAlias): string` zwracającą string `(SELECT COUNT(*) FROM order_notes WHERE order_id = ' . $orderAlias . '.id AND note_type = \'user\')` (wzorzec z `customerReturnedCountSubquerySql`).
|
||||||
|
- W `fetchOrdersForList()` (i innych metodach budujących SELECT dla listy) dodaj kolumnę `... AS user_notes_count` obok `customer_returned_count`.
|
||||||
|
- W `getOrderDetails()` doloż `user_notes_count` i `user_notes_list` (przez OrderNotesService — wstrzyknij go w konstruktorze, lub wczytaj inline analogicznie do `loadOrderNotes`). Zachowaj `loadOrderNotes` jako `loadImportedOrderNotes` (rename) lub dorzuć nową metodę `loadUserOrderNotes` filtrującą po `note_type='user'`.
|
||||||
|
|
||||||
|
3) `OrdersController.php`:
|
||||||
|
- Dodaj prywatne `$orderNotesService` w konstruktorze.
|
||||||
|
- Metoda `storeNote(Request $request): Response` — pobierz orderId z `$request->input('id')` (pattern Phase 108), userId z sesji (`$_SESSION['user']['id']`), authorName z sesji (`$_SESSION['user']['name']`), `body` z `$request->input('body')`. Walidacja CSRF. Wywołaj `OrderNotesService::create()`. Flash success/error, redirect `/orders/{id}#notes`.
|
||||||
|
- Metoda `updateNote(Request $request): Response` — params `id` (order) i `noteId`. CSRF + user authorization (przez return z service). Redirect `/orders/{id}#notes`.
|
||||||
|
- Metoda `deleteNote(Request $request): Response` — analogicznie.
|
||||||
|
- W `formatOrderRow()` (linia ~656): dodaj wyliczenie `$userNotesCount = max(0, (int) ($row['user_notes_count'] ?? 0));` i `$notesBadge = $userNotesCount >= 1 ? ' <a href="/orders/' . $orderId . '#notes" class="order-notes-badge" title="' . $userNotesCount . ' notatek">[' . $userNotesCount . ']</a>' : '';` — wklej w `order_ref` HTML obok `$returnedBadge`.
|
||||||
|
|
||||||
|
4) `routes/web.php` (po linii ~595, blok orders):
|
||||||
|
```php
|
||||||
|
$router->post('/orders/{id}/notes', [$ordersController, 'storeNote'], [$authMiddleware]);
|
||||||
|
$router->post('/orders/{id}/notes/{noteId}/update', [$ordersController, 'updateNote'], [$authMiddleware]);
|
||||||
|
$router->post('/orders/{id}/notes/{noteId}/delete', [$ordersController, 'deleteNote'], [$authMiddleware]);
|
||||||
|
```
|
||||||
|
Wstrzyknięcie `OrderNotesService` analogicznie do innych serwisów (sprawdź jak `SmsConversationService` lub `OrderPaymentsService` są instancjonowane — pattern factory w `Application.php`/`CronHandlerFactory.php` lub bezpośrednie `new` w routes).
|
||||||
|
|
||||||
|
Avoid: sklejania SQL z input; pomijania CSRF; mieszania `comment` (legacy text imported) z nowym body — używamy tej samej kolumny, ale w service zawsze filtrujemy po `note_type='user'`.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
`php -l` na wszystkich zmienionych plikach; `composer dump-autoload` jeśli trzeba; smoke ręczny po deploy: POST `/orders/{X}/notes` z curl (sesja + CSRF) → 302 + nowy wiersz w `order_notes`; UPDATE/DELETE jako inny user → 403 + flash danger.
|
||||||
|
</verify>
|
||||||
|
<done>AC-2, AC-3, AC-6 satisfied: CRUD działa, autoryzacja po `user_id` egzekwowana, subquery count w listingu bez wpływu na paginację.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 3: UI — sekcja notatek w show.php, badge na list.php, JS edit modal, SCSS, i18n</name>
|
||||||
|
<files>resources/views/orders/show.php, resources/views/orders/list.php, resources/lang/pl.php, resources/scss/modules/_order-notes.scss, resources/scss/app.scss, public/assets/js/modules/order-notes.js, resources/views/layouts/app.php</files>
|
||||||
|
<action>
|
||||||
|
1) `resources/views/orders/show.php` (sekcja "Wiadomosci i zalaczniki", linia ~449–463):
|
||||||
|
- Zmień blok renderowania na 2 sub-listy:
|
||||||
|
a) `<div id="notes" class="order-user-notes">` — header "Notatki", iteracja po `$userNotesList` (passed z controllera). Każda notatka: `<div class="order-event order-event--user">` z `<div class="order-event__head">data | autor</div>`, `<div class="order-event__body">body</div>`, oraz `<div class="order-event__actions">` z przyciskami `Edytuj` / `Usuń` widocznymi gdy `(int)($note['user_id'] ?? 0) === $currentUserId`. Przycisk "Usuń" jako `<form method="post" action="/orders/{id}/notes/{noteId}/delete">` + ukryty submit + JS handler wywołujący `OrderProAlerts.confirm({title:'Usunąć notatkę?', message:'Tej operacji nie można cofnąć.', danger:true, onConfirm: function(){ form.submit(); }})` (pattern options-object — decyzja Phase 114).
|
||||||
|
- Pod listą: formularz `<form method="post" action="/orders/{id}/notes">` z CSRF `_token`, `<textarea name="body" maxlength="2000" required>`, przycisk "Zapisz".
|
||||||
|
b) `<div class="order-imported-notes">` — header "Wiadomości ze źródła", iteracja po `$importedNotesList`, render jak dotychczas (bez akcji).
|
||||||
|
- W górze widoku pobierz `$userNotesList = is_array($userNotes ?? null) ? $userNotes : [];`, `$importedNotesList = is_array($importedNotes ?? null) ? $importedNotes : [];`, `$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);`.
|
||||||
|
|
||||||
|
2) `resources/views/orders/list.php` — `order_ref` HTML już jest generowany w controllerze przez `formatOrderRow()`, więc badge wleci automatycznie. Jeśli list.php gdziekolwiek inline renderuje order ref, sprawdź i nie dubluj.
|
||||||
|
|
||||||
|
3) `resources/lang/pl.php` — dodaj klucze w sekcji `orders.details`:
|
||||||
|
```php
|
||||||
|
'notes_user_title' => 'Notatki',
|
||||||
|
'notes_user_empty' => 'Brak notatek.',
|
||||||
|
'notes_user_add_placeholder' => 'Wpisz notatkę...',
|
||||||
|
'notes_user_save' => 'Zapisz',
|
||||||
|
'notes_user_edit' => 'Edytuj',
|
||||||
|
'notes_user_delete' => 'Usuń',
|
||||||
|
'notes_user_confirm_delete' => 'Usunąć notatkę?',
|
||||||
|
'notes_imported_title' => 'Wiadomości ze źródła',
|
||||||
|
'notes_forbidden' => 'Brak uprawnień — tylko autor może edytować/usuwać notatkę.',
|
||||||
|
'notes_created' => 'Notatka dodana.',
|
||||||
|
'notes_updated' => 'Notatka zaktualizowana.',
|
||||||
|
'notes_deleted' => 'Notatka usunięta.',
|
||||||
|
```
|
||||||
|
|
||||||
|
4) `resources/scss/modules/_order-notes.scss` — utwórz nowy moduł:
|
||||||
|
- `.order-user-notes`, `.order-imported-notes`, `.order-event--user`, `.order-event__actions` (flex, gap 8px), `.order-event__actions .btn-icon` (mały rozmiar).
|
||||||
|
- `.order-notes-badge` — neutralny styl (np. `background: #eef2ff; color: #4338ca; padding: 1px 6px; border-radius: 10px; font-size: 11px; font-weight: 600; text-decoration: none; margin-left: 4px;`). Hover: `background: #e0e7ff;`. Subtelniej niż `.risk-return-badge` (czerwony, alertowy).
|
||||||
|
- Formularz dodawania notatki: `.order-note-form textarea { width: 100%; min-height: 60px; }`.
|
||||||
|
Następnie dodaj `@use 'modules/order-notes';` w `resources/scss/app.scss`. Build SCSS lub powiedz operatorowi by uruchomił `npm run build:css` / `php tools/build-scss.php` (sprawdź jaki jest build pipeline w projekcie).
|
||||||
|
|
||||||
|
5) `public/assets/js/modules/order-notes.js` — wanilijowy JS:
|
||||||
|
- Inline edit: klik "Edytuj" zamienia `order-event__body` na textarea + przyciski "Zapisz"/"Anuluj"; "Zapisz" POST `fetch('/orders/'+orderId+'/notes/'+noteId+'/update', {method:'POST', body: new FormData(form)})` → jeśli OK, reload listy AJAX-em lub `location.reload()` (najprostsze, pattern z inline-status-change.js).
|
||||||
|
- Klik "Usuń" — `OrderProAlerts.confirm({title:'Usunąć notatkę?', message:'Tej operacji nie można cofnąć.', danger:true, confirmLabel:'Usuń', onConfirm: function(){ form.submit(); }})`.
|
||||||
|
- Idempotent guard: `if (window.__orderNotesInit) return; window.__orderNotesInit = true;` (pattern Phase 114 confirm-delete.js).
|
||||||
|
|
||||||
|
6) `resources/views/layouts/app.php` — załącz nowy moduł JS (pattern z `invoice-requested-toggle.js`):
|
||||||
|
```php
|
||||||
|
<script src="/assets/js/modules/order-notes.js?ver=<?= filemtime(...) ?: 0 ?>"></script>
|
||||||
|
```
|
||||||
|
|
||||||
|
Avoid: natywnego `confirm()` (zakaz CLAUDE.md); inline styles (zakaz CLAUDE.md — wszystko do SCSS); duplikowania renderowania importowanych notatek; ujawniania `user_id` w UI jeśli `users.id` jest wrażliwe (nie jest — to wewnętrzny ID).
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
Otwórz `/orders/{X}` → widać sekcję "Notatki" + form dodawania; dodaj notatkę → pojawia się w liście z datą i autorem. Spróbuj edit cudzej notatki (z innym session.user_id) → przyciski edit/delete niewidoczne, próba POST → 403. Otwórz `/orders/list` → badge `[N]` widoczny przy zamówieniach z notatkami. Sprawdź czy `risk-return-badge` (Phase 106) nadal działa obok.
|
||||||
|
</verify>
|
||||||
|
<done>AC-4, AC-5 satisfied: UI sekcji notatek działa w show.php, badge w list.php widoczny, akcje edit/delete poprawnie ograniczone do autora.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<boundaries>
|
||||||
|
|
||||||
|
## DO NOT CHANGE
|
||||||
|
- `order_notes.comment`, `order_notes.source_note_id`, `order_notes.payload_json` (kontrakt importu z shopPRO/Allegro — Phase 79 i wcześniejsze).
|
||||||
|
- `OrdersRepository::replaceNotes()`/`loadOrderNotes()` semantyka dla importu — jeśli rename, zachowaj BC alias lub zaktualizuj wszystkie wywołania (delta-only import, Phase 112).
|
||||||
|
- `.risk-return-badge` SCSS i logika (Phase 106) — badge notatek to osobna klasa, nie modyfikujemy zwrotów.
|
||||||
|
- `OrderProAlerts.confirm` API — używamy options-object (Phase 114 decyzja).
|
||||||
|
|
||||||
|
## SCOPE LIMITS
|
||||||
|
- Brak mentions/@-tagowania userów.
|
||||||
|
- Brak załączników do notatek (tylko tekst).
|
||||||
|
- Brak edycji historii edycji notatki (audit log w `order_activity_log` ma tylko `Dodano/Edytowano/Usunięto notatkę` — bez before/after JSON).
|
||||||
|
- Brak globalnych uprawnień admin override — tylko autor edytuje/usuwa (system ról nie istnieje; odłożone do osobnej fazy).
|
||||||
|
- Brak filtrów listy zamówień po "ma/nie ma notatki" — można dodać w przyszłości.
|
||||||
|
- Brak emitowania eventu automatyzacji `note.created` — można dodać jako osobny plan jeśli operator chce.
|
||||||
|
|
||||||
|
</boundaries>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
Before declaring plan complete:
|
||||||
|
- [ ] `php bin/migrate.php` przechodzi bez błędu, re-run = no-op
|
||||||
|
- [ ] `php -l` na każdym zmienionym pliku PHP zwraca "No syntax errors"
|
||||||
|
- [ ] POST `/orders/{id}/notes` jako user A tworzy notatkę
|
||||||
|
- [ ] POST `/orders/{id}/notes/{noteId}/update` jako user A działa, jako user B zwraca 403
|
||||||
|
- [ ] POST `/orders/{id}/notes/{noteId}/delete` jako user A usuwa, jako user B 403
|
||||||
|
- [ ] `/orders/list` pokazuje badge `[N]` przy zamówieniach z notatkami, ukryty gdy N=0
|
||||||
|
- [ ] `/orders/{id}#notes` scrolluje do sekcji notatek
|
||||||
|
- [ ] Importowane notatki ze źródła (shopPRO/allegro) renderują się jako osobny blok bez przycisków edit/delete
|
||||||
|
- [ ] Badge zwrotów (Phase 106) działa obok badge'a notatek (oba widoczne dla zamówień z obojgiem)
|
||||||
|
- [ ] SCSS skompilowany do `public/assets/css/app.css`
|
||||||
|
- [ ] CSRF wymagany w każdym POST — brak tokenu = 419/403
|
||||||
|
- [ ] Brak natywnych `confirm()` w nowym JS — wszystko przez `OrderProAlerts.confirm`
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- AC-1..AC-6 spełnione
|
||||||
|
- Brak regresji w imporcie notatek shopPRO/Allegro (delta-only import z Phase 112 nadal działa, `replaceNotes` filtruje tylko `note_type != 'user'` jeśli musi)
|
||||||
|
- Czas wykonania `/orders/list` z subquery `user_notes_count` nie pogarsza się drastycznie (indeks `idx_order_notes_type_order` aktywny)
|
||||||
|
- `.paul/codebase/db_schema.md` i `.paul/codebase/architecture.md` zaktualizowane o nowy serwis i kolumny
|
||||||
|
- `.paul/codebase/tech_changelog.md` ma wpis dla Phase 129
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create `.paul/phases/129-order-user-notes/129-01-SUMMARY.md` z:
|
||||||
|
- Krótki opis co zostało zbudowane
|
||||||
|
- Decisions (np. brak admin override → tylko autor edytuje)
|
||||||
|
- Files modified (lista z task'ów)
|
||||||
|
- Migration applied (numer + opis)
|
||||||
|
- Manual smoke checklist dla operatora (POST create, UPDATE as A, UPDATE as B → 403, DELETE, badge na liście, edit modal UX)
|
||||||
|
- Deferred / follow-up (event automatyzacji `note.created`, filtr listy "ma notatki", admin override po wprowadzeniu ról)
|
||||||
|
</output>
|
||||||
191
.paul/phases/129-order-user-notes/129-01-SUMMARY.md
Normal file
191
.paul/phases/129-order-user-notes/129-01-SUMMARY.md
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
---
|
||||||
|
phase: 129-order-user-notes
|
||||||
|
plan: 01
|
||||||
|
subsystem: orders
|
||||||
|
tags: [order-notes, crud, badge, audit-log, user-authored]
|
||||||
|
|
||||||
|
requires:
|
||||||
|
- phase: 106-customer-return-alert
|
||||||
|
provides: badge pattern (`risk-return-badge`) + subquery liczby per zamowienie
|
||||||
|
- phase: 114-accounting-configs-refactor
|
||||||
|
provides: `OrderProAlerts.confirm` options-object API
|
||||||
|
provides:
|
||||||
|
- Pelen CRUD notatek autorskich operatora per zamowienie (`note_type='user'`)
|
||||||
|
- Subquery `user_notes_count` + badge `[N]` na `/orders/list`
|
||||||
|
- Inline edit (toggle textarea) + delete z `OrderProAlerts.confirm`
|
||||||
|
affects:
|
||||||
|
- Przyszle fazy z eventem automatyzacji `note.created` lub admin override po wprowadzeniu rol
|
||||||
|
|
||||||
|
tech-stack:
|
||||||
|
added: []
|
||||||
|
patterns:
|
||||||
|
- "Reuse istniejacej tabeli przez nowy `note_type` zamiast tworzenia osobnej tabeli"
|
||||||
|
- "Autoryzacja CRUD przez `WHERE user_id = :user_id` + rowCount=0 ⇒ RuntimeException(403)"
|
||||||
|
|
||||||
|
key-files:
|
||||||
|
created:
|
||||||
|
- database/migrations/20260514_000116_extend_order_notes_user_authored.sql
|
||||||
|
- src/Modules/Orders/OrderNotesService.php
|
||||||
|
- resources/scss/modules/_order-notes.scss
|
||||||
|
- public/assets/js/modules/order-notes.js
|
||||||
|
modified:
|
||||||
|
- src/Modules/Orders/OrdersController.php
|
||||||
|
- src/Modules/Orders/OrdersRepository.php
|
||||||
|
- routes/web.php
|
||||||
|
- resources/views/orders/show.php
|
||||||
|
- resources/views/layouts/app.php
|
||||||
|
- resources/lang/pl.php
|
||||||
|
- resources/scss/app.scss
|
||||||
|
|
||||||
|
key-decisions:
|
||||||
|
- "Reuse `order_notes` przez `note_type='user'` zamiast osobnej tabeli (clarification #1)"
|
||||||
|
- "Badge neutralny `[N]` (indigo `#eef2ff/#4338ca`) — subtelniejszy niz `.risk-return-badge`"
|
||||||
|
- "Brak admin override — edit/delete tylko dla autora (brak systemu rol w aplikacji)"
|
||||||
|
- "Sekcja `#notes` w istniejacej karcie 'Wiadomosci i zalaczniki' — split na 'Notatki' (user) + 'Wiadomosci ze zrodla' (imported)"
|
||||||
|
|
||||||
|
patterns-established:
|
||||||
|
- "`userNotesCountSubquerySql($orderAlias)` — wzorzec dla COUNT-per-order subquery bez wplywu na ORDER BY/GROUP BY (analogiczny do `customerReturnedCountSubquerySql`)"
|
||||||
|
- "`OrderProAlerts.confirm` z `danger:true` + options-object API dla submit'u formularza DELETE (preventDefault + onConfirm submit)"
|
||||||
|
- "Migracje no-op zawsze jako DDL (`ALTER TABLE COMMENT`), nigdy `SELECT 1` (Phase 115 pattern)"
|
||||||
|
|
||||||
|
duration: ~30min
|
||||||
|
started: 2026-05-14T00:00:00Z
|
||||||
|
completed: 2026-05-14T00:00:00Z
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 129 Plan 01: Order User Notes Summary
|
||||||
|
|
||||||
|
**Pelen CRUD notatek autorskich operatora w zamowieniach (extend `order_notes` o `user_id`/`author_name`/`note_type='user'`), z sekcja `#notes` w szczegolach i badge `[N]` na liscie zamowien.**
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
| Metric | Value |
|
||||||
|
|--------|-------|
|
||||||
|
| Duration | ~30min |
|
||||||
|
| Tasks | 3 of 3 completed |
|
||||||
|
| Files created | 5 |
|
||||||
|
| Files modified | 7 |
|
||||||
|
| AC pass rate | 6 of 6 (pending live smoke) |
|
||||||
|
|
||||||
|
## Acceptance Criteria Results
|
||||||
|
|
||||||
|
| Criterion | Status | Notes |
|
||||||
|
|-----------|--------|-------|
|
||||||
|
| AC-1: Migracja DB — kolumny user notes | Pass (code) | Migracja `20260514_000116_*.sql` z `information_schema` guard + DDL no-op fallback. Aktywacja na zywej bazie: pending operator. |
|
||||||
|
| AC-2: Tworzenie notatki | Pass (code) | `OrderNotesService::create()` + `OrdersController::storeNote()` + `recordActivity('note', 'Dodano notatke')`. Redirect 302 → `/orders/{id}#notes`. |
|
||||||
|
| AC-3: Edycja/usuwanie tylko autor | Pass (code) | UPDATE/DELETE z `WHERE user_id = :user_id`; rowCount=0 ⇒ `RuntimeException(403)`. UI ukrywa przyciski gdy `note.user_id != session.user_id`. |
|
||||||
|
| AC-4: Lista w "Wiadomosci i zalaczniki" | Pass (code) | Sekcja `#notes` z 3 blokami (lista user notes → form dodawania → opcjonalny block "Wiadomosci ze zrodla"). |
|
||||||
|
| AC-5: Badge `[N]` na liscie zamowien | Pass (code) | `<a class="order-notes-badge" href="/orders/{id}#notes">[N]</a>` wstrzykniete w `order_ref` HTML; widoczne tylko gdy `user_notes_count >= 1`. |
|
||||||
|
| AC-6: Subquery liczy bez wplywu na paginacje | Pass (code) | `userNotesCountSubquerySql('o')` jako kolumna SELECT (NIE w WHERE/GROUP BY/ORDER). Wspierane indeksem `idx_order_notes_type_order (note_type, order_id)`. |
|
||||||
|
|
||||||
|
> **Live smoke pending**: migracja na zywym XAMPP MySQL + manualny test wieloosobowy (autor vs inny user) — udokumentowane w STATE.md follow-ups.
|
||||||
|
|
||||||
|
## Verification Results
|
||||||
|
|
||||||
|
```
|
||||||
|
$ C:/xampp/php/php.exe -l src/Modules/Orders/OrderNotesService.php
|
||||||
|
No syntax errors detected
|
||||||
|
|
||||||
|
$ C:/xampp/php/php.exe -l src/Modules/Orders/OrdersController.php
|
||||||
|
No syntax errors detected
|
||||||
|
|
||||||
|
$ C:/xampp/php/php.exe -l src/Modules/Orders/OrdersRepository.php
|
||||||
|
No syntax errors detected
|
||||||
|
|
||||||
|
$ C:/xampp/php/php.exe -l routes/web.php
|
||||||
|
No syntax errors detected
|
||||||
|
|
||||||
|
$ C:/xampp/php/php.exe -l resources/views/orders/show.php
|
||||||
|
No syntax errors detected
|
||||||
|
|
||||||
|
$ node --check public/assets/js/modules/order-notes.js
|
||||||
|
JS OK
|
||||||
|
|
||||||
|
$ npm run build:css
|
||||||
|
sass --style=compressed --no-source-map resources/scss/app.scss public/assets/css/app.css
|
||||||
|
(rebuilt successfully)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Accomplishments
|
||||||
|
|
||||||
|
- **Reuse `order_notes` zamiast osobnej tabeli**: jedna tabela obsluguje 2 semantyki (`note_type IN ('shoppro','allegro','message')` = imported, `note_type='user'` = autorska). `UNIQUE (order_id, source_note_id)` nie blokuje user notes bo MySQL traktuje wiele NULL jako unique.
|
||||||
|
- **CRUD z autoryzacja DB-level**: UPDATE/DELETE filtruja po `user_id = :user_id` w SQL; `rowCount=0` rzuca 403 — eliminuje konieczność osobnego SELECT pre-check'a.
|
||||||
|
- **Badge widoczny od razu w liscie**: subquery `user_notes_count` ekspozuje liczbe w `paginate()`, badge wlozony w `toTableRow()` obok numeru zamowienia (klik scrolluje do `#notes`).
|
||||||
|
|
||||||
|
## Files Created/Modified
|
||||||
|
|
||||||
|
| File | Change | Purpose |
|
||||||
|
|------|--------|---------|
|
||||||
|
| `database/migrations/20260514_000116_extend_order_notes_user_authored.sql` | Created | ADD COLUMN user_id + author_name + FK → users + indeks (idempotentne) |
|
||||||
|
| `src/Modules/Orders/OrderNotesService.php` | Created | CRUD service nad `order_notes` (`note_type='user'`) z autoryzacja po user_id |
|
||||||
|
| `resources/scss/modules/_order-notes.scss` | Created | `.order-notes-badge`, `.order-user-notes`, `.order-event--user`, `.btn-link`, `.order-note-form` |
|
||||||
|
| `public/assets/js/modules/order-notes.js` | Created | Vanilla JS: inline edit toggle + `OrderProAlerts.confirm` na delete (idempotent guard) |
|
||||||
|
| `src/Modules/Orders/OrdersController.php` | Modified | Dodano `OrderNotesService` jako nullable dep + `storeNote/updateNote/deleteNote` + badge w `toTableRow()` |
|
||||||
|
| `src/Modules/Orders/OrdersRepository.php` | Modified | `userNotesCountSubquerySql()` + kolumna `user_notes_count` w paginate; `loadOrderNotes()` zawezone do `note_type <> 'user'` |
|
||||||
|
| `routes/web.php` | Modified | 3 nowe routes (POST notes/store|update|delete) + `OrderNotesService` instancjonowany + przekazany do `OrdersController` |
|
||||||
|
| `resources/views/orders/show.php` | Modified | Sekcja `#notes` rozbita na user-notes + form + imported-notes; per-note edit form (ukryty) |
|
||||||
|
| `resources/views/layouts/app.php` | Modified | `<script>` dla `order-notes.js` |
|
||||||
|
| `resources/lang/pl.php` | Modified | 9 nowych kluczy `orders.details.notes_user_*` + `notes_imported_title` |
|
||||||
|
| `resources/scss/app.scss` | Modified | `@use "modules/order-notes"` |
|
||||||
|
| `public/assets/css/app.css` | Modified | Rebuilt by `npm run build:css` |
|
||||||
|
|
||||||
|
## Decisions Made
|
||||||
|
|
||||||
|
| Decision | Rationale | Impact |
|
||||||
|
|----------|-----------|--------|
|
||||||
|
| Reuse `order_notes` z `note_type='user'` | Jedna tabela = mniej obiektow DB, prosciej testowac, UNIQUE NULL nie koliduje | Importowane notatki ze zrodla nadal dzialaja niezmienione; w `loadOrderNotes()` dorzucony filtr `note_type <> 'user'` |
|
||||||
|
| Brak admin override (tylko autor edit/delete) | Brak systemu rol w aplikacji (`grep is_admin\|role=` → 0 hits) | Operator ktory dodal notatke moze ja modyfikowac; admin override odlozony do osobnej fazy gdy beda role |
|
||||||
|
| Badge `[N]` w `order_ref` (NIE osobna kolumna) | Minimalny footprint w tabeli, spojnie z `risk-return-badge` (przy `buyer_name`) | Badge widoczny bez zmian w naglowkach tabeli `/orders/list` |
|
||||||
|
| Body limit 2000 znakow (`mb_strlen`) | TEXT moze przechowac wiecej, ale UX podpowiada krotkie notatki; spojnie z polem comment | Walidacja w `OrderNotesService::sanitizeBody()` — rzut `InvalidArgumentException` gdy przekroczenie |
|
||||||
|
| Migracja idempotentna z DDL no-op fallback | Decyzja z Phase 115/125 — `SELECT 1` powoduje SQLSTATE 2014 przy PDO unbuffered | Re-run migracji = no-op (`ALTER TABLE COMMENT`) bez bledu |
|
||||||
|
| Edit toggle (ukryty form per notatka) zamiast modala | Mniej UI ceremoniaiu, spojnie z istniejacym `order-event` layoutem | JS prosty (show/hide pary `js-order-note-body` ↔ `js-order-note-edit-form`) |
|
||||||
|
|
||||||
|
## Deviations from Plan
|
||||||
|
|
||||||
|
### Summary
|
||||||
|
|
||||||
|
| Type | Count | Impact |
|
||||||
|
|------|-------|--------|
|
||||||
|
| Auto-fixed | 1 | Cosmetic naming |
|
||||||
|
| Scope additions | 0 | None |
|
||||||
|
| Deferred | 0 | None |
|
||||||
|
|
||||||
|
**Total impact:** Minimalne deviation — plan wykonany niemal 1:1.
|
||||||
|
|
||||||
|
### Auto-fixed Issues
|
||||||
|
|
||||||
|
**1. [Naming] `formatOrderRow` → `toTableRow`**
|
||||||
|
- **Found during:** Task 2 (badge w controllerze)
|
||||||
|
- **Issue:** Plan referowal do nieistniejacej metody `formatOrderRow()` w `OrdersController`
|
||||||
|
- **Fix:** Edycja `toTableRow()` (rzeczywista nazwa metody) — semantyka identyczna
|
||||||
|
- **Files:** `src/Modules/Orders/OrdersController.php`
|
||||||
|
- **Verification:** `grep -n "public function|toTableRow"` potwierdzilo `toTableRow` jako wlasciwa nazwa
|
||||||
|
- **Commit:** N/A (jeden Task 2 commit obejmie wszystkie zmiany)
|
||||||
|
|
||||||
|
### Deferred Items
|
||||||
|
|
||||||
|
None — plan executed exactly as written.
|
||||||
|
|
||||||
|
## Issues Encountered
|
||||||
|
|
||||||
|
| Issue | Resolution |
|
||||||
|
|-------|------------|
|
||||||
|
| Plan referowal `formatOrderRow()` (nieistniejacy) | Sprawdzono rzeczywiste metody przez `Grep "public function"` — `toTableRow` jest poprawna nazwa. Patch zaaplikowany. |
|
||||||
|
|
||||||
|
## Next Phase Readiness
|
||||||
|
|
||||||
|
**Ready:**
|
||||||
|
- Migracja czeka na operator (`php bin/migrate.php`).
|
||||||
|
- UI sekcji notatek + badge gotowe — manualny smoke test moze byc wykonany po migracji.
|
||||||
|
- Pattern `userNotesCountSubquerySql` + nullable `OrderNotesService` w `OrdersController` — gotowe do reuse w przyszlych phasach (np. event automatyzacji `note.created` lub admin override).
|
||||||
|
|
||||||
|
**Concerns:**
|
||||||
|
- Bez systemu rol nie ma admin override — jezeli operator chce zeby kazdy user mogl edytowac/usuwac kazda notatke, trzeba zmienic warunek w `OrderNotesService::update()/delete()` (usunac `AND user_id = :user_id`).
|
||||||
|
- Brak filtra "ma notatki" / "nie ma notatek" w liscie zamowien — kandydat na rozszerzenie jezeli operator zechce.
|
||||||
|
|
||||||
|
**Blockers:**
|
||||||
|
- None — plan wdrozony, smoke test po stronie operatora.
|
||||||
|
|
||||||
|
---
|
||||||
|
*Phase: 129-order-user-notes, Plan: 01*
|
||||||
|
*Completed: 2026-05-14*
|
||||||
@@ -0,0 +1,238 @@
|
|||||||
|
---
|
||||||
|
phase: 130-polkurier-delivery-status-mappings
|
||||||
|
plan: 01
|
||||||
|
type: execute
|
||||||
|
wave: 1
|
||||||
|
depends_on: []
|
||||||
|
files_modified:
|
||||||
|
- src/Modules/Shipments/DeliveryStatus.php
|
||||||
|
- src/Modules/Settings/DeliveryStatusesController.php
|
||||||
|
- src/Modules/Settings/DeliveryStatusMappingController.php
|
||||||
|
- src/Modules/Shipments/DeliveryStatusMappingRepository.php
|
||||||
|
autonomous: true
|
||||||
|
delegation: off
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
## Goal
|
||||||
|
Eksponuj `polkurier` w UI `/settings/delivery-statuses?tab=mapping`: dropdown providerów pokazuje pozycję "polkurier", 7 domyślnych mapowań (O/P/A/WP/D/Z/W) ładuje się z `DeliveryStatus::getDefaultMappings('polkurier')`, a badge "niezmapowane statusy" w menu zlicza również polkurier.
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
Phase 128 dodała `PolkurierShipmentService`/`PolkurierTrackingService` i seed migrację `delivery_status_mappings(provider='polkurier')`, ale UI mapowania pozostał hardcoded na 3 providerów (`inpost`/`apaczka`/`allegro_wza`). Operator nie ma jak zmapować/podejrzeć statusów polkuriera w panelu — kontrakt zamknięty od strony backendu, otwarty od strony UI. Bez tej fazy operator musi grzebać w SQL żeby zobaczyć/zmienić mapowania, co łamie wzorzec ustanowiony w Phase 108.
|
||||||
|
|
||||||
|
## Output
|
||||||
|
- `POLKURIER_MAP` + `POLKURIER_DESCRIPTIONS` w `DeliveryStatus.php` (7 wpisów) + rejestracja w `PROVIDER_MAPS`/`PROVIDER_DESCRIPTIONS`/`normalize()`/`description()` match.
|
||||||
|
- `'polkurier' => 'polkurier'` w `PROVIDERS` w obu kontrolerach (`DeliveryStatusesController`, `DeliveryStatusMappingController`).
|
||||||
|
- `'polkurier'` w pętli `countAllUnmappedForBadge()` w `DeliveryStatusMappingRepository`.
|
||||||
|
</objective>
|
||||||
|
|
||||||
|
<context>
|
||||||
|
<clarifications>
|
||||||
|
- **Źródło defaultów** — Skąd UI ma czerpać 7 domyślnych mapowań polkurier (O/P/A/WP/D/Z/W)?
|
||||||
|
→ Odpowiedź: Hardcoded w `DeliveryStatus.php` (POLKURIER_MAP + POLKURIER_DESCRIPTIONS, analogicznie do InPost/Apaczka/Allegro). DB seed migracji z Phase 128 nadal dostępny jako override.
|
||||||
|
- **Etykieta UI** — Jaką etykietę pokazać w dropdownie providerów na tabie Mapowanie?
|
||||||
|
→ Odpowiedź: `polkurier` (lowercase, spójne z hubem integracji Phase 127 i provider code w `shipment_packages.provider`).
|
||||||
|
- **Badge counter** — Czy badge 'niezmapowane statusy' w menu Ustawienia ma uwzględniać polkurier?
|
||||||
|
→ Odpowiedź: Tak — dodać `polkurier` do pętli `countAllUnmappedForBadge()`.
|
||||||
|
</clarifications>
|
||||||
|
|
||||||
|
## Project Context
|
||||||
|
@.paul/PROJECT.md
|
||||||
|
@.paul/ROADMAP.md
|
||||||
|
@.paul/STATE.md
|
||||||
|
@.paul/codebase/architecture.md
|
||||||
|
@.paul/codebase/db_schema.md
|
||||||
|
|
||||||
|
## Source Files
|
||||||
|
@src/Modules/Shipments/DeliveryStatus.php
|
||||||
|
@src/Modules/Settings/DeliveryStatusesController.php
|
||||||
|
@src/Modules/Settings/DeliveryStatusMappingController.php
|
||||||
|
@src/Modules/Shipments/DeliveryStatusMappingRepository.php
|
||||||
|
@resources/views/settings/delivery-statuses.php
|
||||||
|
|
||||||
|
## Prior Work
|
||||||
|
@.paul/phases/128-polkurier-shipment-service/128-01-SUMMARY.md
|
||||||
|
@.paul/phases/108-delivery-status-management/108-02-SUMMARY.md
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<acceptance_criteria>
|
||||||
|
|
||||||
|
## AC-1: Polkurier widoczny w dropdownie providerów
|
||||||
|
```gherkin
|
||||||
|
Given operator jest zalogowany i otwiera `/settings/delivery-statuses?tab=mapping`
|
||||||
|
When dropdown "Provider" jest rozwinięty
|
||||||
|
Then na liście widoczne są 4 pozycje: InPost, Apaczka, Allegro oraz polkurier
|
||||||
|
```
|
||||||
|
|
||||||
|
## AC-2: 7 domyślnych mapowań polkurier
|
||||||
|
```gherkin
|
||||||
|
Given operator wybiera "polkurier" w dropdownie providerów na tabie Mapowanie
|
||||||
|
When tabela mapowań się ładuje (bez uruchamiania migracji seed Phase 128)
|
||||||
|
Then widoczne jest dokładnie 7 wierszy z raw statusami: O, P, A, WP, D, Z, W
|
||||||
|
And każdy wiersz pokazuje znormalizowany status zgodny z dokumentacją polkurier v1.11
|
||||||
|
(O→created, P→confirmed, A→cancelled, WP→in_transit, D→delivered, Z→returned, W→problem)
|
||||||
|
And każdy wiersz pokazuje opis PL (np. "Oczekuje na płatność", "Dostarczona")
|
||||||
|
And wiersze NIE są oznaczone jako "custom" (is_custom=false) — to są defaulty z kodu
|
||||||
|
```
|
||||||
|
|
||||||
|
## AC-3: Badge "niezmapowane" zlicza polkurier
|
||||||
|
```gherkin
|
||||||
|
Given w `shipment_packages` istnieje wiersz z `provider='polkurier'` i `delivery_status_raw='X'`
|
||||||
|
And kod 'X' nie jest w domyślnych 7 ani w override'ach `delivery_status_mappings`
|
||||||
|
When sidebar Ustawień się renderuje (badge "niezmapowane")
|
||||||
|
Then licznik z `countAllUnmappedForBadge()` wzrasta o 1 z tytułu polkurier
|
||||||
|
```
|
||||||
|
|
||||||
|
## AC-4: Override DB nadpisuje hardcoded default
|
||||||
|
```gherkin
|
||||||
|
Given operator zapisuje override dla `provider='polkurier'`, `raw_status='D'` z `normalized_status='problem'`
|
||||||
|
When operator odświeża tab Mapowanie z `provider=polkurier`
|
||||||
|
Then wiersz "D" pokazuje normalized='problem' (z DB) zamiast 'delivered' (z kodu)
|
||||||
|
And wiersz jest oznaczony jako custom (is_custom=true)
|
||||||
|
```
|
||||||
|
|
||||||
|
## AC-5: Zero regresji dla istniejących providerów
|
||||||
|
```gherkin
|
||||||
|
Given operator otwiera `/settings/delivery-statuses?tab=mapping&provider=inpost`
|
||||||
|
When tabela się ładuje
|
||||||
|
Then liczba i treść wierszy InPost/Apaczka/Allegro pozostaje identyczna jak przed zmianami
|
||||||
|
And `DeliveryStatus::normalize('inpost', $raw)` zwraca te same wartości
|
||||||
|
```
|
||||||
|
|
||||||
|
</acceptance_criteria>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 1: Hardcoded POLKURIER_MAP + POLKURIER_DESCRIPTIONS w DeliveryStatus.php</name>
|
||||||
|
<files>src/Modules/Shipments/DeliveryStatus.php</files>
|
||||||
|
<action>
|
||||||
|
Dodaj dwie nowe stałe klasowe analogicznie do `INPOST_MAP`/`INPOST_DESCRIPTIONS`:
|
||||||
|
|
||||||
|
```php
|
||||||
|
private const POLKURIER_MAP = [
|
||||||
|
'O' => self::CREATED,
|
||||||
|
'P' => self::CONFIRMED,
|
||||||
|
'A' => self::CANCELLED,
|
||||||
|
'WP' => self::IN_TRANSIT,
|
||||||
|
'D' => self::DELIVERED,
|
||||||
|
'Z' => self::RETURNED,
|
||||||
|
'W' => self::PROBLEM,
|
||||||
|
];
|
||||||
|
|
||||||
|
private const POLKURIER_DESCRIPTIONS = [
|
||||||
|
'O' => 'Oczekuje na płatność',
|
||||||
|
'P' => 'Potwierdzone, list wygenerowany',
|
||||||
|
'A' => 'Anulowane',
|
||||||
|
'WP' => 'W przewozie',
|
||||||
|
'D' => 'Dostarczona',
|
||||||
|
'Z' => 'Zwrot do nadawcy',
|
||||||
|
'W' => 'Wyjątek',
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
Następnie zarejestruj `'polkurier'` w trzech miejscach:
|
||||||
|
1. `PROVIDER_MAPS` (po `'allegro_wza'`) — `'polkurier' => self::POLKURIER_MAP,`
|
||||||
|
2. `PROVIDER_DESCRIPTIONS` (po `'allegro_wza'`) — `'polkurier' => self::POLKURIER_DESCRIPTIONS,`
|
||||||
|
3. `normalize()` match expression — dodaj `'polkurier' => self::POLKURIER_MAP,`
|
||||||
|
4. `description()` match expression — dodaj `'polkurier' => self::POLKURIER_DESCRIPTIONS,`
|
||||||
|
|
||||||
|
Treść 7 wpisów MUSI być identyczna z migracją Phase 128
|
||||||
|
(`database/migrations/20260514_000115_seed_polkurier_delivery_status_mappings.sql`).
|
||||||
|
To gwarantuje że jeśli operator odpali seed migrację po wdrożeniu, nie zmieni się żadne mapowanie
|
||||||
|
(default == DB override → `is_custom=true` ale ta sama wartość).
|
||||||
|
|
||||||
|
Avoid: zmiana kolejności/struktury INPOST/APACZKA/ALLEGRO_WZA — to złamałoby AC-5.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
php -r "require 'vendor/autoload.php'; var_export(\App\Modules\Shipments\DeliveryStatus::getDefaultMappings('polkurier'));"
|
||||||
|
# Oczekiwane: array z 7 kluczami (O/P/A/WP/D/Z/W), każdy z 'normalized' i 'description'.
|
||||||
|
</verify>
|
||||||
|
<done>AC-2, AC-5 satisfied: 7 defaultów polkurier z poprawnym normalized+description; existing providers nietknięte.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 2: Dodaj 'polkurier' do PROVIDERS w obu kontrolerach + badge counter</name>
|
||||||
|
<files>src/Modules/Settings/DeliveryStatusesController.php, src/Modules/Settings/DeliveryStatusMappingController.php, src/Modules/Shipments/DeliveryStatusMappingRepository.php</files>
|
||||||
|
<action>
|
||||||
|
1. `DeliveryStatusesController.php` (linie 22-26): dodaj `'polkurier' => 'polkurier',` jako 4. wpis w stałej `PROVIDERS`. Zachowaj kolejność: inpost, apaczka, allegro_wza, polkurier.
|
||||||
|
|
||||||
|
2. `DeliveryStatusMappingController.php` (linie 22-26): identyczna zmiana w analogicznej stałej `PROVIDERS`.
|
||||||
|
|
||||||
|
3. `DeliveryStatusMappingRepository.php` linia 158 — zmień:
|
||||||
|
```php
|
||||||
|
$providers = ['inpost', 'apaczka', 'allegro_wza'];
|
||||||
|
```
|
||||||
|
na:
|
||||||
|
```php
|
||||||
|
$providers = ['inpost', 'apaczka', 'allegro_wza', 'polkurier'];
|
||||||
|
```
|
||||||
|
|
||||||
|
Po tej zmianie `index()` w obu kontrolerach automatycznie zaakceptuje `?provider=polkurier`
|
||||||
|
(sprawdza `isset(self::PROVIDERS[$provider])`), pobierze defaulty z `DeliveryStatus::getDefaultMappings('polkurier')`
|
||||||
|
(Task 1), i scali z override'ami z `DeliveryStatusMappingRepository::listByProvider('polkurier')`.
|
||||||
|
|
||||||
|
Widok `resources/views/settings/delivery-statuses.php` iteruje po `$providers` (dropdown)
|
||||||
|
i nie wymaga zmian — automatycznie pokaże nową pozycję.
|
||||||
|
|
||||||
|
Avoid: dodanie polkurier w innej pozycji niż na końcu tablicy — może to zmienić default
|
||||||
|
(`$provider = 'inpost'` w fallback jest niezależny i bezpieczny, ale kolejność wpływa na render dropdownu).
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
# 1. Sprawdź dropdown:
|
||||||
|
curl -s -b "session.cookie" https://orderpro.projectpro.pl/settings/delivery-statuses?tab=mapping | grep -c 'value="polkurier"'
|
||||||
|
# Oczekiwane: 1
|
||||||
|
|
||||||
|
# 2. Sprawdź że provider=polkurier renderuje 7 wierszy:
|
||||||
|
curl -s -b "session.cookie" 'https://orderpro.projectpro.pl/settings/delivery-statuses?tab=mapping&provider=polkurier' | grep -E 'raw_status.*(O|P|A|WP|D|Z|W)' | wc -l
|
||||||
|
# Oczekiwane: 7
|
||||||
|
|
||||||
|
# 3. Smoke regresji — InPost dalej działa:
|
||||||
|
curl -s -b "session.cookie" 'https://orderpro.projectpro.pl/settings/delivery-statuses?tab=mapping&provider=inpost' | grep -c 'value="inpost"'
|
||||||
|
# Oczekiwane: 1 (selected)
|
||||||
|
</verify>
|
||||||
|
<done>AC-1, AC-3, AC-4, AC-5 satisfied: dropdown pokazuje polkurier, 7 defaultów się renderuje, badge zlicza polkurier, override DB nadpisuje default, istniejące providery bez regresji.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<boundaries>
|
||||||
|
|
||||||
|
## DO NOT CHANGE
|
||||||
|
- `database/migrations/20260514_000115_seed_polkurier_delivery_status_mappings.sql` — migracja Phase 128 zostaje as-is; ten plan dubluje jej treść w kodzie ale NIE zmienia samej migracji (operator może ją odpalić lub nie — funkcjonalność niezależna).
|
||||||
|
- Stałe `INPOST_MAP`/`APACZKA_MAP`/`ALLEGRO_MAP`/`ALLEGRO_EDGE_MAP` w `DeliveryStatus.php` — żadnych edycji wartości lub kolejności.
|
||||||
|
- `PolkurierTrackingService` — kontrakt mapowania Phase 128 zostaje nietknięty; ten plan nie zmienia logiki normalizacji w runtime, tylko ekspozycję defaultów w UI.
|
||||||
|
- Schemat tabeli `delivery_status_mappings` — brak migracji w tym planie.
|
||||||
|
- `DeliveryStatus::trackingUrl()` (zawiera już branch `polkurier` z Phase 128) — nietknięte.
|
||||||
|
|
||||||
|
## SCOPE LIMITS
|
||||||
|
- Brak dodatkowych mapowań polkurier (np. nieudokumentowanych w v1.11) — tylko 7 oficjalnych kodów z dokumentacji.
|
||||||
|
- Brak osobnej zakładki/podstrony dla polkurier — reuse istniejącego tab `mapping` z dropdownem.
|
||||||
|
- Brak zmian w `PROJECT.md`/`ROADMAP.md` — to robi UNIFY.
|
||||||
|
- Brak migracji DB — defaulty z kodu, override z DB jak dla pozostałych providerów.
|
||||||
|
- Brak zmian w widoku `delivery-statuses.php` — dropdown iteruje po `$providers` z controllera.
|
||||||
|
|
||||||
|
</boundaries>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
Przed declared complete:
|
||||||
|
- [ ] `DeliveryStatus::getDefaultMappings('polkurier')` zwraca 7 wpisów z poprawnymi normalized+description (AC-2).
|
||||||
|
- [ ] Dropdown providerów w `/settings/delivery-statuses?tab=mapping` pokazuje 4 pozycje w kolejności InPost, Apaczka, Allegro, polkurier (AC-1).
|
||||||
|
- [ ] Selekcja `?provider=polkurier` ładuje 7 wierszy mapowań bez fatal errora (AC-2).
|
||||||
|
- [ ] Override DB (manual INSERT do `delivery_status_mappings` lub przez UI) zmienia `is_custom=true` dla wiersza (AC-4).
|
||||||
|
- [ ] `countAllUnmappedForBadge()` dla wstrzykniętego raw statusu `polkurier:XYZ` zwraca +1 (AC-3).
|
||||||
|
- [ ] Smoke regresji: `?provider=inpost`/`apaczka`/`allegro_wza` zwracają identyczną liczbę wierszy jak przed zmianą (AC-5).
|
||||||
|
- [ ] `php -l` przechodzi dla wszystkich 4 zmienionych plików.
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- 4 pliki zmodyfikowane (3 controllery + repo + DeliveryStatus.php — łącznie 4 fizyczne pliki, 5 punktów edycji).
|
||||||
|
- AC-1..AC-5 zweryfikowane.
|
||||||
|
- Brak zmian schematu DB.
|
||||||
|
- Phase 128 seed migration nie wymaga modyfikacji — pozostaje no-op (po Task 1 defaulty = wartości w migracji).
|
||||||
|
- Manual smoke na `/settings/delivery-statuses?tab=mapping&provider=polkurier` po deploy.
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create `.paul/phases/130-polkurier-delivery-status-mappings/130-01-SUMMARY.md`
|
||||||
|
</output>
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
---
|
||||||
|
phase: 130-polkurier-delivery-status-mappings
|
||||||
|
plan: 01
|
||||||
|
subsystem: ui
|
||||||
|
tags: [delivery-statuses, polkurier, mapping, settings]
|
||||||
|
|
||||||
|
requires:
|
||||||
|
- phase: 128-polkurier-shipment-service
|
||||||
|
provides: PolkurierTrackingService + delivery_status_mappings seed migration (DB-side override)
|
||||||
|
- phase: 108-delivery-status-management
|
||||||
|
provides: DeliveryStatus::PROVIDER_MAPS pattern + DeliveryStatusMappingController + view _delivery-status-mappings-content.php
|
||||||
|
|
||||||
|
provides:
|
||||||
|
- polkurier visible in Provider dropdown on /settings/delivery-statuses?tab=mapping
|
||||||
|
- 7 hardcoded default mappings for polkurier (O/P/A/WP/D/Z/W) in DeliveryStatus.php
|
||||||
|
- polkurier counted in countAllUnmappedForBadge() so menu badge reacts to unknown polkurier raw statuses
|
||||||
|
|
||||||
|
affects:
|
||||||
|
- future polkurier UI work (paczkomaty selector, presety przesylek)
|
||||||
|
- any future delivery provider additions (recipe established: 5 edit points)
|
||||||
|
|
||||||
|
tech-stack:
|
||||||
|
added: []
|
||||||
|
patterns:
|
||||||
|
- "Provider addition recipe: 1 const + 1 PROVIDER_MAPS + 1 PROVIDER_DESCRIPTIONS + 2 match arms + 2 PROVIDERS controller consts + 1 badge providers list = 5 edit points across 4 files"
|
||||||
|
|
||||||
|
key-files:
|
||||||
|
modified:
|
||||||
|
- src/Modules/Shipments/DeliveryStatus.php
|
||||||
|
- src/Modules/Settings/DeliveryStatusesController.php
|
||||||
|
- src/Modules/Settings/DeliveryStatusMappingController.php
|
||||||
|
- src/Modules/Shipments/DeliveryStatusMappingRepository.php
|
||||||
|
|
||||||
|
key-decisions:
|
||||||
|
- "Defaultowe mapowania polkurier hardcoded w DeliveryStatus.php (spojnie z InPost/Apaczka/Allegro)"
|
||||||
|
- "Etykieta dropdownu = 'polkurier' (lowercase, spojne z Phase 127 hub integracji)"
|
||||||
|
- "Badge counter uwzglednia polkurier (caly framework, nie wybiorczo)"
|
||||||
|
|
||||||
|
patterns-established:
|
||||||
|
- "Provider-addition checklist: trzy hardcoded providers (PROVIDER_MAPS/PROVIDER_DESCRIPTIONS + 2× normalize/description match) + dwa hardcoded controllery (PROVIDERS const) + jeden repo (badge providers list)"
|
||||||
|
|
||||||
|
duration: ~15min
|
||||||
|
started: 2026-05-14T18:00:00Z
|
||||||
|
completed: 2026-05-14T18:15:00Z
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 130 Plan 01: polkurier delivery status mappings UI Summary
|
||||||
|
|
||||||
|
**polkurier widoczny w dropdownie `/settings/delivery-statuses?tab=mapping`, 7 oficjalnych kodow ORDER_STATUS (O/P/A/WP/D/Z/W) z dokumentacji v1.11 hardcoded jako defaults; badge "niezmapowane" w menu zlicza polkurier obok inpost/apaczka/allegro_wza.**
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
| Metric | Value |
|
||||||
|
|--------|-------|
|
||||||
|
| Duration | ~15 min |
|
||||||
|
| Started | 2026-05-14T18:00:00Z |
|
||||||
|
| Completed | 2026-05-14T18:15:00Z |
|
||||||
|
| Tasks | 2/2 completed |
|
||||||
|
| Files modified | 4 source files |
|
||||||
|
|
||||||
|
## Acceptance Criteria Results
|
||||||
|
|
||||||
|
| Criterion | Status | Notes |
|
||||||
|
|-----------|--------|-------|
|
||||||
|
| AC-1: polkurier widoczny w dropdownie providerów | Pass | `PROVIDERS` w `DeliveryStatusesController` + `DeliveryStatusMappingController` zawiera 4 wpisy; widok `_delivery-status-mappings-content.php` iteruje po `$providersList` z controllera |
|
||||||
|
| AC-2: 7 domyślnych mapowań polkurier | Pass | Live test: `DeliveryStatus::getDefaultMappings('polkurier')` zwrócił 7 wpisów (O→created, P→confirmed, A→cancelled, WP→in_transit, D→delivered, Z→returned, W→problem) z poprawnymi opisami PL |
|
||||||
|
| AC-3: Badge "niezmapowane" zlicza polkurier | Pass | `DeliveryStatusMappingRepository::countAllUnmappedForBadge()` zmienił listę z `['inpost','apaczka','allegro_wza']` na `[..., 'polkurier']` |
|
||||||
|
| AC-4: Override DB nadpisuje hardcoded default | Pass | Logika `index()` w obu kontrolerach (niezmieniona) iteruje po `defaults` i nadpisuje `$overrideMap[$rawStatus]` z `delivery_status_mappings` — pattern identyczny jak dla inpost/apaczka/allegro_wza |
|
||||||
|
| AC-5: Zero regresji dla istniejących providerów | Pass | `INPOST_MAP`/`APACZKA_MAP`/`ALLEGRO_MAP`/`ALLEGRO_EDGE_MAP` nietknięte; `PROVIDER_MAPS`/`PROVIDER_DESCRIPTIONS` zachowują kolejność; `normalize()`/`description()` match dostały tylko jedną nową gałąź `polkurier` |
|
||||||
|
|
||||||
|
## Accomplishments
|
||||||
|
|
||||||
|
- Hardcoded `POLKURIER_MAP` + `POLKURIER_DESCRIPTIONS` w `DeliveryStatus.php` — 7 wpisów z oficjalnej dokumentacji polkurier API v1.11 (marzec 2026), zgodne wartości z migracją Phase 128 (`20260514_000115_seed_polkurier_delivery_status_mappings.sql`)
|
||||||
|
- 5 punktów edycji w 4 plikach (1 const definition + 2 PROVIDER_* + 2 match arms + 2× PROVIDERS controller + 1 badge providers list)
|
||||||
|
- Brak regresji: defaulty inpost/apaczka/allegro pozostały bit-for-bit identyczne; zero zmian w schemacie DB; zero zmian w widoku (dropdown auto-iteruje po providerach z controllera)
|
||||||
|
|
||||||
|
## Task Commits
|
||||||
|
|
||||||
|
Atomic per-task commit nie wykonany w trakcie APPLY — wszystkie 4 pliki źródłowe zostaną zacommitowane jako jeden commit fazowy `feat(130): polkurier delivery status mappings UI` w kroku transition (zgodnie z konwencją poprzednich faz v3.7).
|
||||||
|
|
||||||
|
## Files Created/Modified
|
||||||
|
|
||||||
|
| File | Change | Purpose |
|
||||||
|
|------|--------|---------|
|
||||||
|
| `src/Modules/Shipments/DeliveryStatus.php` | Modified (+25 linii) | Dodano `POLKURIER_MAP` (7 wpisów) + `POLKURIER_DESCRIPTIONS` (7 opisów PL) + rejestracja w `PROVIDER_MAPS`, `PROVIDER_DESCRIPTIONS`, oraz w match expressions `normalize()` / `description()` |
|
||||||
|
| `src/Modules/Settings/DeliveryStatusesController.php` | Modified (+1) | Dodano `'polkurier' => 'polkurier'` do stałej `PROVIDERS` (4 wpis) |
|
||||||
|
| `src/Modules/Settings/DeliveryStatusMappingController.php` | Modified (+1) | Identyczna zmiana w analogicznej stałej `PROVIDERS` |
|
||||||
|
| `src/Modules/Shipments/DeliveryStatusMappingRepository.php` | Modified (1 ↔) | `countAllUnmappedForBadge()`: lista providerów rozszerzona o `polkurier` |
|
||||||
|
|
||||||
|
## Decisions Made
|
||||||
|
|
||||||
|
| Decision | Rationale | Impact |
|
||||||
|
|----------|-----------|--------|
|
||||||
|
| POLKURIER_MAP/DESCRIPTIONS hardcoded w DeliveryStatus.php zamiast tylko z DB seed | Spójność z inpost/apaczka/allegro_wza — wszyscy mają hardcoded defaults i opcjonalne DB overrides. UI tab `polkurier` działa od razu, niezależnie od tego czy operator uruchomił migrację Phase 128. | Migracja `20260514_000115_seed_polkurier_delivery_status_mappings.sql` z Phase 128 staje się no-op (DB override == default → render `is_custom=true` ale ta sama wartość). Można ją uruchomić lub nie. |
|
||||||
|
| Etykieta dropdownu = `polkurier` (lowercase) | Spójność z provider code w `shipment_packages.provider`, z hubem integracji Phase 127, z PROJECT.md decisions. | Następne integracje powinny używać tej samej konwencji (lowercase brand name). |
|
||||||
|
| Badge counter dodaje `polkurier` | Cały framework "niezmapowane raw statusy" powinien działać jednolicie dla wszystkich providerów obecnych w UI mapowania. | Operator zobaczy w badge'u nowy raw status polkuriera (gdyby pojawił się jakiś kod spoza udokumentowanych 7) — tak samo jak dla innych przewoźników. |
|
||||||
|
|
||||||
|
## Deviations from Plan
|
||||||
|
|
||||||
|
### Summary
|
||||||
|
|
||||||
|
| Type | Count | Impact |
|
||||||
|
|------|-------|--------|
|
||||||
|
| Auto-fixed | 0 | — |
|
||||||
|
| Scope additions | 0 | — |
|
||||||
|
| Deferred | 0 | — |
|
||||||
|
|
||||||
|
**Total impact:** Brak deviacji — plan wykonany 1:1.
|
||||||
|
|
||||||
|
### Deferred Items
|
||||||
|
|
||||||
|
Brak — plan wykonany dokładnie jak napisany.
|
||||||
|
|
||||||
|
## Issues Encountered
|
||||||
|
|
||||||
|
Brak — wszystkie 5 edycji zaaplikowane czysto, PHP lint przeszedł na 4 plikach, runtime test `getDefaultMappings('polkurier')` zwrócił oczekiwane 7 wpisów.
|
||||||
|
|
||||||
|
## Next Phase Readiness
|
||||||
|
|
||||||
|
**Ready:**
|
||||||
|
- Mapowanie polkurier w pełni widoczne w UI dla operatora — może podejrzeć i nadpisać każdy z 7 statusów.
|
||||||
|
- Badge "niezmapowane" zareaguje gdy polkurier zwróci nieudokumentowany raw status.
|
||||||
|
- Provider-addition recipe utrwalony — następny przewoźnik dodawany w 5 punktach edycji (4 pliki).
|
||||||
|
|
||||||
|
**Concerns:**
|
||||||
|
- Migracja Phase 128 (`20260514_000115_seed_polkurier_delivery_status_mappings.sql`) staje się no-op po wdrożeniu — może ją zostawić jako historyczny ślad albo (opcjonalnie, deferred do osobnej fazy cleanup) zamienić na `ALTER TABLE COMMENT` no-op. Nie blokuje niczego.
|
||||||
|
- Brak manualnego smoke na żywej bazie — operator musi otworzyć `/settings/delivery-statuses?tab=mapping&provider=polkurier` po deploy.
|
||||||
|
|
||||||
|
**Blockers:**
|
||||||
|
- None.
|
||||||
|
|
||||||
|
---
|
||||||
|
*Phase: 130-polkurier-delivery-status-mappings, Plan: 01*
|
||||||
|
*Completed: 2026-05-14*
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS `polkurier_integration_settings` (
|
||||||
|
`id` TINYINT UNSIGNED NOT NULL PRIMARY KEY,
|
||||||
|
`integration_id` INT UNSIGNED NULL,
|
||||||
|
`login` VARCHAR(190) NULL,
|
||||||
|
`api_token_encrypted` TEXT NULL,
|
||||||
|
`default_label_format` VARCHAR(8) NOT NULL DEFAULT 'PDF',
|
||||||
|
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE KEY `polkurier_integration_settings_integration_unique` (`integration_id`),
|
||||||
|
CONSTRAINT `polkurier_integration_settings_integration_fk`
|
||||||
|
FOREIGN KEY (`integration_id`) REFERENCES `integrations` (`id`)
|
||||||
|
ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
INSERT INTO `integrations` (`type`, `name`, `base_url`, `timeout_seconds`, `is_active`, `created_at`, `updated_at`)
|
||||||
|
VALUES ('polkurier', 'polkurier', 'https://api.polkurier.pl/', 15, 1, NOW(), NOW())
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
`base_url` = VALUES(`base_url`),
|
||||||
|
`timeout_seconds` = VALUES(`timeout_seconds`),
|
||||||
|
`updated_at` = VALUES(`updated_at`);
|
||||||
|
|
||||||
|
INSERT INTO `polkurier_integration_settings` (`id`, `integration_id`, `created_at`, `updated_at`)
|
||||||
|
SELECT 1, `id`, NOW(), NOW()
|
||||||
|
FROM `integrations`
|
||||||
|
WHERE `type` = 'polkurier' AND `name` = 'polkurier'
|
||||||
|
LIMIT 1
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
`integration_id` = VALUES(`integration_id`),
|
||||||
|
`updated_at` = VALUES(`updated_at`);
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
-- Phase 128-01: seed delivery_status_mappings dla provider='polkurier'.
|
||||||
|
-- Kody zaczerpniete z oficjalnej dokumentacji API polkurier v1.11 (marzec 2026),
|
||||||
|
-- tabela "Status zlecenia ORDER_STATUS":
|
||||||
|
-- O = Oczekuje (zlecenie zapisane, oczekuje na platnosc)
|
||||||
|
-- P = Potwierdzone (zlecenie zapisane, list przewozowy wygenerowany, oczekuje odbioru)
|
||||||
|
-- A = Anulowane (przesylka anulowana)
|
||||||
|
-- WP = W przewozie (odebrana od nadawcy, w drodze do adresata)
|
||||||
|
-- D = Dostarczona (dotarla do adresata)
|
||||||
|
-- Z = Zwrot do nadawcy (odmowa odbioru przez odbiorce)
|
||||||
|
-- W = Wyjatek (problem z doreczeniem)
|
||||||
|
-- Status PZ (Podjazd zbiorczy) jest filtrem wyszukiwania get_orders, nie wystepuje jako
|
||||||
|
-- samodzielny status zlecenia -- pomijamy w seedzie.
|
||||||
|
--
|
||||||
|
-- Migracja idempotentna: ON DUPLICATE KEY UPDATE nadpisuje normalized_status i description.
|
||||||
|
|
||||||
|
INSERT INTO delivery_status_mappings (provider, raw_status, normalized_status, description, created_at, updated_at) VALUES
|
||||||
|
('polkurier', 'O', 'created', 'Oczekuje (oczekuje na platnosc)', NOW(), NOW()),
|
||||||
|
('polkurier', 'P', 'confirmed', 'Potwierdzone (list przewozowy wygenerowany)', NOW(), NOW()),
|
||||||
|
('polkurier', 'A', 'cancelled', 'Anulowane', NOW(), NOW()),
|
||||||
|
('polkurier', 'WP', 'in_transit','W przewozie', NOW(), NOW()),
|
||||||
|
('polkurier', 'D', 'delivered', 'Dostarczona', NOW(), NOW()),
|
||||||
|
('polkurier', 'Z', 'returned', 'Zwrot do nadawcy', NOW(), NOW()),
|
||||||
|
('polkurier', 'W', 'problem', 'Wyjatek (problem z doreczeniem)', NOW(), NOW())
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
normalized_status = VALUES(normalized_status),
|
||||||
|
description = VALUES(description),
|
||||||
|
updated_at = NOW();
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
-- Phase 129-01: extend order_notes o pola dla notatek autorskich (user-authored)
|
||||||
|
-- Reuse istniejacej tabeli z nowym note_type='user', user_id (FK->users), author_name (snapshot).
|
||||||
|
-- Idempotentna: guard przez information_schema; no-op po pierwszym uruchomieniu.
|
||||||
|
-- Pattern z Key Decision 2026-05-10: migracje no-op zawsze jako DDL (ALTER TABLE COMMENT),
|
||||||
|
-- nigdy SELECT 1 (PDO unbuffered + result set -> SQLSTATE 2014).
|
||||||
|
|
||||||
|
-- 1) ADD COLUMN user_id
|
||||||
|
SET @col_user_id := (
|
||||||
|
SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'order_notes'
|
||||||
|
AND COLUMN_NAME = 'user_id'
|
||||||
|
);
|
||||||
|
SET @sql_user_id := IF(@col_user_id = 0,
|
||||||
|
'ALTER TABLE order_notes ADD COLUMN user_id INT UNSIGNED NULL AFTER note_type',
|
||||||
|
'ALTER TABLE order_notes COMMENT = ''phase-129 user_id no-op'''
|
||||||
|
);
|
||||||
|
PREPARE s1 FROM @sql_user_id; EXECUTE s1; DEALLOCATE PREPARE s1;
|
||||||
|
|
||||||
|
-- 2) ADD COLUMN author_name
|
||||||
|
SET @col_author_name := (
|
||||||
|
SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'order_notes'
|
||||||
|
AND COLUMN_NAME = 'author_name'
|
||||||
|
);
|
||||||
|
SET @sql_author_name := IF(@col_author_name = 0,
|
||||||
|
'ALTER TABLE order_notes ADD COLUMN author_name VARCHAR(190) NULL AFTER user_id',
|
||||||
|
'ALTER TABLE order_notes COMMENT = ''phase-129 author_name no-op'''
|
||||||
|
);
|
||||||
|
PREPARE s2 FROM @sql_author_name; EXECUTE s2; DEALLOCATE PREPARE s2;
|
||||||
|
|
||||||
|
-- 3) ADD FOREIGN KEY user_id -> users(id) ON DELETE SET NULL
|
||||||
|
SET @fk_exists := (
|
||||||
|
SELECT COUNT(*) FROM information_schema.TABLE_CONSTRAINTS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'order_notes'
|
||||||
|
AND CONSTRAINT_NAME = 'order_notes_user_fk'
|
||||||
|
);
|
||||||
|
SET @sql_fk := IF(@fk_exists = 0,
|
||||||
|
'ALTER TABLE order_notes ADD CONSTRAINT order_notes_user_fk FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE',
|
||||||
|
'ALTER TABLE order_notes COMMENT = ''phase-129 fk no-op'''
|
||||||
|
);
|
||||||
|
PREPARE s3 FROM @sql_fk; EXECUTE s3; DEALLOCATE PREPARE s3;
|
||||||
|
|
||||||
|
-- 4) ADD INDEX (note_type, order_id) — wspiera subquery user_notes_count i listUserNotes
|
||||||
|
SET @idx_exists := (
|
||||||
|
SELECT COUNT(*) FROM information_schema.STATISTICS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'order_notes'
|
||||||
|
AND INDEX_NAME = 'idx_order_notes_type_order'
|
||||||
|
);
|
||||||
|
SET @sql_idx := IF(@idx_exists = 0,
|
||||||
|
'ALTER TABLE order_notes ADD INDEX idx_order_notes_type_order (note_type, order_id)',
|
||||||
|
'ALTER TABLE order_notes COMMENT = ''phase-129 idx no-op'''
|
||||||
|
);
|
||||||
|
PREPARE s4 FROM @sql_idx; EXECUTE s4; DEALLOCATE PREPARE s4;
|
||||||
File diff suppressed because one or more lines are too long
89
public/assets/js/modules/order-notes.js
Normal file
89
public/assets/js/modules/order-notes.js
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
if (window.__orderNotesInit) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.__orderNotesInit = true;
|
||||||
|
|
||||||
|
function bindEdit(button) {
|
||||||
|
if (button.dataset.bound === '1') return;
|
||||||
|
button.dataset.bound = '1';
|
||||||
|
|
||||||
|
button.addEventListener('click', function () {
|
||||||
|
var noteEl = button.closest('[data-note-id]');
|
||||||
|
if (!noteEl) return;
|
||||||
|
var body = noteEl.querySelector('.js-order-note-body');
|
||||||
|
var form = noteEl.querySelector('.js-order-note-edit-form');
|
||||||
|
if (!body || !form) return;
|
||||||
|
body.style.display = 'none';
|
||||||
|
form.style.display = '';
|
||||||
|
var textarea = form.querySelector('textarea[name="body"]');
|
||||||
|
if (textarea) {
|
||||||
|
textarea.focus();
|
||||||
|
var len = textarea.value.length;
|
||||||
|
try { textarea.setSelectionRange(len, len); } catch (e) { /* ignore */ }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindCancel(button) {
|
||||||
|
if (button.dataset.bound === '1') return;
|
||||||
|
button.dataset.bound = '1';
|
||||||
|
|
||||||
|
button.addEventListener('click', function () {
|
||||||
|
var noteEl = button.closest('[data-note-id]');
|
||||||
|
if (!noteEl) return;
|
||||||
|
var body = noteEl.querySelector('.js-order-note-body');
|
||||||
|
var form = noteEl.querySelector('.js-order-note-edit-form');
|
||||||
|
if (!body || !form) return;
|
||||||
|
form.style.display = 'none';
|
||||||
|
body.style.display = '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindDelete(form) {
|
||||||
|
if (form.dataset.bound === '1') return;
|
||||||
|
form.dataset.bound = '1';
|
||||||
|
|
||||||
|
form.addEventListener('submit', function (event) {
|
||||||
|
event.preventDefault();
|
||||||
|
if (window.OrderProAlerts && typeof window.OrderProAlerts.confirm === 'function') {
|
||||||
|
window.OrderProAlerts.confirm({
|
||||||
|
title: 'Usunac notatke?',
|
||||||
|
message: 'Tej operacji nie mozna cofnac.',
|
||||||
|
danger: true,
|
||||||
|
confirmLabel: 'Usun',
|
||||||
|
onConfirm: function () {
|
||||||
|
form.dataset.bound = '2';
|
||||||
|
form.submit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
form.dataset.bound = '2';
|
||||||
|
form.submit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
var editButtons = document.querySelectorAll('.js-order-note-edit');
|
||||||
|
for (var i = 0; i < editButtons.length; i++) {
|
||||||
|
bindEdit(editButtons[i]);
|
||||||
|
}
|
||||||
|
var cancelButtons = document.querySelectorAll('.js-order-note-edit-cancel');
|
||||||
|
for (var j = 0; j < cancelButtons.length; j++) {
|
||||||
|
bindCancel(cancelButtons[j]);
|
||||||
|
}
|
||||||
|
var deleteForms = document.querySelectorAll('.js-order-note-delete');
|
||||||
|
for (var k = 0; k < deleteForms.length; k++) {
|
||||||
|
bindDelete(deleteForms[k]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', init);
|
||||||
|
} else {
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
})();
|
||||||
@@ -190,6 +190,15 @@ return [
|
|||||||
'address_invoice' => 'Dane do faktury',
|
'address_invoice' => 'Dane do faktury',
|
||||||
'address_delivery' => 'Dane wysylki',
|
'address_delivery' => 'Dane wysylki',
|
||||||
'notes_title' => 'Wiadomosci i zalaczniki',
|
'notes_title' => 'Wiadomosci i zalaczniki',
|
||||||
|
'notes_user_title' => 'Notatki',
|
||||||
|
'notes_user_empty' => 'Brak notatek.',
|
||||||
|
'notes_user_add_placeholder' => 'Wpisz notatke...',
|
||||||
|
'notes_user_save' => 'Zapisz',
|
||||||
|
'notes_user_edit' => 'Edytuj',
|
||||||
|
'notes_user_delete' => 'Usun',
|
||||||
|
'notes_user_cancel' => 'Anuluj',
|
||||||
|
'notes_user_confirm_delete' => 'Usunac notatke?',
|
||||||
|
'notes_imported_title' => 'Wiadomosci ze zrodla',
|
||||||
'history_title' => 'Historia statusow',
|
'history_title' => 'Historia statusow',
|
||||||
'fields' => [
|
'fields' => [
|
||||||
'status' => 'Status',
|
'status' => 'Status',
|
||||||
@@ -587,6 +596,7 @@ return [
|
|||||||
'hostedsms' => 'HostedSMS',
|
'hostedsms' => 'HostedSMS',
|
||||||
'smsplanet' => 'SMSPLANET',
|
'smsplanet' => 'SMSPLANET',
|
||||||
'erli' => 'Erli',
|
'erli' => 'Erli',
|
||||||
|
'polkurier' => 'polkurier.pl',
|
||||||
'shoppro_instances' => ':count instancji',
|
'shoppro_instances' => ':count instancji',
|
||||||
],
|
],
|
||||||
'status' => [
|
'status' => [
|
||||||
@@ -742,6 +752,47 @@ return [
|
|||||||
'test_failed' => 'Nie udalo sie polaczyc z API Apaczka.',
|
'test_failed' => 'Nie udalo sie polaczyc z API Apaczka.',
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
'polkurier' => [
|
||||||
|
'title' => 'Integracja polkurier.pl',
|
||||||
|
'description' => 'Broker kurierski polkurier.pl - alternatywa dla Apaczki. Token API generujesz w Panel Klienta > Ustawienia > Token API.',
|
||||||
|
'config' => [
|
||||||
|
'title' => 'Konfiguracja API',
|
||||||
|
],
|
||||||
|
'test' => [
|
||||||
|
'title' => 'Test polaczenia',
|
||||||
|
'description' => 'Test realnie wywoluje metode test_auth_api w API polkurier (bez tworzenia przesylki).',
|
||||||
|
],
|
||||||
|
'fields' => [
|
||||||
|
'login' => 'Login (e-mail z panelu klienta)',
|
||||||
|
'api_token' => 'Token API',
|
||||||
|
'default_label_format' => 'Domyslny format etykiety',
|
||||||
|
'is_active' => 'Integracja aktywna',
|
||||||
|
],
|
||||||
|
'hints' => [
|
||||||
|
'login' => 'Login uzywany przy generowaniu Token API w Panel Klienta polkurier.',
|
||||||
|
],
|
||||||
|
'token' => [
|
||||||
|
'saved' => 'Token API jest zapisany. Pozostaw pole puste, aby nie zmieniac.',
|
||||||
|
'missing' => 'Brak zapisanego Token API.',
|
||||||
|
],
|
||||||
|
'status' => [
|
||||||
|
'token' => 'Token API',
|
||||||
|
'active' => 'Aktywna',
|
||||||
|
'saved' => 'zapisany',
|
||||||
|
'missing' => 'brak',
|
||||||
|
'last_test' => 'Ostatni test',
|
||||||
|
],
|
||||||
|
'actions' => [
|
||||||
|
'save' => 'Zapisz ustawienia polkurier',
|
||||||
|
'send_test' => 'Testuj polaczenie',
|
||||||
|
],
|
||||||
|
'flash' => [
|
||||||
|
'saved' => 'Ustawienia polkurier zostaly zapisane.',
|
||||||
|
'save_failed' => 'Nie udalo sie zapisac ustawien polkurier.',
|
||||||
|
'test_success' => 'Polaczenie z polkurier dziala. :message',
|
||||||
|
'test_failed' => 'Nie udalo sie polaczyc z API polkurier.',
|
||||||
|
],
|
||||||
|
],
|
||||||
'hostedsms' => [
|
'hostedsms' => [
|
||||||
'title' => 'Integracja HostedSMS',
|
'title' => 'Integracja HostedSMS',
|
||||||
'description' => 'Konfiguracja konta HostedSMS do wysylki SMS z orderPRO.',
|
'description' => 'Konfiguracja konta HostedSMS do wysylki SMS z orderPRO.',
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
@use "modules/project-mappings";
|
@use "modules/project-mappings";
|
||||||
@use "modules/customer-risk-alert";
|
@use "modules/customer-risk-alert";
|
||||||
@use "modules/sms-templates";
|
@use "modules/sms-templates";
|
||||||
|
@use "modules/order-notes";
|
||||||
|
|
||||||
* {
|
* {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|||||||
112
resources/scss/modules/_order-notes.scss
Normal file
112
resources/scss/modules/_order-notes.scss
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
// Phase 129-01: notatki autorskie operatora w szczegolach zamowienia + badge na liscie.
|
||||||
|
|
||||||
|
.order-notes-badge {
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 6px;
|
||||||
|
padding: 1px 6px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #eef2ff;
|
||||||
|
color: #4338ca;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.4;
|
||||||
|
text-decoration: none;
|
||||||
|
vertical-align: middle;
|
||||||
|
transition: background 0.15s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #e0e7ff;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-notes-subtitle {
|
||||||
|
margin: 0 0 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--c-muted, #64748b);
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-user-notes {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.order-event--user {
|
||||||
|
border-left: 3px solid #6366f1;
|
||||||
|
padding-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-event__head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-event__meta {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--c-muted, #64748b);
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-event__actions {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-imported-notes {
|
||||||
|
.order-event--imported {
|
||||||
|
opacity: 0.75;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-link {
|
||||||
|
background: none;
|
||||||
|
border: 0;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--c-primary, #2563eb);
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: underline;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--danger {
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-note-form,
|
||||||
|
.order-note-edit-form {
|
||||||
|
textarea {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 60px;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-note-form__actions,
|
||||||
|
.order-note-edit-form__actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-note-edit-form {
|
||||||
|
margin-top: 6px;
|
||||||
|
|
||||||
|
.order-note-edit-form__actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -218,6 +218,7 @@
|
|||||||
<script src="/assets/js/modules/alert-dismiss.js?ver=<?= filemtime(dirname(__DIR__, 3) . '/public/assets/js/modules/alert-dismiss.js') ?: 0 ?>"></script>
|
<script src="/assets/js/modules/alert-dismiss.js?ver=<?= filemtime(dirname(__DIR__, 3) . '/public/assets/js/modules/alert-dismiss.js') ?: 0 ?>"></script>
|
||||||
<script src="/assets/js/modules/notifications.js?ver=<?= filemtime(dirname(__DIR__, 3) . '/public/assets/js/modules/notifications.js') ?: 0 ?>"></script>
|
<script src="/assets/js/modules/notifications.js?ver=<?= filemtime(dirname(__DIR__, 3) . '/public/assets/js/modules/notifications.js') ?: 0 ?>"></script>
|
||||||
<script src="/assets/js/modules/sms-template-picker.js?ver=<?= filemtime(dirname(__DIR__, 3) . '/public/assets/js/modules/sms-template-picker.js') ?: 0 ?>"></script>
|
<script src="/assets/js/modules/sms-template-picker.js?ver=<?= filemtime(dirname(__DIR__, 3) . '/public/assets/js/modules/sms-template-picker.js') ?: 0 ?>"></script>
|
||||||
|
<script src="/assets/js/modules/order-notes.js?ver=<?= filemtime(dirname(__DIR__, 3) . '/public/assets/js/modules/order-notes.js') ?: 0 ?>"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.8/dist/chart.umd.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.8/dist/chart.umd.min.js"></script>
|
||||||
<script src="/assets/js/modules/statistics-summary-charts.js?ver=<?= filemtime(dirname(__DIR__, 3) . '/public/assets/js/modules/statistics-summary-charts.js') ?: 0 ?>"></script>
|
<script src="/assets/js/modules/statistics-summary-charts.js?ver=<?= filemtime(dirname(__DIR__, 3) . '/public/assets/js/modules/statistics-summary-charts.js') ?: 0 ?>"></script>
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ $shipmentsList = is_array($shipments ?? null) ? $shipments : [];
|
|||||||
$packagesList = is_array($packages ?? null) ? $packages : [];
|
$packagesList = is_array($packages ?? null) ? $packages : [];
|
||||||
$documentsList = is_array($documents ?? null) ? $documents : [];
|
$documentsList = is_array($documents ?? null) ? $documents : [];
|
||||||
$notesList = is_array($notes ?? null) ? $notes : [];
|
$notesList = is_array($notes ?? null) ? $notes : [];
|
||||||
|
$userNotesList = is_array($userNotes ?? null) ? $userNotes : [];
|
||||||
|
$currentUserIdValue = (int) ($currentUserId ?? 0);
|
||||||
|
$csrfTokenValue = (string) ($csrfToken ?? '');
|
||||||
$receiptsList = is_array($receipts ?? null) ? $receipts : [];
|
$receiptsList = is_array($receipts ?? null) ? $receipts : [];
|
||||||
$receiptConfigsList = is_array($receiptConfigs ?? null) ? $receiptConfigs : [];
|
$receiptConfigsList = is_array($receiptConfigs ?? null) ? $receiptConfigs : [];
|
||||||
$invoicesList = is_array($invoices ?? null) ? $invoices : [];
|
$invoicesList = is_array($invoices ?? null) ? $invoices : [];
|
||||||
@@ -447,19 +450,70 @@ foreach ($addressesList as $address) {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="mt-16 order-grid-2">
|
<section class="mt-16 order-grid-2">
|
||||||
<article class="card">
|
<article class="card" id="notes">
|
||||||
<h3 class="section-title"><?= $e($t('orders.details.notes_title')) ?></h3>
|
<h3 class="section-title"><?= $e($t('orders.details.notes_title')) ?></h3>
|
||||||
<div class="order-events mt-12">
|
|
||||||
<?php if ($notesList === []): ?>
|
<div class="order-user-notes mt-12" data-order-id="<?= (int) ($orderId ?? 0) ?>">
|
||||||
<div class="muted">-</div>
|
<h4 class="order-notes-subtitle"><?= $e($t('orders.details.notes_user_title')) ?></h4>
|
||||||
|
<?php if ($userNotesList === []): ?>
|
||||||
|
<div class="muted"><?= $e($t('orders.details.notes_user_empty')) ?></div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
<?php foreach ($userNotesList as $userNote): ?>
|
||||||
|
<?php
|
||||||
|
$noteId = (int) ($userNote['id'] ?? 0);
|
||||||
|
$noteAuthorId = (int) ($userNote['user_id'] ?? 0);
|
||||||
|
$noteAuthorName = trim((string) ($userNote['author_name'] ?? ''));
|
||||||
|
$noteCreatedAt = (string) ($userNote['created_at'] ?? '');
|
||||||
|
$noteBody = (string) ($userNote['body'] ?? '');
|
||||||
|
$canEdit = $currentUserIdValue > 0 && $noteAuthorId === $currentUserIdValue;
|
||||||
|
?>
|
||||||
|
<div class="order-event order-event--user" data-note-id="<?= $noteId ?>">
|
||||||
|
<div class="order-event__head">
|
||||||
|
<span class="order-event__meta"><?= $e($noteCreatedAt) ?><?php if ($noteAuthorName !== ''): ?> · <?= $e($noteAuthorName) ?><?php endif; ?></span>
|
||||||
|
<?php if ($canEdit): ?>
|
||||||
|
<span class="order-event__actions">
|
||||||
|
<button type="button" class="btn-link js-order-note-edit" data-note-id="<?= $noteId ?>"><?= $e($t('orders.details.notes_user_edit')) ?></button>
|
||||||
|
<form method="post" action="/orders/<?= (int) ($orderId ?? 0) ?>/notes/<?= $noteId ?>/delete" class="js-order-note-delete" style="display:inline">
|
||||||
|
<input type="hidden" name="_token" value="<?= $e($csrfTokenValue) ?>">
|
||||||
|
<button type="submit" class="btn-link btn-link--danger"><?= $e($t('orders.details.notes_user_delete')) ?></button>
|
||||||
|
</form>
|
||||||
|
</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<div class="order-event__body js-order-note-body"><?= nl2br($e($noteBody)) ?></div>
|
||||||
|
<?php if ($canEdit): ?>
|
||||||
|
<form method="post" action="/orders/<?= (int) ($orderId ?? 0) ?>/notes/<?= $noteId ?>/update" class="order-note-edit-form js-order-note-edit-form" style="display:none">
|
||||||
|
<input type="hidden" name="_token" value="<?= $e($csrfTokenValue) ?>">
|
||||||
|
<textarea name="body" class="form-control" rows="3" maxlength="2000" required><?= $e($noteBody) ?></textarea>
|
||||||
|
<div class="order-note-edit-form__actions">
|
||||||
|
<button type="submit" class="btn btn-primary btn-sm"><?= $e($t('orders.details.notes_user_save')) ?></button>
|
||||||
|
<button type="button" class="btn btn-default btn-sm js-order-note-edit-cancel"><?= $e($t('orders.details.notes_user_cancel')) ?></button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
|
||||||
|
<form method="post" action="/orders/<?= (int) ($orderId ?? 0) ?>/notes" class="order-note-form mt-12">
|
||||||
|
<input type="hidden" name="_token" value="<?= $e($csrfTokenValue) ?>">
|
||||||
|
<textarea name="body" class="form-control" rows="3" maxlength="2000" placeholder="<?= $e($t('orders.details.notes_user_add_placeholder')) ?>" required></textarea>
|
||||||
|
<div class="order-note-form__actions">
|
||||||
|
<button type="submit" class="btn btn-primary btn-sm"><?= $e($t('orders.details.notes_user_save')) ?></button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($notesList !== []): ?>
|
||||||
|
<div class="order-imported-notes mt-16">
|
||||||
|
<h4 class="order-notes-subtitle"><?= $e($t('orders.details.notes_imported_title')) ?></h4>
|
||||||
<?php foreach ($notesList as $note): ?>
|
<?php foreach ($notesList as $note): ?>
|
||||||
<div class="order-event">
|
<div class="order-event order-event--imported">
|
||||||
<div class="order-event__head"><?= $e((string) ($note['note_type'] ?? '')) ?> | <?= $e((string) ($note['created_at_external'] ?? '')) ?></div>
|
<div class="order-event__head"><?= $e((string) ($note['note_type'] ?? '')) ?> | <?= $e((string) ($note['created_at_external'] ?? '')) ?></div>
|
||||||
<div class="order-event__body"><?= $e((string) ($note['comment'] ?? '')) ?></div>
|
<div class="order-event__body"><?= $e((string) ($note['comment'] ?? '')) ?></div>
|
||||||
</div>
|
</div>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</div>
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<article class="card">
|
<article class="card">
|
||||||
|
|||||||
110
resources/views/settings/polkurier.php
Normal file
110
resources/views/settings/polkurier.php
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
<?php
|
||||||
|
$settings = is_array($settings ?? null) ? $settings : [];
|
||||||
|
$login = trim((string) ($settings['login'] ?? ''));
|
||||||
|
$labelFormat = strtoupper(trim((string) ($settings['default_label_format'] ?? 'PDF'))) ?: 'PDF';
|
||||||
|
$hasToken = (bool) ($settings['has_api_token'] ?? false);
|
||||||
|
$isActive = (bool) ($settings['is_active'] ?? true);
|
||||||
|
$lastTestAt = trim((string) ($settings['last_test_at'] ?? ''));
|
||||||
|
$lastTestStatus = trim((string) ($settings['last_test_status'] ?? ''));
|
||||||
|
$lastTestMessage = trim((string) ($settings['last_test_message'] ?? ''));
|
||||||
|
$lastTestHttpCode = $settings['last_test_http_code'] ?? null;
|
||||||
|
$labelFormats = ['PDF', 'ZPL', 'EPL'];
|
||||||
|
?>
|
||||||
|
|
||||||
|
<section class="card">
|
||||||
|
<h2 class="section-title"><?= $e($t('settings.polkurier.title')) ?></h2>
|
||||||
|
<p class="muted mt-12"><?= $e($t('settings.polkurier.description')) ?></p>
|
||||||
|
|
||||||
|
<?php if (!empty($errorMessage)): ?>
|
||||||
|
<div class="mt-12"><?php $type='danger'; $message=(string) $errorMessage; $dismissible=true; include dirname(__DIR__) . '/components/alert.php'; ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if (!empty($successMessage)): ?>
|
||||||
|
<div class="mt-12"><?php $type='success'; $message=(string) $successMessage; $dismissible=true; include dirname(__DIR__) . '/components/alert.php'; ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if (!empty($testMessage)): ?>
|
||||||
|
<div class="mt-12"><?php $type='info'; $message=(string) $testMessage; $dismissible=true; include dirname(__DIR__) . '/components/alert.php'; ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="card mt-16">
|
||||||
|
<h3 class="section-title"><?= $e($t('settings.polkurier.config.title')) ?></h3>
|
||||||
|
|
||||||
|
<div class="muted mt-12">
|
||||||
|
<?= $e($t('settings.polkurier.status.token')) ?>:
|
||||||
|
<strong><?= $e($hasToken ? $t('settings.polkurier.status.saved') : $t('settings.polkurier.status.missing')) ?></strong>
|
||||||
|
|
|
||||||
|
<?= $e($t('settings.polkurier.status.active')) ?>:
|
||||||
|
<strong><?= $e($isActive ? $t('settings.integrations_hub.active.yes') : $t('settings.integrations_hub.active.no')) ?></strong>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form class="statuses-form mt-16" action="/settings/integrations/polkurier/save" method="post" novalidate>
|
||||||
|
<input type="hidden" name="_token" value="<?= $e($csrfToken ?? '') ?>">
|
||||||
|
|
||||||
|
<label class="form-field">
|
||||||
|
<span class="field-label"><?= $e($t('settings.polkurier.fields.login')) ?></span>
|
||||||
|
<input class="form-control" type="text" name="login" maxlength="190" value="<?= $e($login) ?>" required>
|
||||||
|
<span class="muted"><?= $e($t('settings.polkurier.hints.login')) ?></span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="form-field">
|
||||||
|
<span class="field-label"><?= $e($t('settings.polkurier.fields.api_token')) ?></span>
|
||||||
|
<input class="form-control" type="password" name="api_token" autocomplete="new-password" placeholder="<?= $hasToken ? '********' : '' ?>" <?= $hasToken ? '' : 'required' ?>>
|
||||||
|
<span class="muted"><?= $e($hasToken ? $t('settings.polkurier.token.saved') : $t('settings.polkurier.token.missing')) ?></span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="form-field">
|
||||||
|
<span class="field-label"><?= $e($t('settings.polkurier.fields.default_label_format')) ?></span>
|
||||||
|
<select class="form-control" name="default_label_format">
|
||||||
|
<?php foreach ($labelFormats as $fmt): ?>
|
||||||
|
<option value="<?= $e($fmt) ?>"<?= $fmt === $labelFormat ? ' selected' : '' ?>><?= $e($fmt) ?></option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="form-field form-field--inline">
|
||||||
|
<input type="checkbox" name="is_active" value="1"<?= $isActive ? ' checked' : '' ?>>
|
||||||
|
<span class="field-label"><?= $e($t('settings.polkurier.fields.is_active')) ?></span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div class="form-actions mt-16">
|
||||||
|
<button type="submit" class="btn btn--primary"><?= $e($t('settings.polkurier.actions.save')) ?></button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="card mt-16">
|
||||||
|
<h3 class="section-title"><?= $e($t('settings.polkurier.test.title')) ?></h3>
|
||||||
|
<p class="muted mt-12"><?= $e($t('settings.polkurier.test.description')) ?></p>
|
||||||
|
|
||||||
|
<form class="statuses-form mt-16" action="/settings/integrations/polkurier/test" method="post" novalidate>
|
||||||
|
<input type="hidden" name="_token" value="<?= $e($csrfToken ?? '') ?>">
|
||||||
|
<div class="form-actions mt-16">
|
||||||
|
<button type="submit" class="btn btn--secondary"><?= $e($t('settings.polkurier.actions.send_test')) ?></button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<?php if ($lastTestAt !== ''): ?>
|
||||||
|
<div class="mt-16">
|
||||||
|
<?php
|
||||||
|
$type = $lastTestStatus === 'ok' ? 'success' : 'danger';
|
||||||
|
$parts = [];
|
||||||
|
$parts[] = $e($t('settings.polkurier.status.last_test')) . ': ' . $e($lastTestAt);
|
||||||
|
if ($lastTestStatus !== '') {
|
||||||
|
$parts[] = '<span class="badge badge--' . ($lastTestStatus === 'ok' ? 'success' : 'muted') . '">' . $e(strtoupper($lastTestStatus)) . '</span>';
|
||||||
|
}
|
||||||
|
if ($lastTestHttpCode !== null) {
|
||||||
|
$parts[] = '<span class="badge badge--muted">HTTP ' . $e((string) $lastTestHttpCode) . '</span>';
|
||||||
|
}
|
||||||
|
if ($lastTestMessage !== '') {
|
||||||
|
$parts[] = $e($lastTestMessage);
|
||||||
|
}
|
||||||
|
$messageHtml = implode(' · ', $parts);
|
||||||
|
$dismissible = false;
|
||||||
|
include dirname(__DIR__) . '/components/alert.php';
|
||||||
|
unset($messageHtml);
|
||||||
|
?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</section>
|
||||||
@@ -6,6 +6,7 @@ $prefs = is_array($preferences ?? null) ? $preferences : [];
|
|||||||
$comp = is_array($company ?? null) ? $company : [];
|
$comp = is_array($company ?? null) ? $company : [];
|
||||||
$services = is_array($deliveryServices ?? null) ? $deliveryServices : [];
|
$services = is_array($deliveryServices ?? null) ? $deliveryServices : [];
|
||||||
$apaczkaSvcList = is_array($apaczkaServices ?? null) ? $apaczkaServices : [];
|
$apaczkaSvcList = is_array($apaczkaServices ?? null) ? $apaczkaServices : [];
|
||||||
|
$polkurierSvcList = is_array($polkurierServices ?? null) ? $polkurierServices : [];
|
||||||
$packages = is_array($existingPackages ?? null) ? $existingPackages : [];
|
$packages = is_array($existingPackages ?? null) ? $existingPackages : [];
|
||||||
$pendingPrintIds = is_array($pendingPrintPackageIds ?? null) ? $pendingPrintPackageIds : [];
|
$pendingPrintIds = is_array($pendingPrintPackageIds ?? null) ? $pendingPrintPackageIds : [];
|
||||||
$servicesError = (string) ($deliveryServicesError ?? '');
|
$servicesError = (string) ($deliveryServicesError ?? '');
|
||||||
@@ -105,6 +106,7 @@ $defaultCodAmount = $isCod ? number_format($totalWithTax, 2, '.', '') : '0';
|
|||||||
<option value="allegro"<?= $preselectedCarrier === 'allegro' ? ' selected' : '' ?>>Allegro</option>
|
<option value="allegro"<?= $preselectedCarrier === 'allegro' ? ' selected' : '' ?>>Allegro</option>
|
||||||
<option value="inpost"<?= $preselectedCarrier === 'inpost' ? ' selected' : '' ?>>InPost</option>
|
<option value="inpost"<?= $preselectedCarrier === 'inpost' ? ' selected' : '' ?>>InPost</option>
|
||||||
<option value="apaczka"<?= $preselectedCarrier === 'apaczka' ? ' selected' : '' ?>>Apaczka</option>
|
<option value="apaczka"<?= $preselectedCarrier === 'apaczka' ? ' selected' : '' ?>>Apaczka</option>
|
||||||
|
<option value="polkurier"<?= $preselectedCarrier === 'polkurier' ? ' selected' : '' ?>>polkurier</option>
|
||||||
</select>
|
</select>
|
||||||
<?php if ($deliveryMethodName !== ''): ?>
|
<?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' : ($mappedCarrier === 'apaczka' ? 'Apaczka' : '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>
|
||||||
@@ -213,12 +215,46 @@ $defaultCodAmount = $isCod ? number_format($totalWithTax, 2, '.', '') : '0';
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="shipment-polkurier-panel" style="<?= $preselectedCarrier !== 'polkurier' ? 'display:none' : '' ?>">
|
||||||
|
<?php if ($polkurierSvcList === []): ?>
|
||||||
|
<div class="muted">Brak uslug polkurier (sprawdz konfiguracje w Ustawienia → Integracje → polkurier).</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<select class="form-control" id="shipment-polkurier-select">
|
||||||
|
<option value="">-- Wybierz usluge polkurier --</option>
|
||||||
|
<?php foreach ($polkurierSvcList as $pSvc): ?>
|
||||||
|
<?php
|
||||||
|
if (!is_array($pSvc)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$pSvcId = trim((string) ($pSvc['id'] ?? ''));
|
||||||
|
$pSvcName = trim((string) ($pSvc['name'] ?? $pSvcId));
|
||||||
|
if ($pSvcId === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<option
|
||||||
|
value="<?= $e($pSvcId) ?>"
|
||||||
|
data-carrier-id="<?= $e($pSvcId) ?>">
|
||||||
|
<?= $e($pSvcName) ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
<div class="muted mt-4" style="font-size:12px">Dla uslug paczkomatowych wpisz ID punktu w pole "Punkt odbioru" w sekcji Adres odbiorcy ponizej (np. <code>POP-RZE54</code>).</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="shipment-empty-panel" class="muted" style="<?= $preselectedCarrier !== '' ? 'display:none' : '' ?>">Wybierz przewoznika</div>
|
<div id="shipment-empty-panel" class="muted" style="<?= $preselectedCarrier !== '' ? 'display:none' : '' ?>">Wybierz przewoznika</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<input type="hidden" name="credentials_id" id="shipment-credentials-id" value="">
|
<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="carrier_id" id="shipment-carrier-id" value="">
|
||||||
<input type="hidden" name="provider_code" id="shipment-provider-code" value="<?= $e($preselectedCarrier === 'apaczka' ? 'apaczka' : ($preselectedCarrier === 'inpost' ? 'inpost' : 'allegro_wza')) ?>">
|
<input type="hidden" name="service_code" id="shipment-service-code" value="">
|
||||||
|
<input type="hidden" name="provider_code" id="shipment-provider-code" value="<?php
|
||||||
|
if ($preselectedCarrier === 'apaczka') { echo 'apaczka'; }
|
||||||
|
elseif ($preselectedCarrier === 'inpost') { echo 'inpost'; }
|
||||||
|
elseif ($preselectedCarrier === 'polkurier') { echo 'polkurier'; }
|
||||||
|
else { echo 'allegro_wza'; }
|
||||||
|
?>">
|
||||||
|
|
||||||
<label class="form-field">
|
<label class="form-field">
|
||||||
<span class="field-label">Typ paczki</span>
|
<span class="field-label">Typ paczki</span>
|
||||||
@@ -586,6 +622,8 @@ $defaultCodAmount = $isCod ? number_format($totalWithTax, 2, '.', '') : '0';
|
|||||||
var carrierSelect = document.getElementById('shipment-carrier-select');
|
var carrierSelect = document.getElementById('shipment-carrier-select');
|
||||||
var inpostSelect = document.getElementById('shipment-inpost-select');
|
var inpostSelect = document.getElementById('shipment-inpost-select');
|
||||||
var apaczkaSelect = document.getElementById('shipment-apaczka-select');
|
var apaczkaSelect = document.getElementById('shipment-apaczka-select');
|
||||||
|
var polkurierSelect = document.getElementById('shipment-polkurier-select');
|
||||||
|
var serviceCodeInput = document.getElementById('shipment-service-code');
|
||||||
|
|
||||||
document.querySelectorAll('form select.form-control').forEach(function (sel) {
|
document.querySelectorAll('form select.form-control').forEach(function (sel) {
|
||||||
enhanceSelect(sel);
|
enhanceSelect(sel);
|
||||||
@@ -594,6 +632,7 @@ $defaultCodAmount = $isCod ? number_format($totalWithTax, 2, '.', '') : '0';
|
|||||||
var allegroPanel = document.getElementById('shipment-allegro-panel');
|
var allegroPanel = document.getElementById('shipment-allegro-panel');
|
||||||
var inpostPanel = document.getElementById('shipment-inpost-panel');
|
var inpostPanel = document.getElementById('shipment-inpost-panel');
|
||||||
var apaczkaPanel = document.getElementById('shipment-apaczka-panel');
|
var apaczkaPanel = document.getElementById('shipment-apaczka-panel');
|
||||||
|
var polkurierPanel = document.getElementById('shipment-polkurier-panel');
|
||||||
var emptyPanel = document.getElementById('shipment-empty-panel');
|
var emptyPanel = document.getElementById('shipment-empty-panel');
|
||||||
|
|
||||||
var wrapper = document.getElementById('shipment-service-wrapper');
|
var wrapper = document.getElementById('shipment-service-wrapper');
|
||||||
@@ -612,14 +651,21 @@ $defaultCodAmount = $isCod ? number_format($totalWithTax, 2, '.', '') : '0';
|
|||||||
hiddenInput.value = '';
|
hiddenInput.value = '';
|
||||||
credentialsInput.value = '';
|
credentialsInput.value = '';
|
||||||
carrierInput.value = '';
|
carrierInput.value = '';
|
||||||
|
if (serviceCodeInput) serviceCodeInput.value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
function showPanel(carrier) {
|
function showPanel(carrier) {
|
||||||
if (allegroPanel) allegroPanel.style.display = carrier === 'allegro' ? '' : 'none';
|
if (allegroPanel) allegroPanel.style.display = carrier === 'allegro' ? '' : 'none';
|
||||||
if (inpostPanel) inpostPanel.style.display = carrier === 'inpost' ? '' : 'none';
|
if (inpostPanel) inpostPanel.style.display = carrier === 'inpost' ? '' : 'none';
|
||||||
if (apaczkaPanel) apaczkaPanel.style.display = carrier === 'apaczka' ? '' : 'none';
|
if (apaczkaPanel) apaczkaPanel.style.display = carrier === 'apaczka' ? '' : 'none';
|
||||||
|
if (polkurierPanel) polkurierPanel.style.display = carrier === 'polkurier' ? '' : 'none';
|
||||||
if (emptyPanel) emptyPanel.style.display = carrier === '' ? '' : 'none';
|
if (emptyPanel) emptyPanel.style.display = carrier === '' ? '' : 'none';
|
||||||
if (providerInput) providerInput.value = carrier === 'apaczka' ? 'apaczka' : (carrier === 'inpost' ? 'inpost' : 'allegro_wza');
|
if (providerInput) {
|
||||||
|
if (carrier === 'apaczka') providerInput.value = 'apaczka';
|
||||||
|
else if (carrier === 'inpost') providerInput.value = 'inpost';
|
||||||
|
else if (carrier === 'polkurier') providerInput.value = 'polkurier';
|
||||||
|
else providerInput.value = 'allegro_wza';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var weekendWrap = document.getElementById('shipment-apaczka-weekend-wrap');
|
var weekendWrap = document.getElementById('shipment-apaczka-weekend-wrap');
|
||||||
@@ -648,6 +694,10 @@ $defaultCodAmount = $isCod ? number_format($totalWithTax, 2, '.', '') : '0';
|
|||||||
apaczkaSelect.selectedIndex = 0;
|
apaczkaSelect.selectedIndex = 0;
|
||||||
if (apaczkaSelect._syncTrigger) apaczkaSelect._syncTrigger();
|
if (apaczkaSelect._syncTrigger) apaczkaSelect._syncTrigger();
|
||||||
}
|
}
|
||||||
|
if (polkurierSelect) {
|
||||||
|
polkurierSelect.selectedIndex = 0;
|
||||||
|
if (polkurierSelect._syncTrigger) polkurierSelect._syncTrigger();
|
||||||
|
}
|
||||||
allegroOpts.forEach(function (o) { o.classList.remove('is-selected'); });
|
allegroOpts.forEach(function (o) { o.classList.remove('is-selected'); });
|
||||||
showPanel(carrierSelect.value);
|
showPanel(carrierSelect.value);
|
||||||
toggleWeekendOption();
|
toggleWeekendOption();
|
||||||
@@ -683,6 +733,24 @@ $defaultCodAmount = $isCod ? number_format($totalWithTax, 2, '.', '') : '0';
|
|||||||
}
|
}
|
||||||
toggleWeekendOption();
|
toggleWeekendOption();
|
||||||
|
|
||||||
|
if (polkurierSelect) {
|
||||||
|
function syncPolkurierFields() {
|
||||||
|
var opt = polkurierSelect.options[polkurierSelect.selectedIndex];
|
||||||
|
var serviceCode = polkurierSelect.value;
|
||||||
|
hiddenInput.value = serviceCode;
|
||||||
|
if (serviceCodeInput) serviceCodeInput.value = serviceCode;
|
||||||
|
credentialsInput.value = '';
|
||||||
|
carrierInput.value = opt ? (opt.getAttribute('data-carrier-id') || serviceCode) : serviceCode;
|
||||||
|
if (providerInput) providerInput.value = 'polkurier';
|
||||||
|
}
|
||||||
|
|
||||||
|
polkurierSelect.addEventListener('change', syncPolkurierFields);
|
||||||
|
|
||||||
|
if (carrierSelect.value === 'polkurier' && polkurierSelect.value !== '') {
|
||||||
|
syncPolkurierFields();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (wrapper && searchInput && dropdown) {
|
if (wrapper && searchInput && dropdown) {
|
||||||
var isAllegroOpen = false;
|
var isAllegroOpen = false;
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ use App\Modules\Cron\CronHandlerFactory;
|
|||||||
use App\Modules\Cron\CronRepository;
|
use App\Modules\Cron\CronRepository;
|
||||||
use App\Modules\Orders\OrdersController;
|
use App\Modules\Orders\OrdersController;
|
||||||
use App\Modules\Orders\OrderImportRepository;
|
use App\Modules\Orders\OrderImportRepository;
|
||||||
|
use App\Modules\Orders\OrderNotesService;
|
||||||
use App\Modules\Orders\OrdersRepository;
|
use App\Modules\Orders\OrdersRepository;
|
||||||
use App\Modules\Statistics\OrdersStatisticsController;
|
use App\Modules\Statistics\OrdersStatisticsController;
|
||||||
use App\Modules\Statistics\OrdersStatisticsRepository;
|
use App\Modules\Statistics\OrdersStatisticsRepository;
|
||||||
@@ -49,6 +50,9 @@ use App\Modules\Settings\InpostIntegrationController;
|
|||||||
use App\Modules\Settings\InpostIntegrationRepository;
|
use App\Modules\Settings\InpostIntegrationRepository;
|
||||||
use App\Modules\Settings\IntegrationsHubController;
|
use App\Modules\Settings\IntegrationsHubController;
|
||||||
use App\Modules\Settings\IntegrationsRepository;
|
use App\Modules\Settings\IntegrationsRepository;
|
||||||
|
use App\Modules\Settings\PolkurierApiClient;
|
||||||
|
use App\Modules\Settings\PolkurierIntegrationController;
|
||||||
|
use App\Modules\Settings\PolkurierIntegrationRepository;
|
||||||
use App\Modules\Settings\SmsplanetApiClient;
|
use App\Modules\Settings\SmsplanetApiClient;
|
||||||
use App\Modules\Settings\SmsplanetIntegrationController;
|
use App\Modules\Settings\SmsplanetIntegrationController;
|
||||||
use App\Modules\Settings\SmsplanetIntegrationRepository;
|
use App\Modules\Settings\SmsplanetIntegrationRepository;
|
||||||
@@ -89,6 +93,7 @@ use App\Modules\Settings\DeliveryStatusMappingController;
|
|||||||
use App\Modules\Settings\DeliveryStatusesController;
|
use App\Modules\Settings\DeliveryStatusesController;
|
||||||
use App\Modules\Settings\SettingsController;
|
use App\Modules\Settings\SettingsController;
|
||||||
use App\Modules\Shipments\ApaczkaShipmentService;
|
use App\Modules\Shipments\ApaczkaShipmentService;
|
||||||
|
use App\Modules\Shipments\PolkurierShipmentService;
|
||||||
use App\Modules\Shipments\AllegroShipmentService;
|
use App\Modules\Shipments\AllegroShipmentService;
|
||||||
use App\Modules\Shipments\InpostShipmentService;
|
use App\Modules\Shipments\InpostShipmentService;
|
||||||
use App\Modules\Shipments\ShipmentController;
|
use App\Modules\Shipments\ShipmentController;
|
||||||
@@ -227,6 +232,18 @@ return static function (Application $app): void {
|
|||||||
new HostedSmsApiClient(),
|
new HostedSmsApiClient(),
|
||||||
new IntegrationsRepository($app->db())
|
new IntegrationsRepository($app->db())
|
||||||
);
|
);
|
||||||
|
$polkurierIntegrationRepository = new PolkurierIntegrationRepository(
|
||||||
|
$app->db(),
|
||||||
|
(string) $app->config('app.integrations.secret', '')
|
||||||
|
);
|
||||||
|
$polkurierIntegrationController = new PolkurierIntegrationController(
|
||||||
|
$template,
|
||||||
|
$translator,
|
||||||
|
$auth,
|
||||||
|
$polkurierIntegrationRepository,
|
||||||
|
new PolkurierApiClient(),
|
||||||
|
new IntegrationsRepository($app->db())
|
||||||
|
);
|
||||||
$smsplanetIntegrationRepository = new SmsplanetIntegrationRepository(
|
$smsplanetIntegrationRepository = new SmsplanetIntegrationRepository(
|
||||||
$app->db(),
|
$app->db(),
|
||||||
(string) $app->config('app.integrations.secret', '')
|
(string) $app->config('app.integrations.secret', '')
|
||||||
@@ -268,6 +285,7 @@ return static function (Application $app): void {
|
|||||||
$fakturowniaIntegrationRepository,
|
$fakturowniaIntegrationRepository,
|
||||||
$hostedSmsIntegrationRepository,
|
$hostedSmsIntegrationRepository,
|
||||||
$smsplanetIntegrationRepository,
|
$smsplanetIntegrationRepository,
|
||||||
|
$polkurierIntegrationRepository,
|
||||||
$erliIntegrationRepository
|
$erliIntegrationRepository
|
||||||
);
|
);
|
||||||
$cronSettingsController = new CronSettingsController(
|
$cronSettingsController = new CronSettingsController(
|
||||||
@@ -424,7 +442,8 @@ return static function (Application $app): void {
|
|||||||
$allegroDeliveryMappingController
|
$allegroDeliveryMappingController
|
||||||
);
|
);
|
||||||
$printJobRepository = new PrintJobRepository($app->db());
|
$printJobRepository = new PrintJobRepository($app->db());
|
||||||
$ordersController = new OrdersController($template, $translator, $auth, $app->orders(), $shipmentPackageRepositoryForOrders, $receiptRepository, $receiptConfigRepository, $emailSendingService, $emailTemplateRepository, $emailMailboxRepository, $app->basePath('storage'), $printJobRepository, $shopproIntegrationsRepository, $automationService, $invoiceRepository, $invoiceConfigRepository, $smsMessageRepository, $smsConversationService, $smsTemplateRepository, $smsVariableResolver, $companySettingsRepository);
|
$orderNotesService = new OrderNotesService($app->db());
|
||||||
|
$ordersController = new OrdersController($template, $translator, $auth, $app->orders(), $shipmentPackageRepositoryForOrders, $receiptRepository, $receiptConfigRepository, $emailSendingService, $emailTemplateRepository, $emailMailboxRepository, $app->basePath('storage'), $printJobRepository, $shopproIntegrationsRepository, $automationService, $invoiceRepository, $invoiceConfigRepository, $smsMessageRepository, $smsConversationService, $smsTemplateRepository, $smsVariableResolver, $companySettingsRepository, $orderNotesService);
|
||||||
$ordersStatisticsController = new OrdersStatisticsController(
|
$ordersStatisticsController = new OrdersStatisticsController(
|
||||||
$template,
|
$template,
|
||||||
$translator,
|
$translator,
|
||||||
@@ -482,10 +501,18 @@ return static function (Application $app): void {
|
|||||||
$companySettingsRepository,
|
$companySettingsRepository,
|
||||||
new OrdersRepository($app->db())
|
new OrdersRepository($app->db())
|
||||||
);
|
);
|
||||||
|
$polkurierShipmentService = new PolkurierShipmentService(
|
||||||
|
$polkurierIntegrationRepository,
|
||||||
|
new PolkurierApiClient(),
|
||||||
|
$shipmentPackageRepository,
|
||||||
|
$companySettingsRepository,
|
||||||
|
new OrdersRepository($app->db())
|
||||||
|
);
|
||||||
$shipmentProviderRegistry = new ShipmentProviderRegistry([
|
$shipmentProviderRegistry = new ShipmentProviderRegistry([
|
||||||
$shipmentService,
|
$shipmentService,
|
||||||
$apaczkaShipmentService,
|
$apaczkaShipmentService,
|
||||||
$inpostShipmentService,
|
$inpostShipmentService,
|
||||||
|
$polkurierShipmentService,
|
||||||
]);
|
]);
|
||||||
$erliDeliveryMappingController = new ErliDeliveryMappingController(
|
$erliDeliveryMappingController = new ErliDeliveryMappingController(
|
||||||
$translator,
|
$translator,
|
||||||
@@ -627,6 +654,9 @@ return static function (Application $app): void {
|
|||||||
$router->post('/orders/{id}/email-preview', [$ordersController, 'emailPreview'], [$authMiddleware]);
|
$router->post('/orders/{id}/email-preview', [$ordersController, 'emailPreview'], [$authMiddleware]);
|
||||||
$router->get('/api/orders/search', [$ordersController, 'quickSearch'], [$authMiddleware]);
|
$router->get('/api/orders/search', [$ordersController, 'quickSearch'], [$authMiddleware]);
|
||||||
$router->get('/api/orders/{id}/preview', [$ordersController, 'preview'], [$authMiddleware]);
|
$router->get('/api/orders/{id}/preview', [$ordersController, 'preview'], [$authMiddleware]);
|
||||||
|
$router->post('/orders/{id}/notes', [$ordersController, 'storeNote'], [$authMiddleware]);
|
||||||
|
$router->post('/orders/{id}/notes/{noteId}/update', [$ordersController, 'updateNote'], [$authMiddleware]);
|
||||||
|
$router->post('/orders/{id}/notes/{noteId}/delete', [$ordersController, 'deleteNote'], [$authMiddleware]);
|
||||||
$router->post('/users', [$usersController, 'store'], [$authMiddleware]);
|
$router->post('/users', [$usersController, 'store'], [$authMiddleware]);
|
||||||
$router->get('/settings/users', [$usersController, 'index'], [$authMiddleware]);
|
$router->get('/settings/users', [$usersController, 'index'], [$authMiddleware]);
|
||||||
$router->post('/settings/users', [$usersController, 'store'], [$authMiddleware]);
|
$router->post('/settings/users', [$usersController, 'store'], [$authMiddleware]);
|
||||||
@@ -670,6 +700,9 @@ return static function (Application $app): void {
|
|||||||
$router->get('/settings/integrations/hostedsms', [$hostedSmsIntegrationController, 'index'], [$authMiddleware]);
|
$router->get('/settings/integrations/hostedsms', [$hostedSmsIntegrationController, 'index'], [$authMiddleware]);
|
||||||
$router->post('/settings/integrations/hostedsms/save', [$hostedSmsIntegrationController, 'save'], [$authMiddleware]);
|
$router->post('/settings/integrations/hostedsms/save', [$hostedSmsIntegrationController, 'save'], [$authMiddleware]);
|
||||||
$router->post('/settings/integrations/hostedsms/test', [$hostedSmsIntegrationController, 'test'], [$authMiddleware]);
|
$router->post('/settings/integrations/hostedsms/test', [$hostedSmsIntegrationController, 'test'], [$authMiddleware]);
|
||||||
|
$router->get('/settings/integrations/polkurier', [$polkurierIntegrationController, 'index'], [$authMiddleware]);
|
||||||
|
$router->post('/settings/integrations/polkurier/save', [$polkurierIntegrationController, 'save'], [$authMiddleware]);
|
||||||
|
$router->post('/settings/integrations/polkurier/test', [$polkurierIntegrationController, 'test'], [$authMiddleware]);
|
||||||
$router->get('/settings/integrations/smsplanet', [$smsplanetIntegrationController, 'index'], [$authMiddleware]);
|
$router->get('/settings/integrations/smsplanet', [$smsplanetIntegrationController, 'index'], [$authMiddleware]);
|
||||||
$router->post('/settings/integrations/smsplanet/save', [$smsplanetIntegrationController, 'save'], [$authMiddleware]);
|
$router->post('/settings/integrations/smsplanet/save', [$smsplanetIntegrationController, 'save'], [$authMiddleware]);
|
||||||
$router->post('/settings/integrations/smsplanet/test', [$smsplanetIntegrationController, 'test'], [$authMiddleware]);
|
$router->post('/settings/integrations/smsplanet/test', [$smsplanetIntegrationController, 'test'], [$authMiddleware]);
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ use App\Modules\Settings\ErliStatusMappingRepository;
|
|||||||
use App\Modules\Settings\ErliStatusSyncService;
|
use App\Modules\Settings\ErliStatusSyncService;
|
||||||
use App\Modules\Settings\InpostIntegrationRepository;
|
use App\Modules\Settings\InpostIntegrationRepository;
|
||||||
use App\Modules\Settings\IntegrationSecretCipher;
|
use App\Modules\Settings\IntegrationSecretCipher;
|
||||||
|
use App\Modules\Settings\PolkurierApiClient;
|
||||||
|
use App\Modules\Settings\PolkurierIntegrationRepository;
|
||||||
use App\Modules\Settings\ReceiptConfigRepository;
|
use App\Modules\Settings\ReceiptConfigRepository;
|
||||||
use App\Modules\Settings\ShopproApiClient;
|
use App\Modules\Settings\ShopproApiClient;
|
||||||
use App\Modules\Settings\ShopproIntegrationsRepository;
|
use App\Modules\Settings\ShopproIntegrationsRepository;
|
||||||
@@ -58,6 +60,7 @@ use App\Modules\Shipments\AllegroTrackingService;
|
|||||||
use App\Modules\Shipments\ApaczkaTrackingService;
|
use App\Modules\Shipments\ApaczkaTrackingService;
|
||||||
use App\Modules\Shipments\DeliveryStatusMappingRepository;
|
use App\Modules\Shipments\DeliveryStatusMappingRepository;
|
||||||
use App\Modules\Shipments\InpostTrackingService;
|
use App\Modules\Shipments\InpostTrackingService;
|
||||||
|
use App\Modules\Shipments\PolkurierTrackingService;
|
||||||
use App\Modules\Shipments\ShipmentPackageRepository;
|
use App\Modules\Shipments\ShipmentPackageRepository;
|
||||||
use App\Modules\Shipments\ShipmentTrackingRegistry;
|
use App\Modules\Shipments\ShipmentTrackingRegistry;
|
||||||
use PDO;
|
use PDO;
|
||||||
@@ -210,6 +213,11 @@ final class CronHandlerFactory
|
|||||||
new AllegroTrackingService(
|
new AllegroTrackingService(
|
||||||
new InpostIntegrationRepository($this->db, $this->integrationSecret)
|
new InpostIntegrationRepository($this->db, $this->integrationSecret)
|
||||||
),
|
),
|
||||||
|
new PolkurierTrackingService(
|
||||||
|
new PolkurierApiClient(),
|
||||||
|
new PolkurierIntegrationRepository($this->db, $this->integrationSecret),
|
||||||
|
new DeliveryStatusMappingRepository($this->db)
|
||||||
|
),
|
||||||
]),
|
]),
|
||||||
new ShipmentPackageRepository($this->db),
|
new ShipmentPackageRepository($this->db),
|
||||||
$automationService,
|
$automationService,
|
||||||
|
|||||||
182
src/Modules/Orders/OrderNotesService.php
Normal file
182
src/Modules/Orders/OrderNotesService.php
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Modules\Orders;
|
||||||
|
|
||||||
|
use InvalidArgumentException;
|
||||||
|
use PDO;
|
||||||
|
use RuntimeException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Phase 129-01: CRUD notatek autorskich operatora (note_type='user').
|
||||||
|
* Importowane notatki ze zrodla (shoppro/allegro/message) maja wlasne zarzadzanie
|
||||||
|
* w OrderImportRepository::replaceNotes() — ten serwis ich nie dotyka.
|
||||||
|
*/
|
||||||
|
final class OrderNotesService
|
||||||
|
{
|
||||||
|
private const NOTE_TYPE_USER = 'user';
|
||||||
|
private const BODY_MAX_LENGTH = 2000;
|
||||||
|
|
||||||
|
public function __construct(private readonly PDO $pdo)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int, array<string, mixed>>
|
||||||
|
*/
|
||||||
|
public function listUserNotes(int $orderId): array
|
||||||
|
{
|
||||||
|
if ($orderId <= 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $this->pdo->prepare(
|
||||||
|
'SELECT id, order_id, user_id, author_name, comment AS body, created_at, updated_at
|
||||||
|
FROM order_notes
|
||||||
|
WHERE order_id = :order_id AND note_type = :note_type
|
||||||
|
ORDER BY created_at DESC, id DESC'
|
||||||
|
);
|
||||||
|
$stmt->execute(['order_id' => $orderId, 'note_type' => self::NOTE_TYPE_USER]);
|
||||||
|
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
return is_array($rows) ? $rows : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int, array<string, mixed>>
|
||||||
|
*/
|
||||||
|
public function listImportedNotes(int $orderId): array
|
||||||
|
{
|
||||||
|
if ($orderId <= 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $this->pdo->prepare(
|
||||||
|
'SELECT * FROM order_notes
|
||||||
|
WHERE order_id = :order_id AND note_type <> :note_type
|
||||||
|
ORDER BY created_at_external DESC, id DESC'
|
||||||
|
);
|
||||||
|
$stmt->execute(['order_id' => $orderId, 'note_type' => self::NOTE_TYPE_USER]);
|
||||||
|
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
return is_array($rows) ? $rows : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function countUserNotes(int $orderId): int
|
||||||
|
{
|
||||||
|
if ($orderId <= 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $this->pdo->prepare(
|
||||||
|
'SELECT COUNT(*) FROM order_notes WHERE order_id = :order_id AND note_type = :note_type'
|
||||||
|
);
|
||||||
|
$stmt->execute(['order_id' => $orderId, 'note_type' => self::NOTE_TYPE_USER]);
|
||||||
|
|
||||||
|
return (int) $stmt->fetchColumn();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findById(int $noteId): ?array
|
||||||
|
{
|
||||||
|
if ($noteId <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $this->pdo->prepare(
|
||||||
|
'SELECT id, order_id, user_id, author_name, comment AS body, created_at, updated_at, note_type
|
||||||
|
FROM order_notes WHERE id = :id LIMIT 1'
|
||||||
|
);
|
||||||
|
$stmt->execute(['id' => $noteId]);
|
||||||
|
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
return is_array($row) ? $row : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create(int $orderId, int $userId, string $authorName, string $body): int
|
||||||
|
{
|
||||||
|
$body = $this->sanitizeBody($body);
|
||||||
|
if ($orderId <= 0 || $userId <= 0) {
|
||||||
|
throw new InvalidArgumentException('Nieprawidlowe parametry notatki.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $this->pdo->prepare(
|
||||||
|
'INSERT INTO order_notes
|
||||||
|
(order_id, source_note_id, note_type, user_id, author_name, comment, created_at, updated_at)
|
||||||
|
VALUES
|
||||||
|
(:order_id, NULL, :note_type, :user_id, :author_name, :comment, NOW(), NOW())'
|
||||||
|
);
|
||||||
|
$stmt->execute([
|
||||||
|
'order_id' => $orderId,
|
||||||
|
'note_type' => self::NOTE_TYPE_USER,
|
||||||
|
'user_id' => $userId,
|
||||||
|
'author_name' => $authorName !== '' ? $authorName : null,
|
||||||
|
'comment' => $body,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (int) $this->pdo->lastInsertId();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws RuntimeException kod 403 gdy uzytkownik nie jest autorem notatki
|
||||||
|
*/
|
||||||
|
public function update(int $noteId, int $userId, string $body): void
|
||||||
|
{
|
||||||
|
$body = $this->sanitizeBody($body);
|
||||||
|
if ($noteId <= 0 || $userId <= 0) {
|
||||||
|
throw new InvalidArgumentException('Nieprawidlowe parametry notatki.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $this->pdo->prepare(
|
||||||
|
'UPDATE order_notes
|
||||||
|
SET comment = :comment, updated_at = NOW()
|
||||||
|
WHERE id = :id AND note_type = :note_type AND user_id = :user_id'
|
||||||
|
);
|
||||||
|
$stmt->execute([
|
||||||
|
'id' => $noteId,
|
||||||
|
'note_type' => self::NOTE_TYPE_USER,
|
||||||
|
'user_id' => $userId,
|
||||||
|
'comment' => $body,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($stmt->rowCount() === 0) {
|
||||||
|
throw new RuntimeException('Brak uprawnien — tylko autor moze edytowac notatke.', 403);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws RuntimeException kod 403 gdy uzytkownik nie jest autorem notatki
|
||||||
|
*/
|
||||||
|
public function delete(int $noteId, int $userId): void
|
||||||
|
{
|
||||||
|
if ($noteId <= 0 || $userId <= 0) {
|
||||||
|
throw new InvalidArgumentException('Nieprawidlowe parametry notatki.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $this->pdo->prepare(
|
||||||
|
'DELETE FROM order_notes
|
||||||
|
WHERE id = :id AND note_type = :note_type AND user_id = :user_id'
|
||||||
|
);
|
||||||
|
$stmt->execute([
|
||||||
|
'id' => $noteId,
|
||||||
|
'note_type' => self::NOTE_TYPE_USER,
|
||||||
|
'user_id' => $userId,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($stmt->rowCount() === 0) {
|
||||||
|
throw new RuntimeException('Brak uprawnien — tylko autor moze usunac notatke.', 403);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function sanitizeBody(string $body): string
|
||||||
|
{
|
||||||
|
$body = trim($body);
|
||||||
|
if ($body === '') {
|
||||||
|
throw new InvalidArgumentException('Tresc notatki nie moze byc pusta.');
|
||||||
|
}
|
||||||
|
if (function_exists('mb_strlen') ? mb_strlen($body) > self::BODY_MAX_LENGTH : strlen($body) > self::BODY_MAX_LENGTH) {
|
||||||
|
throw new InvalidArgumentException('Tresc notatki przekracza ' . self::BODY_MAX_LENGTH . ' znakow.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $body;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,6 +27,7 @@ use App\Modules\Sms\SmsConversationService;
|
|||||||
use App\Modules\Sms\SmsMessageRepository;
|
use App\Modules\Sms\SmsMessageRepository;
|
||||||
use App\Modules\Sms\SmsTemplateRepository;
|
use App\Modules\Sms\SmsTemplateRepository;
|
||||||
use App\Modules\Sms\SmsVariableResolver;
|
use App\Modules\Sms\SmsVariableResolver;
|
||||||
|
use RuntimeException;
|
||||||
use Throwable;
|
use Throwable;
|
||||||
|
|
||||||
final class OrdersController
|
final class OrdersController
|
||||||
@@ -52,7 +53,8 @@ final class OrdersController
|
|||||||
private readonly ?SmsConversationService $smsConversation = null,
|
private readonly ?SmsConversationService $smsConversation = null,
|
||||||
private readonly ?SmsTemplateRepository $smsTemplates = null,
|
private readonly ?SmsTemplateRepository $smsTemplates = null,
|
||||||
private readonly ?SmsVariableResolver $smsVariableResolver = null,
|
private readonly ?SmsVariableResolver $smsVariableResolver = null,
|
||||||
private readonly ?CompanySettingsRepository $companySettingsRepo = null
|
private readonly ?CompanySettingsRepository $companySettingsRepo = null,
|
||||||
|
private readonly ?OrderNotesService $orderNotes = null
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,6 +205,9 @@ final class OrdersController
|
|||||||
$shipments = is_array($details['shipments'] ?? null) ? $details['shipments'] : [];
|
$shipments = is_array($details['shipments'] ?? null) ? $details['shipments'] : [];
|
||||||
$documents = is_array($details['documents'] ?? null) ? $details['documents'] : [];
|
$documents = is_array($details['documents'] ?? null) ? $details['documents'] : [];
|
||||||
$notes = is_array($details['notes'] ?? null) ? $details['notes'] : [];
|
$notes = is_array($details['notes'] ?? null) ? $details['notes'] : [];
|
||||||
|
$userNotes = $this->orderNotes !== null ? $this->orderNotes->listUserNotes($orderId) : [];
|
||||||
|
$currentUser = $this->auth->user();
|
||||||
|
$currentUserId = is_array($currentUser) ? (int) ($currentUser['id'] ?? 0) : 0;
|
||||||
$history = is_array($details['status_history'] ?? null) ? $details['status_history'] : [];
|
$history = is_array($details['status_history'] ?? null) ? $details['status_history'] : [];
|
||||||
$activityLog = is_array($details['activity_log'] ?? null) ? $details['activity_log'] : [];
|
$activityLog = is_array($details['activity_log'] ?? null) ? $details['activity_log'] : [];
|
||||||
$statusCode = (string) (($order['effective_status_id'] ?? '') !== '' ? $order['effective_status_id'] : ($order['status_code'] ?? ''));
|
$statusCode = (string) (($order['effective_status_id'] ?? '') !== '' ? $order['effective_status_id'] : ($order['status_code'] ?? ''));
|
||||||
@@ -279,6 +284,8 @@ final class OrdersController
|
|||||||
'pendingPrintPackageIds' => $this->printJobRepo !== null ? $this->printJobRepo->pendingPackageIds() : [],
|
'pendingPrintPackageIds' => $this->printJobRepo !== null ? $this->printJobRepo->pendingPackageIds() : [],
|
||||||
'documents' => $documents,
|
'documents' => $documents,
|
||||||
'notes' => $notes,
|
'notes' => $notes,
|
||||||
|
'userNotes' => $userNotes,
|
||||||
|
'currentUserId' => $currentUserId,
|
||||||
'history' => $resolvedHistory,
|
'history' => $resolvedHistory,
|
||||||
'activityLog' => $activityLog,
|
'activityLog' => $activityLog,
|
||||||
'statusLabel' => $this->statusLabel($statusCode, $statusLabelMap),
|
'statusLabel' => $this->statusLabel($statusCode, $statusLabelMap),
|
||||||
@@ -624,6 +631,136 @@ final class OrdersController
|
|||||||
return Response::json(['success' => true, 'invoice_requested' => $value ? 1 : 0]);
|
return Response::json(['success' => true, 'invoice_requested' => $value ? 1 : 0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function storeNote(Request $request): Response
|
||||||
|
{
|
||||||
|
$orderId = max(0, (int) $request->input('id', 0));
|
||||||
|
$redirectTo = '/orders/' . $orderId . '#notes';
|
||||||
|
|
||||||
|
if (!Csrf::validate((string) $request->input('_token', ''))) {
|
||||||
|
Flash::set('order.error', $this->translator->get('auth.errors.csrf_expired'));
|
||||||
|
return Response::redirect('/orders/' . $orderId);
|
||||||
|
}
|
||||||
|
if ($orderId <= 0 || $this->orderNotes === null) {
|
||||||
|
Flash::set('order.error', 'Modul notatek nie jest dostepny.');
|
||||||
|
return Response::redirect('/orders/' . $orderId);
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = $this->auth->user();
|
||||||
|
$userId = is_array($user) ? (int) ($user['id'] ?? 0) : 0;
|
||||||
|
$authorName = is_array($user) ? trim((string) ($user['name'] ?? $user['email'] ?? '')) : '';
|
||||||
|
if ($userId <= 0) {
|
||||||
|
Flash::set('order.error', 'Wymagane zalogowanie.');
|
||||||
|
return Response::redirect('/orders/' . $orderId);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->orderNotes->create($orderId, $userId, $authorName, (string) $request->input('body', ''));
|
||||||
|
$this->orders->recordActivity(
|
||||||
|
$orderId,
|
||||||
|
'note',
|
||||||
|
'Dodano notatke',
|
||||||
|
null,
|
||||||
|
'user',
|
||||||
|
$authorName !== '' ? $authorName : null
|
||||||
|
);
|
||||||
|
Flash::set('order.success', 'Notatka dodana.');
|
||||||
|
} catch (\InvalidArgumentException $exception) {
|
||||||
|
Flash::set('order.error', $exception->getMessage());
|
||||||
|
} catch (Throwable $exception) {
|
||||||
|
Flash::set('order.error', 'Nie udalo sie dodac notatki: ' . $exception->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response::redirect($redirectTo);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateNote(Request $request): Response
|
||||||
|
{
|
||||||
|
$orderId = max(0, (int) $request->input('id', 0));
|
||||||
|
$noteId = max(0, (int) $request->input('noteId', 0));
|
||||||
|
$redirectTo = '/orders/' . $orderId . '#notes';
|
||||||
|
|
||||||
|
if (!Csrf::validate((string) $request->input('_token', ''))) {
|
||||||
|
Flash::set('order.error', $this->translator->get('auth.errors.csrf_expired'));
|
||||||
|
return Response::redirect('/orders/' . $orderId);
|
||||||
|
}
|
||||||
|
if ($orderId <= 0 || $noteId <= 0 || $this->orderNotes === null) {
|
||||||
|
Flash::set('order.error', 'Nieprawidlowe parametry.');
|
||||||
|
return Response::redirect('/orders/' . $orderId);
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = $this->auth->user();
|
||||||
|
$userId = is_array($user) ? (int) ($user['id'] ?? 0) : 0;
|
||||||
|
$authorName = is_array($user) ? trim((string) ($user['name'] ?? $user['email'] ?? '')) : '';
|
||||||
|
if ($userId <= 0) {
|
||||||
|
Flash::set('order.error', 'Wymagane zalogowanie.');
|
||||||
|
return Response::redirect('/orders/' . $orderId);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->orderNotes->update($noteId, $userId, (string) $request->input('body', ''));
|
||||||
|
$this->orders->recordActivity(
|
||||||
|
$orderId,
|
||||||
|
'note',
|
||||||
|
'Zaktualizowano notatke',
|
||||||
|
['note_id' => $noteId],
|
||||||
|
'user',
|
||||||
|
$authorName !== '' ? $authorName : null
|
||||||
|
);
|
||||||
|
Flash::set('order.success', 'Notatka zaktualizowana.');
|
||||||
|
} catch (RuntimeException $exception) {
|
||||||
|
Flash::set('order.error', $exception->getMessage());
|
||||||
|
} catch (\InvalidArgumentException $exception) {
|
||||||
|
Flash::set('order.error', $exception->getMessage());
|
||||||
|
} catch (Throwable $exception) {
|
||||||
|
Flash::set('order.error', 'Nie udalo sie zaktualizowac notatki: ' . $exception->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response::redirect($redirectTo);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function deleteNote(Request $request): Response
|
||||||
|
{
|
||||||
|
$orderId = max(0, (int) $request->input('id', 0));
|
||||||
|
$noteId = max(0, (int) $request->input('noteId', 0));
|
||||||
|
$redirectTo = '/orders/' . $orderId . '#notes';
|
||||||
|
|
||||||
|
if (!Csrf::validate((string) $request->input('_token', ''))) {
|
||||||
|
Flash::set('order.error', $this->translator->get('auth.errors.csrf_expired'));
|
||||||
|
return Response::redirect('/orders/' . $orderId);
|
||||||
|
}
|
||||||
|
if ($orderId <= 0 || $noteId <= 0 || $this->orderNotes === null) {
|
||||||
|
Flash::set('order.error', 'Nieprawidlowe parametry.');
|
||||||
|
return Response::redirect('/orders/' . $orderId);
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = $this->auth->user();
|
||||||
|
$userId = is_array($user) ? (int) ($user['id'] ?? 0) : 0;
|
||||||
|
$authorName = is_array($user) ? trim((string) ($user['name'] ?? $user['email'] ?? '')) : '';
|
||||||
|
if ($userId <= 0) {
|
||||||
|
Flash::set('order.error', 'Wymagane zalogowanie.');
|
||||||
|
return Response::redirect('/orders/' . $orderId);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->orderNotes->delete($noteId, $userId);
|
||||||
|
$this->orders->recordActivity(
|
||||||
|
$orderId,
|
||||||
|
'note',
|
||||||
|
'Usunieto notatke',
|
||||||
|
['note_id' => $noteId],
|
||||||
|
'user',
|
||||||
|
$authorName !== '' ? $authorName : null
|
||||||
|
);
|
||||||
|
Flash::set('order.success', 'Notatka usunieta.');
|
||||||
|
} catch (RuntimeException $exception) {
|
||||||
|
Flash::set('order.error', $exception->getMessage());
|
||||||
|
} catch (Throwable $exception) {
|
||||||
|
Flash::set('order.error', 'Nie udalo sie usunac notatki: ' . $exception->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response::redirect($redirectTo);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<string, mixed> $row
|
* @param array<string, mixed> $row
|
||||||
* @return array<string, mixed>
|
* @return array<string, mixed>
|
||||||
@@ -658,6 +795,13 @@ final class OrdersController
|
|||||||
? ' <span class="risk-return-badge" title="Klient nie odebral ' . $returnedCount . ' przesylek w historii">zwroty: ' . $returnedCount . '</span>'
|
? ' <span class="risk-return-badge" title="Klient nie odebral ' . $returnedCount . ' przesylek w historii">zwroty: ' . $returnedCount . '</span>'
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
|
$userNotesCount = max(0, (int) ($row['user_notes_count'] ?? 0));
|
||||||
|
$notesBadge = $userNotesCount >= 1
|
||||||
|
? ' <a href="/orders/' . (int) ($row['id'] ?? 0) . '#notes" class="order-notes-badge" title="' . $userNotesCount . ' '
|
||||||
|
. ($userNotesCount === 1 ? 'notatka' : ($userNotesCount < 5 ? 'notatki' : 'notatek')) . '">['
|
||||||
|
. $userNotesCount . ']</a>'
|
||||||
|
: '';
|
||||||
|
|
||||||
$previewBtn = '<button type="button" class="btn-icon js-order-preview-btn" data-order-id="' . (int) ($row['id'] ?? 0) . '" title="Podglad">'
|
$previewBtn = '<button type="button" class="btn-icon js-order-preview-btn" data-order-id="' . (int) ($row['id'] ?? 0) . '" title="Podglad">'
|
||||||
. '<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>'
|
. '<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>'
|
||||||
. '</button>';
|
. '</button>';
|
||||||
@@ -667,7 +811,7 @@ final class OrdersController
|
|||||||
'order_ref' => '<div class="orders-ref">'
|
'order_ref' => '<div class="orders-ref">'
|
||||||
. '<div class="orders-ref__main">' . $previewBtn . '<a href="/orders/' . (int) ($row['id'] ?? 0) . '">'
|
. '<div class="orders-ref__main">' . $previewBtn . '<a href="/orders/' . (int) ($row['id'] ?? 0) . '">'
|
||||||
. htmlspecialchars($internalOrderNumber !== '' ? $internalOrderNumber : ('#' . (string) ($row['id'] ?? 0)), ENT_QUOTES, 'UTF-8')
|
. htmlspecialchars($internalOrderNumber !== '' ? $internalOrderNumber : ('#' . (string) ($row['id'] ?? 0)), ENT_QUOTES, 'UTF-8')
|
||||||
. '</a></div>'
|
. '</a>' . $notesBadge . '</div>'
|
||||||
. '<div class="orders-ref__meta">'
|
. '<div class="orders-ref__meta">'
|
||||||
. '<span>' . htmlspecialchars($integrationName !== '' ? $integrationName : $this->sourceLabel($source), ENT_QUOTES, 'UTF-8') . '</span>'
|
. '<span>' . htmlspecialchars($integrationName !== '' ? $integrationName : $this->sourceLabel($source), ENT_QUOTES, 'UTF-8') . '</span>'
|
||||||
. '<span>ID: ' . htmlspecialchars($sourceOrderId !== '' ? $sourceOrderId : $externalOrderId, ENT_QUOTES, 'UTF-8') . '</span>'
|
. '<span>ID: ' . htmlspecialchars($sourceOrderId !== '' ? $sourceOrderId : $externalOrderId, ENT_QUOTES, 'UTF-8') . '</span>'
|
||||||
|
|||||||
@@ -182,7 +182,8 @@ final class OrdersRepository
|
|||||||
COALESCE(sh_agg.shipments_count, 0) AS shipments_count,
|
COALESCE(sh_agg.shipments_count, 0) AS shipments_count,
|
||||||
COALESCE(od_agg.documents_count, 0) AS documents_count,
|
COALESCE(od_agg.documents_count, 0) AS documents_count,
|
||||||
ig.name AS integration_name,
|
ig.name AS integration_name,
|
||||||
' . $this->customerReturnedCountSubquerySql('o', 'a') . ' AS customer_returned_count
|
' . $this->customerReturnedCountSubquerySql('o', 'a') . ' AS customer_returned_count,
|
||||||
|
' . $this->userNotesCountSubquerySql('o') . ' AS user_notes_count
|
||||||
FROM orders o
|
FROM orders o
|
||||||
LEFT JOIN order_addresses a ON a.order_id = o.id AND a.address_type = "customer"
|
LEFT JOIN order_addresses a ON a.order_id = o.id AND a.address_type = "customer"
|
||||||
LEFT JOIN allegro_order_status_mappings asm ON o.source = "allegro" AND LOWER(o.status_code) = asm.allegro_status_code
|
LEFT JOIN allegro_order_status_mappings asm ON o.source = "allegro" AND LOWER(o.status_code) = asm.allegro_status_code
|
||||||
@@ -246,6 +247,7 @@ final class OrdersRepository
|
|||||||
'projects_done' => (int) ($row['projects_done'] ?? 0),
|
'projects_done' => (int) ($row['projects_done'] ?? 0),
|
||||||
'projects_total' => (int) ($row['projects_total'] ?? 0),
|
'projects_total' => (int) ($row['projects_total'] ?? 0),
|
||||||
'customer_returned_count' => max(0, (int) ($row['customer_returned_count'] ?? 0)),
|
'customer_returned_count' => max(0, (int) ($row['customer_returned_count'] ?? 0)),
|
||||||
|
'user_notes_count' => max(0, (int) ($row['user_notes_count'] ?? 0)),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -595,7 +597,9 @@ final class OrdersRepository
|
|||||||
*/
|
*/
|
||||||
private function loadOrderNotes(int $orderId): array
|
private function loadOrderNotes(int $orderId): array
|
||||||
{
|
{
|
||||||
$stmt = $this->pdo->prepare('SELECT * FROM order_notes WHERE order_id = :order_id ORDER BY created_at_external DESC, id DESC');
|
// Phase 129-01: zwraca tylko notatki importowane ze zrodla (note_type != 'user').
|
||||||
|
// Notatki autorskie operatora ladowane sa osobno przez OrderNotesService::listUserNotes().
|
||||||
|
$stmt = $this->pdo->prepare('SELECT * FROM order_notes WHERE order_id = :order_id AND note_type <> "user" ORDER BY created_at_external DESC, id DESC');
|
||||||
$stmt->execute(['order_id' => $orderId]);
|
$stmt->execute(['order_id' => $orderId]);
|
||||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
@@ -707,6 +711,16 @@ final class OrdersRepository
|
|||||||
))';
|
))';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Phase 129-01: subquery liczby notatek autorskich (note_type='user') dla zamowienia.
|
||||||
|
* Wspierane indeksem idx_order_notes_type_order (note_type, order_id).
|
||||||
|
*/
|
||||||
|
private function userNotesCountSubquerySql(string $orderAlias): string
|
||||||
|
{
|
||||||
|
return '(SELECT COUNT(*) FROM order_notes
|
||||||
|
WHERE order_id = ' . $orderAlias . '.id AND note_type = "user")';
|
||||||
|
}
|
||||||
|
|
||||||
private function effectiveStatusSql(string $orderAlias, string $mappingAlias): string
|
private function effectiveStatusSql(string $orderAlias, string $mappingAlias): string
|
||||||
{
|
{
|
||||||
return 'CASE
|
return 'CASE
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ final class DeliveryStatusMappingController
|
|||||||
'inpost' => 'InPost',
|
'inpost' => 'InPost',
|
||||||
'apaczka' => 'Apaczka',
|
'apaczka' => 'Apaczka',
|
||||||
'allegro_wza' => 'Allegro',
|
'allegro_wza' => 'Allegro',
|
||||||
|
'polkurier' => 'polkurier',
|
||||||
];
|
];
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ final class DeliveryStatusesController
|
|||||||
'inpost' => 'InPost',
|
'inpost' => 'InPost',
|
||||||
'apaczka' => 'Apaczka',
|
'apaczka' => 'Apaczka',
|
||||||
'allegro_wza' => 'Allegro',
|
'allegro_wza' => 'Allegro',
|
||||||
|
'polkurier' => 'polkurier',
|
||||||
];
|
];
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ final class IntegrationsHubController
|
|||||||
private readonly FakturowniaIntegrationRepository $fakturownia,
|
private readonly FakturowniaIntegrationRepository $fakturownia,
|
||||||
private readonly HostedSmsIntegrationRepository $hostedSms,
|
private readonly HostedSmsIntegrationRepository $hostedSms,
|
||||||
private readonly SmsplanetIntegrationRepository $smsplanet,
|
private readonly SmsplanetIntegrationRepository $smsplanet,
|
||||||
|
private readonly PolkurierIntegrationRepository $polkurier,
|
||||||
private readonly ErliIntegrationRepository $erli
|
private readonly ErliIntegrationRepository $erli
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
@@ -35,6 +36,7 @@ final class IntegrationsHubController
|
|||||||
$this->buildAllegroRow('sandbox'),
|
$this->buildAllegroRow('sandbox'),
|
||||||
$this->buildAllegroRow('production'),
|
$this->buildAllegroRow('production'),
|
||||||
$this->buildApaczkaRow(),
|
$this->buildApaczkaRow(),
|
||||||
|
$this->buildPolkurierRow(),
|
||||||
$this->buildInpostRow(),
|
$this->buildInpostRow(),
|
||||||
$this->buildShopproRow(),
|
$this->buildShopproRow(),
|
||||||
$this->buildFakturowniaRow(),
|
$this->buildFakturowniaRow(),
|
||||||
@@ -226,6 +228,30 @@ final class IntegrationsHubController
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
private function buildPolkurierRow(): array
|
||||||
|
{
|
||||||
|
$settings = $this->polkurier->getSettings();
|
||||||
|
$isConfigured = !empty($settings['login']) && !empty($settings['has_api_token']);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'provider' => $this->translator->get('settings.integrations_hub.providers.polkurier'),
|
||||||
|
'instance' => 'polkurier.pl',
|
||||||
|
'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_token'])
|
||||||
|
? $this->translator->get('settings.integrations_hub.status.saved')
|
||||||
|
: $this->translator->get('settings.integrations_hub.status.missing'),
|
||||||
|
'is_active' => !empty($settings['is_active']),
|
||||||
|
'last_test_at' => trim((string) ($settings['last_test_at'] ?? '')),
|
||||||
|
'configure_url' => '/settings/integrations/polkurier',
|
||||||
|
'configure_label' => $this->translator->get('settings.integrations_hub.actions.configure'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array<string, mixed>
|
* @return array<string, mixed>
|
||||||
*/
|
*/
|
||||||
|
|||||||
360
src/Modules/Settings/PolkurierApiClient.php
Normal file
360
src/Modules/Settings/PolkurierApiClient.php
Normal file
@@ -0,0 +1,360 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Modules\Settings;
|
||||||
|
|
||||||
|
use App\Core\Http\SslCertificateResolver;
|
||||||
|
use RuntimeException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* polkurier.pl Web Service API client.
|
||||||
|
*
|
||||||
|
* Kontrakt API zweryfikowany na podstawie oficjalnego SDK (https://github.com/Polkurier/polkurier-sdk):
|
||||||
|
* - Base URL: https://api.polkurier.pl/ (single endpoint, brak path per metoda)
|
||||||
|
* - HTTP POST, Content-Type: application/json (DOKLADNIE bez parametru charset)
|
||||||
|
* - Body: {"authorization": {"login", "token"}, "apimetod": "<method_name>", "data": {...}}
|
||||||
|
* - Sukces: top-level "status" === "success", tresc odpowiedzi w polu "response"
|
||||||
|
* - Blad: top-level "status" !== "success"; tresc bledu w polu "response" (string lub tablica)
|
||||||
|
*
|
||||||
|
* Phase 127: testConnection (apimetod=test_auth_api) - zweryfikowany na zywym koncie.
|
||||||
|
* Phase 128: createShipment/getLabel/getStatus/cancelOrder/getAvailableCarriers/getParcelMachines.
|
||||||
|
*/
|
||||||
|
final class PolkurierApiClient
|
||||||
|
{
|
||||||
|
private const API_URL = 'https://api.polkurier.pl/';
|
||||||
|
private const PLATFORM = 'orderPRO';
|
||||||
|
private const PLATFORM_VERSION = '1.0';
|
||||||
|
|
||||||
|
public function __construct(private readonly int $timeoutSeconds = 30)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{ok: bool, http_code: int, message: string}
|
||||||
|
*/
|
||||||
|
public function testConnection(string $login, string $apiToken): array
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$response = $this->call('test_auth_api', [
|
||||||
|
'platform' => self::PLATFORM,
|
||||||
|
'platform_version' => self::PLATFORM_VERSION,
|
||||||
|
], $login, $apiToken);
|
||||||
|
} catch (RuntimeException $exception) {
|
||||||
|
return [
|
||||||
|
'ok' => false,
|
||||||
|
'http_code' => $this->lastHttpCode,
|
||||||
|
'message' => $exception->getMessage(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$authorization = '';
|
||||||
|
if (is_array($response) && isset($response['authorization'])) {
|
||||||
|
$authorization = trim((string) $response['authorization']);
|
||||||
|
}
|
||||||
|
$message = $authorization !== ''
|
||||||
|
? 'Autoryzacja: ' . $authorization
|
||||||
|
: 'Polaczenie OK (HTTP ' . $this->lastHttpCode . ').';
|
||||||
|
|
||||||
|
return [
|
||||||
|
'ok' => true,
|
||||||
|
'http_code' => $this->lastHttpCode,
|
||||||
|
'message' => $message,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zwraca liste dostepnych przewoznikow z konta polkurier.
|
||||||
|
* apimetod=available_carriers; brak wymaganych parametrow.
|
||||||
|
*
|
||||||
|
* @return array<int, array<string, mixed>> Lista przewoznikow z polami:
|
||||||
|
* - servicecode (string) - kod uslugi do uzycia w 'courier' przy create_order
|
||||||
|
* - name (string) - czytelna nazwa
|
||||||
|
* - additional_data (array) - dodatkowe wymagania
|
||||||
|
* - foreign_shipments (bool)
|
||||||
|
*/
|
||||||
|
public function getAvailableCarriers(string $login, string $apiToken): array
|
||||||
|
{
|
||||||
|
$response = $this->call('available_carriers', [], $login, $apiToken);
|
||||||
|
|
||||||
|
if (is_array($response)) {
|
||||||
|
// Polkurier moze zwracac liste bezposrednio jako array lub pod kluczem 'carriers'
|
||||||
|
if (isset($response[0]) && is_array($response[0])) {
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
if (isset($response['carriers']) && is_array($response['carriers'])) {
|
||||||
|
return $response['carriers'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tworzy zamowienie (paczke) w polkurier.
|
||||||
|
* apimetod=create_order.
|
||||||
|
*
|
||||||
|
* @param array<string, mixed> $payload Struktura zgodna z SDK polkurier-sdk/CreateOrder:
|
||||||
|
* - shipmenttype (string, np. 'BOX')
|
||||||
|
* - courier (string, servicecode z available_carriers)
|
||||||
|
* - description (string)
|
||||||
|
* - sender (array: company, person, street, housenumber, flatnumber, postcode, city, email, phone, country, point_id)
|
||||||
|
* - recipient (array: same shape as sender)
|
||||||
|
* - packs (array of {length, width, height, weight, amount, type})
|
||||||
|
* - pickup (array: pickupdate, pickuptimefrom, pickuptimeto, nocourierorder)
|
||||||
|
* - COD (array: codtype, codamount, codbankaccount, return_cod) - optional
|
||||||
|
* - insurance (float) - optional
|
||||||
|
* - courierservice (array) - optional additional services
|
||||||
|
*
|
||||||
|
* @return array<string, mixed> Tresc z pola 'response' API. Typowe pola: 'orderno', 'cost', etc.
|
||||||
|
*/
|
||||||
|
public function createShipment(string $login, string $apiToken, array $payload): array
|
||||||
|
{
|
||||||
|
$response = $this->call('create_order', $payload, $login, $apiToken);
|
||||||
|
return is_array($response) ? $response : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pobiera etykiete dla zamowienia.
|
||||||
|
* apimetod=get_label; wymagane: orderno (string lub array).
|
||||||
|
*
|
||||||
|
* @return array<string, mixed> Tresc z 'response'. Etykieta zwykle base64 w polu 'label' albo 'pdf'.
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* Pobiera etykiete dla zamowienia.
|
||||||
|
* API polkurier (apimetod=get_label) przyjmuje wylacznie `orderno: Array<String>`.
|
||||||
|
* Rozmiar etykiety (A4 vs A6) jest sterowany w panelu klienta polkurier.pl
|
||||||
|
* (Ustawienia konta -> Preferencje etykiet), nie przez parametry API.
|
||||||
|
*
|
||||||
|
* @return array<string, mixed> Tresc z 'response'. Etykieta base64 w polu 'file'.
|
||||||
|
*/
|
||||||
|
public function getLabel(string $login, string $apiToken, string $orderno): array
|
||||||
|
{
|
||||||
|
$response = $this->call('get_label', [
|
||||||
|
'orderno' => [$orderno],
|
||||||
|
], $login, $apiToken);
|
||||||
|
|
||||||
|
return is_array($response) ? $response : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pobiera status zamowienia.
|
||||||
|
* apimetod=get_status; wymagane: orderno.
|
||||||
|
*
|
||||||
|
* @return array<string, mixed> Tresc z 'response'. Pola: status, statuscode, statusdate, deliverydate, url.
|
||||||
|
*/
|
||||||
|
public function getStatus(string $login, string $apiToken, string $orderno): array
|
||||||
|
{
|
||||||
|
$response = $this->call('get_status', [
|
||||||
|
'orderno' => $orderno,
|
||||||
|
], $login, $apiToken);
|
||||||
|
|
||||||
|
return is_array($response) ? $response : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Anuluje zamowienie.
|
||||||
|
* apimetod=cancel_order; wymagane: orderno.
|
||||||
|
*
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function cancelOrder(string $login, string $apiToken, string $orderno): array
|
||||||
|
{
|
||||||
|
$response = $this->call('cancel_order', [
|
||||||
|
'orderno' => $orderno,
|
||||||
|
], $login, $apiToken);
|
||||||
|
|
||||||
|
return is_array($response) ? $response : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pobiera liste paczkomatow InPost.
|
||||||
|
* apimetod=inpost_parcel_machines (deprecated wg SDK, ale nadal dziala — alternatywa: get_courier_point).
|
||||||
|
*
|
||||||
|
* @return array<int, array<string, mixed>>
|
||||||
|
*/
|
||||||
|
public function getInpostParcelMachines(string $login, string $apiToken, bool $codAvailable = false): array
|
||||||
|
{
|
||||||
|
$response = $this->call('inpost_parcel_machines', [
|
||||||
|
'cod_available' => $codAvailable,
|
||||||
|
'parcel_send' => false,
|
||||||
|
], $login, $apiToken);
|
||||||
|
|
||||||
|
if (is_array($response)) {
|
||||||
|
if (isset($response[0]) && is_array($response[0])) {
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
if (isset($response['machines']) && is_array($response['machines'])) {
|
||||||
|
return $response['machines'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generyczny lookup punktow odbioru per courier.
|
||||||
|
* apimetod=get_courier_point (SDK nowsze API).
|
||||||
|
*
|
||||||
|
* @return array<int, array<string, mixed>>
|
||||||
|
*/
|
||||||
|
public function getCourierPoints(string $login, string $apiToken, string $courier, ?string $postcode = null): array
|
||||||
|
{
|
||||||
|
$data = ['courier' => $courier];
|
||||||
|
if ($postcode !== null && $postcode !== '') {
|
||||||
|
$data['postcode'] = $postcode;
|
||||||
|
}
|
||||||
|
$response = $this->call('get_courier_point', $data, $login, $apiToken);
|
||||||
|
|
||||||
|
if (is_array($response)) {
|
||||||
|
if (isset($response[0]) && is_array($response[0])) {
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
if (isset($response['points']) && is_array($response['points'])) {
|
||||||
|
return $response['points'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wspolny wrapper dla wszystkich apimetod.
|
||||||
|
* Sukces -> zwraca tresc pola 'response'. Blad -> rzuca RuntimeException z trescia z 'response'.
|
||||||
|
*
|
||||||
|
* @param array<string, mixed> $data
|
||||||
|
* @return mixed Tresc pola 'response' z API.
|
||||||
|
*/
|
||||||
|
private function call(string $apimetod, array $data, string $login, string $apiToken): mixed
|
||||||
|
{
|
||||||
|
$payload = [
|
||||||
|
'authorization' => [
|
||||||
|
'login' => trim($login),
|
||||||
|
'token' => trim($apiToken),
|
||||||
|
],
|
||||||
|
'apimetod' => $apimetod,
|
||||||
|
'data' => $data,
|
||||||
|
];
|
||||||
|
|
||||||
|
[$body, $httpCode, $curlError] = $this->postJson($payload);
|
||||||
|
$this->lastHttpCode = $httpCode;
|
||||||
|
|
||||||
|
if ($curlError !== null) {
|
||||||
|
throw new RuntimeException('Blad polaczenia z polkurier: ' . $curlError);
|
||||||
|
}
|
||||||
|
|
||||||
|
$decoded = json_decode(ltrim($body, "\xEF\xBB\xBF \t\n\r\0\x0B"), true);
|
||||||
|
if (!is_array($decoded)) {
|
||||||
|
$snippet = substr(trim(strip_tags($body)), 0, 240);
|
||||||
|
throw new RuntimeException('Niepoprawna odpowiedz JSON polkurier (HTTP ' . $httpCode . '): ' . $snippet);
|
||||||
|
}
|
||||||
|
|
||||||
|
$status = strtolower(trim((string) ($decoded['status'] ?? '')));
|
||||||
|
$responseField = $decoded['response'] ?? null;
|
||||||
|
|
||||||
|
if ($status === 'success' && $httpCode >= 200 && $httpCode < 300) {
|
||||||
|
return $responseField;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error path
|
||||||
|
$errorMessage = $this->extractErrorMessage($responseField, $decoded, $status, $httpCode);
|
||||||
|
throw new RuntimeException($errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int $lastHttpCode = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param mixed $responseField
|
||||||
|
* @param array<string, mixed> $decoded
|
||||||
|
*/
|
||||||
|
private function extractErrorMessage(mixed $responseField, array $decoded, string $status, int $httpCode): string
|
||||||
|
{
|
||||||
|
if (is_string($responseField)) {
|
||||||
|
$msg = trim($responseField);
|
||||||
|
if ($msg !== '') {
|
||||||
|
return $msg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (is_array($responseField)) {
|
||||||
|
$msg = trim((string) (
|
||||||
|
$responseField['error_message']
|
||||||
|
?? $responseField['errorMessage']
|
||||||
|
?? $responseField['message']
|
||||||
|
?? $responseField['error']
|
||||||
|
?? ''
|
||||||
|
));
|
||||||
|
if ($msg === '') {
|
||||||
|
$jsonDump = json_encode($responseField, JSON_UNESCAPED_UNICODE);
|
||||||
|
if (is_string($jsonDump)) {
|
||||||
|
$msg = substr($jsonDump, 0, 280);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($msg !== '') {
|
||||||
|
return $msg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$msg = trim((string) (
|
||||||
|
$decoded['error_message']
|
||||||
|
?? $decoded['errorMessage']
|
||||||
|
?? $decoded['message']
|
||||||
|
?? $decoded['error']
|
||||||
|
?? ''
|
||||||
|
));
|
||||||
|
if ($msg !== '') {
|
||||||
|
return $msg;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $status !== ''
|
||||||
|
? 'polkurier status: ' . $status . ' (HTTP ' . $httpCode . ')'
|
||||||
|
: 'polkurier HTTP ' . $httpCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $payload
|
||||||
|
* @return array{0: string, 1: int, 2: ?string}
|
||||||
|
*/
|
||||||
|
private function postJson(array $payload): array
|
||||||
|
{
|
||||||
|
$ch = curl_init(self::API_URL);
|
||||||
|
if ($ch === false) {
|
||||||
|
return ['', 0, 'Nie udalo sie zainicjowac cURL.'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$encoded = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||||
|
if ($encoded === false) {
|
||||||
|
return ['', 0, 'Nie udalo sie zakodowac payloadu JSON.'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$options = [
|
||||||
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
|
CURLOPT_POST => true,
|
||||||
|
CURLOPT_POSTFIELDS => $encoded,
|
||||||
|
CURLOPT_TIMEOUT => $this->timeoutSeconds,
|
||||||
|
CURLOPT_CONNECTTIMEOUT => 10,
|
||||||
|
CURLOPT_SSL_VERIFYPEER => true,
|
||||||
|
CURLOPT_SSL_VERIFYHOST => 2,
|
||||||
|
CURLOPT_HTTPHEADER => [
|
||||||
|
'Accept: application/json',
|
||||||
|
'Content-Type: application/json',
|
||||||
|
'User-Agent: orderPRO/1.0',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$caPath = SslCertificateResolver::resolve();
|
||||||
|
if ($caPath !== null) {
|
||||||
|
$options[CURLOPT_CAINFO] = $caPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
curl_setopt_array($ch, $options);
|
||||||
|
$rawBody = curl_exec($ch);
|
||||||
|
$httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
$curlError = curl_error($ch);
|
||||||
|
unset($ch);
|
||||||
|
|
||||||
|
if ($rawBody === false) {
|
||||||
|
return ['', $httpCode, $curlError !== '' ? $curlError : 'Brak odpowiedzi z API.'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [(string) $rawBody, $httpCode, null];
|
||||||
|
}
|
||||||
|
}
|
||||||
123
src/Modules/Settings/PolkurierIntegrationController.php
Normal file
123
src/Modules/Settings/PolkurierIntegrationController.php
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Modules\Settings;
|
||||||
|
|
||||||
|
use App\Core\Exceptions\IntegrationConfigException;
|
||||||
|
use App\Core\Http\RedirectPathResolver;
|
||||||
|
use App\Core\Http\Request;
|
||||||
|
use App\Core\Http\Response;
|
||||||
|
use App\Core\I18n\Translator;
|
||||||
|
use App\Core\Security\Csrf;
|
||||||
|
use App\Core\Support\Flash;
|
||||||
|
use App\Core\View\Template;
|
||||||
|
use App\Modules\Auth\AuthService;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
final class PolkurierIntegrationController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly Template $template,
|
||||||
|
private readonly Translator $translator,
|
||||||
|
private readonly AuthService $auth,
|
||||||
|
private readonly PolkurierIntegrationRepository $repository,
|
||||||
|
private readonly PolkurierApiClient $apiClient,
|
||||||
|
private readonly IntegrationsRepository $integrations
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function index(Request $request): Response
|
||||||
|
{
|
||||||
|
$html = $this->template->render('settings/polkurier', [
|
||||||
|
'title' => $this->translator->get('settings.polkurier.title'),
|
||||||
|
'activeMenu' => 'settings',
|
||||||
|
'activeSettings' => 'integrations',
|
||||||
|
'user' => $this->auth->user(),
|
||||||
|
'csrfToken' => Csrf::token(),
|
||||||
|
'settings' => $this->repository->getSettings(),
|
||||||
|
'errorMessage' => (string) Flash::get('settings_error', ''),
|
||||||
|
'successMessage' => (string) Flash::get('settings_success', ''),
|
||||||
|
'testMessage' => (string) Flash::get('polkurier_test', ''),
|
||||||
|
], 'layouts/app');
|
||||||
|
|
||||||
|
return Response::html($html);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function save(Request $request): Response
|
||||||
|
{
|
||||||
|
$redirectTo = $this->resolveRedirect($request);
|
||||||
|
|
||||||
|
if (!Csrf::validate((string) $request->input('_token', ''))) {
|
||||||
|
Flash::set('settings_error', $this->translator->get('auth.errors.csrf_expired'));
|
||||||
|
return Response::redirect($redirectTo);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->repository->saveSettings([
|
||||||
|
'login' => (string) $request->input('login', ''),
|
||||||
|
'api_token' => (string) $request->input('api_token', ''),
|
||||||
|
'default_label_format' => (string) $request->input('default_label_format', 'PDF'),
|
||||||
|
'is_active' => $request->input('is_active', ''),
|
||||||
|
]);
|
||||||
|
Flash::set('settings_success', $this->translator->get('settings.polkurier.flash.saved'));
|
||||||
|
} catch (Throwable $exception) {
|
||||||
|
Flash::set(
|
||||||
|
'settings_error',
|
||||||
|
$this->translator->get('settings.polkurier.flash.save_failed') . ' ' . $exception->getMessage()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response::redirect($redirectTo);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test(Request $request): Response
|
||||||
|
{
|
||||||
|
$redirectTo = $this->resolveRedirect($request);
|
||||||
|
|
||||||
|
if (!Csrf::validate((string) $request->input('_token', ''))) {
|
||||||
|
Flash::set('settings_error', $this->translator->get('auth.errors.csrf_expired'));
|
||||||
|
return Response::redirect($redirectTo);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$credentials = $this->repository->getCredentials();
|
||||||
|
if ($credentials === null) {
|
||||||
|
throw new IntegrationConfigException('Najpierw zapisz kompletna konfiguracje polkurier (login + Token API + aktywacja).');
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $this->apiClient->testConnection(
|
||||||
|
$credentials['login'],
|
||||||
|
$credentials['api_token']
|
||||||
|
);
|
||||||
|
|
||||||
|
$status = $result['ok'] ? 'ok' : 'fail';
|
||||||
|
$this->integrations->updateTestResult(
|
||||||
|
$credentials['integration_id'],
|
||||||
|
$status,
|
||||||
|
(int) $result['http_code'],
|
||||||
|
(string) $result['message']
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($result['ok']) {
|
||||||
|
Flash::set('polkurier_test', $this->translator->get('settings.polkurier.flash.test_success', [
|
||||||
|
'message' => (string) $result['message'],
|
||||||
|
]));
|
||||||
|
} else {
|
||||||
|
Flash::set('settings_error', $this->translator->get('settings.polkurier.flash.test_failed') . ' ' . $result['message']);
|
||||||
|
}
|
||||||
|
} catch (Throwable $exception) {
|
||||||
|
Flash::set('settings_error', $this->translator->get('settings.polkurier.flash.test_failed') . ' ' . $exception->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response::redirect($redirectTo);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveRedirect(Request $request): string
|
||||||
|
{
|
||||||
|
return RedirectPathResolver::resolve(
|
||||||
|
(string) $request->input('return_to', '/settings/integrations/polkurier'),
|
||||||
|
['/settings/integrations'],
|
||||||
|
'/settings/integrations/polkurier'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
218
src/Modules/Settings/PolkurierIntegrationRepository.php
Normal file
218
src/Modules/Settings/PolkurierIntegrationRepository.php
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Modules\Settings;
|
||||||
|
|
||||||
|
use App\Core\Exceptions\IntegrationConfigException;
|
||||||
|
use App\Core\Support\StringHelper;
|
||||||
|
use PDO;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
final class PolkurierIntegrationRepository
|
||||||
|
{
|
||||||
|
private const INTEGRATION_TYPE = 'polkurier';
|
||||||
|
private const INTEGRATION_NAME = 'polkurier';
|
||||||
|
private const INTEGRATION_BASE_URL = 'https://api.polkurier.pl/';
|
||||||
|
|
||||||
|
private readonly IntegrationsRepository $integrations;
|
||||||
|
private readonly IntegrationSecretCipher $cipher;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private readonly PDO $pdo,
|
||||||
|
private readonly string $secret
|
||||||
|
) {
|
||||||
|
$this->integrations = new IntegrationsRepository($this->pdo);
|
||||||
|
$this->cipher = new IntegrationSecretCipher($this->secret);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function getSettings(): array
|
||||||
|
{
|
||||||
|
$this->ensureRow();
|
||||||
|
$integrationId = $this->ensureBaseIntegration();
|
||||||
|
$row = $this->fetchRow();
|
||||||
|
$integration = $this->integrations->findById($integrationId);
|
||||||
|
$tokenEncrypted = $this->resolveTokenEncrypted($row, $integration);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'integration_id' => $integrationId,
|
||||||
|
'login' => trim((string) ($row['login'] ?? '')),
|
||||||
|
'default_label_format' => trim((string) ($row['default_label_format'] ?? 'PDF')) ?: 'PDF',
|
||||||
|
'has_api_token' => $tokenEncrypted !== null && $tokenEncrypted !== '',
|
||||||
|
'is_active' => (int) ($integration['is_active'] ?? 1) === 1,
|
||||||
|
'last_test_status' => trim((string) ($integration['last_test_status'] ?? '')),
|
||||||
|
'last_test_http_code' => isset($integration['last_test_http_code']) ? (int) $integration['last_test_http_code'] : null,
|
||||||
|
'last_test_message' => trim((string) ($integration['last_test_message'] ?? '')),
|
||||||
|
'last_test_at' => trim((string) ($integration['last_test_at'] ?? '')),
|
||||||
|
'updated_at' => trim((string) ($row['updated_at'] ?? '')),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $payload
|
||||||
|
*/
|
||||||
|
public function saveSettings(array $payload): void
|
||||||
|
{
|
||||||
|
$this->ensureRow();
|
||||||
|
$integrationId = $this->ensureBaseIntegration();
|
||||||
|
$row = $this->fetchRow();
|
||||||
|
if ($row === null) {
|
||||||
|
throw new IntegrationConfigException('Brak rekordu konfiguracji polkurier.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$login = trim((string) ($payload['login'] ?? ''));
|
||||||
|
if ($login === '' || strlen($login) > 190) {
|
||||||
|
throw new IntegrationConfigException('Podaj login polkurier (e-mail lub identyfikator z panelu klienta, maks. 190 znakow).');
|
||||||
|
}
|
||||||
|
|
||||||
|
$labelFormatRaw = strtoupper(trim((string) ($payload['default_label_format'] ?? 'PDF')));
|
||||||
|
$allowedFormats = ['PDF', 'ZPL', 'EPL'];
|
||||||
|
$labelFormat = in_array($labelFormatRaw, $allowedFormats, true) ? $labelFormatRaw : 'PDF';
|
||||||
|
|
||||||
|
$currentEncrypted = $this->resolveTokenEncrypted($row, $this->integrations->findById($integrationId));
|
||||||
|
$token = trim((string) ($payload['api_token'] ?? ''));
|
||||||
|
$nextEncrypted = $currentEncrypted;
|
||||||
|
if ($token !== '') {
|
||||||
|
$nextEncrypted = $this->cipher->encrypt($token);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($nextEncrypted === null || $nextEncrypted === '') {
|
||||||
|
throw new IntegrationConfigException('Podaj Token API polkurier (z Panel Klienta -> Ustawienia -> Token API).');
|
||||||
|
}
|
||||||
|
|
||||||
|
$statement = $this->pdo->prepare(
|
||||||
|
'UPDATE polkurier_integration_settings
|
||||||
|
SET login = :login,
|
||||||
|
api_token_encrypted = :api_token_encrypted,
|
||||||
|
default_label_format = :default_label_format,
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE id = 1'
|
||||||
|
);
|
||||||
|
$statement->execute([
|
||||||
|
'login' => $login,
|
||||||
|
'api_token_encrypted' => $nextEncrypted,
|
||||||
|
'default_label_format' => $labelFormat,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->updateIntegrationActive($integrationId, !empty($payload['is_active']));
|
||||||
|
$this->integrations->updateApiKeyEncrypted($integrationId, $nextEncrypted);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{integration_id: int, login: string, api_token: string, default_label_format: string}|null
|
||||||
|
*/
|
||||||
|
public function getCredentials(): ?array
|
||||||
|
{
|
||||||
|
$this->ensureRow();
|
||||||
|
$integrationId = $this->ensureBaseIntegration();
|
||||||
|
$row = $this->fetchRow();
|
||||||
|
if ($row === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$integration = $this->integrations->findById($integrationId);
|
||||||
|
if (empty($integration) || (int) ($integration['is_active'] ?? 0) !== 1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$login = trim((string) ($row['login'] ?? ''));
|
||||||
|
$encrypted = $this->resolveTokenEncrypted($row, $integration);
|
||||||
|
|
||||||
|
if ($login === '' || $encrypted === null || $encrypted === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$token = trim((string) $this->cipher->decrypt($encrypted));
|
||||||
|
if ($token === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'integration_id' => $integrationId,
|
||||||
|
'login' => $login,
|
||||||
|
'api_token' => $token,
|
||||||
|
'default_label_format' => trim((string) ($row['default_label_format'] ?? 'PDF')) ?: 'PDF',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getIntegrationId(): ?int
|
||||||
|
{
|
||||||
|
$integration = $this->integrations->findByTypeAndName(self::INTEGRATION_TYPE, self::INTEGRATION_NAME);
|
||||||
|
if ($integration === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$id = (int) ($integration['id'] ?? 0);
|
||||||
|
return $id > 0 ? $id : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function ensureBaseIntegration(): int
|
||||||
|
{
|
||||||
|
return $this->integrations->ensureIntegration(
|
||||||
|
self::INTEGRATION_TYPE,
|
||||||
|
self::INTEGRATION_NAME,
|
||||||
|
self::INTEGRATION_BASE_URL,
|
||||||
|
15,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function ensureRow(): void
|
||||||
|
{
|
||||||
|
$integrationId = $this->ensureBaseIntegration();
|
||||||
|
$statement = $this->pdo->prepare(
|
||||||
|
'INSERT INTO polkurier_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)'
|
||||||
|
);
|
||||||
|
$statement->execute(['integration_id' => $integrationId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>|null
|
||||||
|
*/
|
||||||
|
private function fetchRow(): ?array
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$statement = $this->pdo->prepare('SELECT * FROM polkurier_integration_settings WHERE id = 1 LIMIT 1');
|
||||||
|
$statement->execute();
|
||||||
|
$row = $statement->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 resolveTokenEncrypted(?array $row, ?array $integration): ?string
|
||||||
|
{
|
||||||
|
$settingsValue = trim((string) ($row['api_token_encrypted'] ?? ''));
|
||||||
|
if ($settingsValue !== '') {
|
||||||
|
return $settingsValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$baseValue = trim((string) ($integration['api_key_encrypted'] ?? ''));
|
||||||
|
return StringHelper::nullableString($baseValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function updateIntegrationActive(int $integrationId, bool $isActive): void
|
||||||
|
{
|
||||||
|
$statement = $this->pdo->prepare(
|
||||||
|
'UPDATE integrations
|
||||||
|
SET is_active = :is_active,
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE id = :id AND type = :type'
|
||||||
|
);
|
||||||
|
$statement->execute([
|
||||||
|
'id' => $integrationId,
|
||||||
|
'type' => self::INTEGRATION_TYPE,
|
||||||
|
'is_active' => $isActive ? 1 : 0,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -296,11 +296,33 @@ final class DeliveryStatus
|
|||||||
self::PROBLEM,
|
self::PROBLEM,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
private const POLKURIER_MAP = [
|
||||||
|
// Oficjalne kody ORDER_STATUS z dokumentacji polkurier API v1.11 (marzec 2026)
|
||||||
|
'O' => self::CREATED,
|
||||||
|
'P' => self::CONFIRMED,
|
||||||
|
'A' => self::CANCELLED,
|
||||||
|
'WP' => self::IN_TRANSIT,
|
||||||
|
'D' => self::DELIVERED,
|
||||||
|
'Z' => self::RETURNED,
|
||||||
|
'W' => self::PROBLEM,
|
||||||
|
];
|
||||||
|
|
||||||
|
private const POLKURIER_DESCRIPTIONS = [
|
||||||
|
'O' => 'Oczekuje na płatność',
|
||||||
|
'P' => 'Potwierdzone, list wygenerowany',
|
||||||
|
'A' => 'Anulowane',
|
||||||
|
'WP' => 'W przewozie',
|
||||||
|
'D' => 'Dostarczona',
|
||||||
|
'Z' => 'Zwrot do nadawcy',
|
||||||
|
'W' => 'Wyjątek',
|
||||||
|
];
|
||||||
|
|
||||||
private const PROVIDER_MAPS = [
|
private const PROVIDER_MAPS = [
|
||||||
'inpost' => self::INPOST_MAP,
|
'inpost' => self::INPOST_MAP,
|
||||||
'apaczka' => self::APACZKA_MAP,
|
'apaczka' => self::APACZKA_MAP,
|
||||||
'allegro_wza' => self::ALLEGRO_MAP,
|
'allegro_wza' => self::ALLEGRO_MAP,
|
||||||
'allegro_edge' => self::ALLEGRO_EDGE_MAP,
|
'allegro_edge' => self::ALLEGRO_EDGE_MAP,
|
||||||
|
'polkurier' => self::POLKURIER_MAP,
|
||||||
];
|
];
|
||||||
|
|
||||||
private const PROVIDER_DESCRIPTIONS = [
|
private const PROVIDER_DESCRIPTIONS = [
|
||||||
@@ -308,6 +330,7 @@ final class DeliveryStatus
|
|||||||
'apaczka' => self::APACZKA_DESCRIPTIONS,
|
'apaczka' => self::APACZKA_DESCRIPTIONS,
|
||||||
'allegro_wza' => self::ALLEGRO_DESCRIPTIONS,
|
'allegro_wza' => self::ALLEGRO_DESCRIPTIONS,
|
||||||
'allegro_edge' => self::ALLEGRO_EDGE_DESCRIPTIONS,
|
'allegro_edge' => self::ALLEGRO_EDGE_DESCRIPTIONS,
|
||||||
|
'polkurier' => self::POLKURIER_DESCRIPTIONS,
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -362,6 +385,7 @@ final class DeliveryStatus
|
|||||||
'apaczka' => self::APACZKA_MAP,
|
'apaczka' => self::APACZKA_MAP,
|
||||||
'allegro_wza' => self::ALLEGRO_MAP,
|
'allegro_wza' => self::ALLEGRO_MAP,
|
||||||
'allegro_edge' => self::ALLEGRO_EDGE_MAP,
|
'allegro_edge' => self::ALLEGRO_EDGE_MAP,
|
||||||
|
'polkurier' => self::POLKURIER_MAP,
|
||||||
default => [],
|
default => [],
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -375,6 +399,7 @@ final class DeliveryStatus
|
|||||||
'apaczka' => self::APACZKA_DESCRIPTIONS,
|
'apaczka' => self::APACZKA_DESCRIPTIONS,
|
||||||
'allegro_wza' => self::ALLEGRO_DESCRIPTIONS,
|
'allegro_wza' => self::ALLEGRO_DESCRIPTIONS,
|
||||||
'allegro_edge' => self::ALLEGRO_EDGE_DESCRIPTIONS,
|
'allegro_edge' => self::ALLEGRO_EDGE_DESCRIPTIONS,
|
||||||
|
'polkurier' => self::POLKURIER_DESCRIPTIONS,
|
||||||
default => [],
|
default => [],
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -554,6 +579,10 @@ final class DeliveryStatus
|
|||||||
return 'https://allegro.pl/allegrodelivery/sledzenie-paczki?numer=' . $encoded;
|
return 'https://allegro.pl/allegrodelivery/sledzenie-paczki?numer=' . $encoded;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($provider === 'polkurier') {
|
||||||
|
return 'https://polkurier.pl/sledz-paczke/' . $encoded;
|
||||||
|
}
|
||||||
|
|
||||||
return 'https://www.google.com/search?q=' . $encoded . '+sledzenie+przesylki';
|
return 'https://www.google.com/search?q=' . $encoded . '+sledzenie+przesylki';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ final class DeliveryStatusMappingRepository
|
|||||||
return $cached;
|
return $cached;
|
||||||
}
|
}
|
||||||
|
|
||||||
$providers = ['inpost', 'apaczka', 'allegro_wza'];
|
$providers = ['inpost', 'apaczka', 'allegro_wza', 'polkurier'];
|
||||||
$knownKeysByProvider = [];
|
$knownKeysByProvider = [];
|
||||||
foreach ($providers as $prov) {
|
foreach ($providers as $prov) {
|
||||||
$knownKeysByProvider[$prov] = [];
|
$knownKeysByProvider[$prov] = [];
|
||||||
|
|||||||
760
src/Modules/Shipments/PolkurierShipmentService.php
Normal file
760
src/Modules/Shipments/PolkurierShipmentService.php
Normal file
@@ -0,0 +1,760 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Modules\Shipments;
|
||||||
|
|
||||||
|
use App\Core\Exceptions\IntegrationConfigException;
|
||||||
|
use App\Core\Exceptions\ShipmentException;
|
||||||
|
use App\Modules\Orders\OrdersRepository;
|
||||||
|
use App\Modules\Settings\CompanySettingsRepository;
|
||||||
|
use App\Modules\Settings\PolkurierApiClient;
|
||||||
|
use App\Modules\Settings\PolkurierIntegrationRepository;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* polkurier.pl ShipmentProvider (Phase 128).
|
||||||
|
*
|
||||||
|
* Tworzy paczki, pobiera etykiety i wystawia dostepne uslugi przewoznicze przez API polkurier.
|
||||||
|
* Payload zgodny z SDK polkurier-sdk (zweryfikowany na Sender/Recipient/Pack/Pickup/COD entity klasach).
|
||||||
|
*/
|
||||||
|
final class PolkurierShipmentService implements ShipmentProviderInterface
|
||||||
|
{
|
||||||
|
/** @var array<int, array<string, mixed>>|null */
|
||||||
|
private ?array $carriersCache = null;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private readonly PolkurierIntegrationRepository $integrationRepository,
|
||||||
|
private readonly PolkurierApiClient $apiClient,
|
||||||
|
private readonly ShipmentPackageRepository $packages,
|
||||||
|
private readonly CompanySettingsRepository $companySettings,
|
||||||
|
private readonly OrdersRepository $ordersRepository
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function code(): string
|
||||||
|
{
|
||||||
|
return 'polkurier';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int, array<string, mixed>>
|
||||||
|
*/
|
||||||
|
public function getDeliveryServices(): array
|
||||||
|
{
|
||||||
|
if ($this->carriersCache !== null) {
|
||||||
|
return $this->carriersCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
$credentials = $this->integrationRepository->getCredentials();
|
||||||
|
if ($credentials === null) {
|
||||||
|
return $this->carriersCache = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$carriers = $this->apiClient->getAvailableCarriers(
|
||||||
|
$credentials['login'],
|
||||||
|
$credentials['api_token']
|
||||||
|
);
|
||||||
|
} catch (Throwable) {
|
||||||
|
return $this->carriersCache = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalizacja: ujednolicony shape `{id, name, supports_pickup_point, foreign_shipments}`
|
||||||
|
$normalized = [];
|
||||||
|
foreach ($carriers as $carrier) {
|
||||||
|
if (!is_array($carrier)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$code = trim((string) ($carrier['servicecode'] ?? $carrier['code'] ?? ''));
|
||||||
|
$name = trim((string) ($carrier['name'] ?? $code));
|
||||||
|
if ($code === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$supportsPoint = $this->detectPickupPointSupport($code, $carrier);
|
||||||
|
$normalized[] = [
|
||||||
|
'id' => $code,
|
||||||
|
'name' => $name !== '' ? $name : $code,
|
||||||
|
'supports_pickup_point' => $supportsPoint,
|
||||||
|
'point_courier' => $supportsPoint ? $this->resolvePointCourierKey($code) : null,
|
||||||
|
'foreign_shipments' => !empty($carrier['foreign_shipments']),
|
||||||
|
'raw' => $carrier,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
usort($normalized, static fn ($a, $b) => strcasecmp((string) $a['name'], (string) $b['name']));
|
||||||
|
return $this->carriersCache = $normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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 ShipmentException('Zamowienie nie znalezione.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$credentials = $this->requireCredentials();
|
||||||
|
$sender = $this->companySettings->getSenderAddress();
|
||||||
|
$this->validateSender($sender);
|
||||||
|
|
||||||
|
$courierCode = strtoupper(trim((string) (
|
||||||
|
$formData['service_code']
|
||||||
|
?? $formData['delivery_method_id']
|
||||||
|
?? ''
|
||||||
|
)));
|
||||||
|
if ($courierCode === '') {
|
||||||
|
throw new ShipmentException('Nie wybrano uslugi polkurier (servicecode).');
|
||||||
|
}
|
||||||
|
|
||||||
|
$orderData = is_array($order['order'] ?? null) ? $order['order'] : [];
|
||||||
|
$sourceOrderId = trim((string) ($orderData['source_order_id'] ?? ($orderData['external_order_number'] ?? '')));
|
||||||
|
$description = 'orderPRO ' . ($sourceOrderId !== '' ? $sourceOrderId : (string) $orderId);
|
||||||
|
|
||||||
|
$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));
|
||||||
|
$shipmentType = $this->normalizeShipmentType((string) ($formData['package_type'] ?? 'BOX'));
|
||||||
|
|
||||||
|
$receiverPointId = trim((string) ($formData['receiver_point_id'] ?? ''));
|
||||||
|
$recipient = $this->buildRecipient($order, $formData, $receiverPointId);
|
||||||
|
$senderPayload = $this->buildSenderPayload($sender);
|
||||||
|
|
||||||
|
$packs = [[
|
||||||
|
'length' => (int) round($lengthCm),
|
||||||
|
'width' => (int) round($widthCm),
|
||||||
|
'height' => (int) round($heightCm),
|
||||||
|
'weight' => round($weightKg, 3),
|
||||||
|
'amount' => 1,
|
||||||
|
'type' => $shipmentType,
|
||||||
|
]];
|
||||||
|
|
||||||
|
$pickup = $this->buildPickup($formData);
|
||||||
|
|
||||||
|
$apiPayload = [
|
||||||
|
'shipmenttype' => $shipmentType,
|
||||||
|
'courier' => $courierCode,
|
||||||
|
'description' => $description,
|
||||||
|
'sender' => $senderPayload,
|
||||||
|
'recipient' => $recipient,
|
||||||
|
'packs' => $packs,
|
||||||
|
'pickup' => $pickup,
|
||||||
|
];
|
||||||
|
|
||||||
|
$insurance = max(0.0, (float) ($formData['insurance_amount'] ?? 0));
|
||||||
|
if ($insurance > 0) {
|
||||||
|
$apiPayload['insurance'] = round($insurance, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
$cod = max(0.0, (float) ($formData['cod_amount'] ?? 0));
|
||||||
|
if ($cod > 0) {
|
||||||
|
$companySettings = $this->companySettings->getSettings();
|
||||||
|
$bankAccount = preg_replace('/[^0-9]/', '', (string) ($companySettings['bank_account'] ?? '')) ?? '';
|
||||||
|
if ($bankAccount === '') {
|
||||||
|
throw new ShipmentException('Przesylka COD wymaga numeru konta bankowego. Uzupelnij go w Ustawienia > Firma.');
|
||||||
|
}
|
||||||
|
$apiPayload['COD'] = [
|
||||||
|
'codtype' => 'transfer',
|
||||||
|
'codamount' => round($cod, 2),
|
||||||
|
'codbankaccount' => $bankAccount,
|
||||||
|
'return_cod' => 'transfer',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$carrierLabel = $this->resolveCarrierLabel($courierCode);
|
||||||
|
$labelFormat = strtoupper(trim((string) ($formData['label_format'] ?? $credentials['default_label_format'] ?? 'PDF')));
|
||||||
|
if (!in_array($labelFormat, ['PDF', 'ZPL', 'EPL'], true)) {
|
||||||
|
$labelFormat = 'PDF';
|
||||||
|
}
|
||||||
|
|
||||||
|
$packageId = $this->packages->create([
|
||||||
|
'order_id' => $orderId,
|
||||||
|
'provider' => 'polkurier',
|
||||||
|
'delivery_method_id' => $courierCode,
|
||||||
|
'credentials_id' => null,
|
||||||
|
'command_id' => null,
|
||||||
|
'status' => 'pending',
|
||||||
|
'carrier_id' => $carrierLabel,
|
||||||
|
'package_type' => $shipmentType,
|
||||||
|
'weight_kg' => $weightKg,
|
||||||
|
'length_cm' => $lengthCm,
|
||||||
|
'width_cm' => $widthCm,
|
||||||
|
'height_cm' => $heightCm,
|
||||||
|
'insurance_amount' => $insurance > 0 ? $insurance : null,
|
||||||
|
'insurance_currency' => 'PLN',
|
||||||
|
'cod_amount' => $cod > 0 ? $cod : null,
|
||||||
|
'cod_currency' => $cod > 0 ? 'PLN' : null,
|
||||||
|
'label_format' => $labelFormat,
|
||||||
|
'receiver_point_id' => $receiverPointId,
|
||||||
|
'sender_point_id' => trim((string) ($formData['sender_point_id'] ?? '')),
|
||||||
|
'reference_number' => $sourceOrderId !== '' ? $sourceOrderId : (string) $orderId,
|
||||||
|
'payload_json' => $apiPayload,
|
||||||
|
]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$response = $this->apiClient->createShipment(
|
||||||
|
$credentials['login'],
|
||||||
|
$credentials['api_token'],
|
||||||
|
$apiPayload
|
||||||
|
);
|
||||||
|
} catch (Throwable $exception) {
|
||||||
|
$message = 'polkurier create_order: ' . $exception->getMessage();
|
||||||
|
$this->packages->update($packageId, [
|
||||||
|
'status' => 'error',
|
||||||
|
'error_message' => $message,
|
||||||
|
]);
|
||||||
|
throw new ShipmentException($message, 0, $exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
$orderno = $this->extractOrderNumber($response);
|
||||||
|
$tracking = $this->extractTrackingNumber($response, $orderno);
|
||||||
|
|
||||||
|
if ($orderno === '') {
|
||||||
|
// Diagnostyka — polkurier zwrocil odpowiedz ale bez rozpoznawalnego pola order number.
|
||||||
|
// Zapisujemy fragment odpowiedzi do error_message zeby operator/dev zobaczyl shape.
|
||||||
|
$dump = json_encode($response, JSON_UNESCAPED_UNICODE);
|
||||||
|
if (is_string($dump)) {
|
||||||
|
$dump = substr($dump, 0, 400);
|
||||||
|
} else {
|
||||||
|
$dump = '(brak czytelnej odpowiedzi)';
|
||||||
|
}
|
||||||
|
$this->packages->update($packageId, [
|
||||||
|
'status' => 'pending',
|
||||||
|
'error_message' => 'polkurier: utworzono w API ale nie znaleziono order number w odpowiedzi. Fragment: ' . $dump,
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
$this->packages->update($packageId, [
|
||||||
|
'status' => 'created',
|
||||||
|
'shipment_id' => $orderno,
|
||||||
|
'command_id' => $orderno,
|
||||||
|
'tracking_number' => $tracking !== '' ? $tracking : null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sprobuj odrazu pobrac etykiete (synchronously). Niekrytyczne — operator moze pobrac pozniej.
|
||||||
|
if ($orderno !== '') {
|
||||||
|
try {
|
||||||
|
$this->downloadLabel($packageId, $this->resolveStorageRoot());
|
||||||
|
} catch (Throwable) {
|
||||||
|
// ignore — etykieta jeszcze nie gotowa po stronie polkuriera, operator klikni "Pobierz" pozniej
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'package_id' => $packageId,
|
||||||
|
'command_id' => $orderno !== '' ? $orderno : null,
|
||||||
|
'tracking_number' => $tracking,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function checkCreationStatus(int $packageId): array
|
||||||
|
{
|
||||||
|
$package = $this->packages->findById($packageId);
|
||||||
|
if ($package === null) {
|
||||||
|
throw new ShipmentException('Paczka nie znaleziona.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$orderno = trim((string) ($package['shipment_id'] ?? $package['command_id'] ?? ''));
|
||||||
|
if ($orderno === '') {
|
||||||
|
return ['status' => 'in_progress'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$credentials = $this->requireCredentials();
|
||||||
|
try {
|
||||||
|
$statusResp = $this->apiClient->getStatus(
|
||||||
|
$credentials['login'],
|
||||||
|
$credentials['api_token'],
|
||||||
|
$orderno
|
||||||
|
);
|
||||||
|
} catch (Throwable) {
|
||||||
|
return [
|
||||||
|
'status' => 'created',
|
||||||
|
'shipment_id' => $orderno,
|
||||||
|
'tracking_number' => trim((string) ($package['tracking_number'] ?? '')),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'status' => 'created',
|
||||||
|
'shipment_id' => $orderno,
|
||||||
|
'tracking_number' => trim((string) ($package['tracking_number'] ?? '')),
|
||||||
|
'raw_status' => trim((string) ($statusResp['status'] ?? $statusResp['statuscode'] ?? '')),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function downloadLabel(int $packageId, string $storagePath): array
|
||||||
|
{
|
||||||
|
$package = $this->packages->findById($packageId);
|
||||||
|
if ($package === null) {
|
||||||
|
throw new ShipmentException('Paczka nie znaleziona.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$orderno = trim((string) ($package['shipment_id'] ?? $package['command_id'] ?? ''));
|
||||||
|
if ($orderno === '') {
|
||||||
|
throw new ShipmentException('Przesylka polkurier nie zostala jeszcze utworzona.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$credentials = $this->requireCredentials();
|
||||||
|
$format = strtoupper(trim((string) ($package['label_format'] ?? 'PDF')));
|
||||||
|
if (!in_array($format, ['PDF', 'ZPL', 'EPL'], true)) {
|
||||||
|
$format = 'PDF';
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$response = $this->apiClient->getLabel(
|
||||||
|
$credentials['login'],
|
||||||
|
$credentials['api_token'],
|
||||||
|
$orderno
|
||||||
|
);
|
||||||
|
} catch (Throwable $exception) {
|
||||||
|
throw new ShipmentException('polkurier get_label: ' . $exception->getMessage(), 0, $exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
$base64 = $this->extractLabelBase64($response);
|
||||||
|
if ($base64 === '') {
|
||||||
|
throw new ShipmentException('polkurier nie zwrocil danych etykiety.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$binary = base64_decode($base64, true);
|
||||||
|
if (!is_string($binary) || $binary === '') {
|
||||||
|
throw new ShipmentException('Nie mozna odczytac etykiety polkurier.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$extension = strtolower($format);
|
||||||
|
$dir = rtrim($storagePath, '/\\') . '/labels';
|
||||||
|
if (!is_dir($dir)) {
|
||||||
|
mkdir($dir, 0775, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
$filename = 'polkurier_' . $packageId . '_' . preg_replace('/[^A-Za-z0-9_-]/', '', $orderno) . '.' . $extension;
|
||||||
|
$filePath = $dir . '/' . $filename;
|
||||||
|
file_put_contents($filePath, $binary);
|
||||||
|
|
||||||
|
$this->packages->update($packageId, [
|
||||||
|
'status' => 'label_ready',
|
||||||
|
'label_path' => 'labels/' . $filename,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'label_path' => 'labels/' . $filename,
|
||||||
|
'full_path' => $filePath,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* polkurier get_label zwraca base64 PDF pod kluczem 'file' (zweryfikowane w SDK GetLabel.php).
|
||||||
|
*/
|
||||||
|
private function extractLabelBase64(mixed $response): string
|
||||||
|
{
|
||||||
|
if (is_string($response)) {
|
||||||
|
return trim($response);
|
||||||
|
}
|
||||||
|
if (is_array($response)) {
|
||||||
|
foreach (['file', 'label', 'pdf', 'data', 'content', 'zpl', 'epl'] as $key) {
|
||||||
|
if (isset($response[$key]) && is_string($response[$key])) {
|
||||||
|
$candidate = trim((string) $response[$key]);
|
||||||
|
if ($candidate !== '') {
|
||||||
|
return $candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isset($response[0]) && is_array($response[0])) {
|
||||||
|
return $this->extractLabelBase64($response[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* polkurier create_order zwraca Order entity. Numer zamowienia jest w polu 'number'
|
||||||
|
* (zmapowane z setNumber() w SDK Order.php). Fallbacki dla mozliwych wariantow.
|
||||||
|
*
|
||||||
|
* @param array<string, mixed> $response
|
||||||
|
*/
|
||||||
|
private function extractOrderNumber(array $response): string
|
||||||
|
{
|
||||||
|
// Czasami polkurier opakowuje w {order: {...}} lub zwraca liste
|
||||||
|
if (isset($response['order']) && is_array($response['order'])) {
|
||||||
|
$inner = $this->extractOrderNumber($response['order']);
|
||||||
|
if ($inner !== '') {
|
||||||
|
return $inner;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isset($response[0]) && is_array($response[0])) {
|
||||||
|
$inner = $this->extractOrderNumber($response[0]);
|
||||||
|
if ($inner !== '') {
|
||||||
|
return $inner;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (['number', 'orderno', 'order_no', 'order_number', 'order_id', 'id'] as $key) {
|
||||||
|
$value = trim((string) ($response[$key] ?? ''));
|
||||||
|
if ($value !== '') {
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Waybill polkurier zazwyczaj w `waybills[0].number` (OrderWaybill entity).
|
||||||
|
* Fallbacki dla starszych wariantow odpowiedzi.
|
||||||
|
*
|
||||||
|
* @param array<string, mixed> $response
|
||||||
|
*/
|
||||||
|
private function extractTrackingNumber(array $response, string $orderno): string
|
||||||
|
{
|
||||||
|
if (isset($response['order']) && is_array($response['order'])) {
|
||||||
|
$inner = $this->extractTrackingNumber($response['order'], $orderno);
|
||||||
|
if ($inner !== '') {
|
||||||
|
return $inner;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$waybills = $response['waybills'] ?? $response['waybill'] ?? null;
|
||||||
|
if (is_array($waybills)) {
|
||||||
|
// Lista OrderWaybill
|
||||||
|
if (isset($waybills[0]) && is_array($waybills[0])) {
|
||||||
|
foreach (['number', 'waybillno', 'waybill_number', 'tracking_number'] as $key) {
|
||||||
|
$value = trim((string) ($waybills[0][$key] ?? ''));
|
||||||
|
if ($value !== '') {
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Pojedynczy obiekt
|
||||||
|
foreach (['number', 'waybillno', 'waybill_number', 'tracking_number'] as $key) {
|
||||||
|
$value = trim((string) ($waybills[$key] ?? ''));
|
||||||
|
if ($value !== '') {
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (['waybillno', 'waybill_number', 'parcel_number', 'tracking_number'] as $key) {
|
||||||
|
$value = trim((string) ($response[$key] ?? ''));
|
||||||
|
if ($value !== '') {
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $orderno;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveStorageRoot(): string
|
||||||
|
{
|
||||||
|
$root = dirname(__DIR__, 3) . '/storage';
|
||||||
|
return is_dir($root) ? $root : sys_get_temp_dir();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{login: string, api_token: string, default_label_format: string, integration_id: int}
|
||||||
|
*/
|
||||||
|
private function requireCredentials(): array
|
||||||
|
{
|
||||||
|
$credentials = $this->integrationRepository->getCredentials();
|
||||||
|
if ($credentials === null) {
|
||||||
|
throw new IntegrationConfigException('Brak konfiguracji polkurier (login/Token API/aktywnosc).');
|
||||||
|
}
|
||||||
|
return $credentials;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $sender
|
||||||
|
*/
|
||||||
|
private function validateSender(array $sender): void
|
||||||
|
{
|
||||||
|
$required = ['street', 'city', 'postalCode'];
|
||||||
|
foreach ($required as $key) {
|
||||||
|
if (trim((string) ($sender[$key] ?? '')) === '') {
|
||||||
|
throw new ShipmentException('Niekompletne dane nadawcy w Ustawieniach firmy (ulica/miasto/kod pocztowy).');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$name = trim((string) ($sender['name'] ?? ''));
|
||||||
|
$company = trim((string) ($sender['company'] ?? ''));
|
||||||
|
if ($name === '' && $company === '') {
|
||||||
|
throw new ShipmentException('Niekompletne dane nadawcy w Ustawieniach firmy (imie/nazwisko lub nazwa firmy).');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $sender
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
private function buildSenderPayload(array $sender): array
|
||||||
|
{
|
||||||
|
$street = trim((string) ($sender['street'] ?? ''));
|
||||||
|
$parsed = $this->splitStreetAndNumber($street);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'company' => trim((string) ($sender['company'] ?? '')),
|
||||||
|
'person' => trim((string) ($sender['contactPerson'] ?? $sender['name'] ?? '')),
|
||||||
|
'street' => $parsed['street'],
|
||||||
|
'housenumber' => $parsed['house'],
|
||||||
|
'flatnumber' => $parsed['flat'],
|
||||||
|
'postcode' => trim((string) ($sender['postalCode'] ?? '')),
|
||||||
|
'city' => trim((string) ($sender['city'] ?? '')),
|
||||||
|
'email' => trim((string) ($sender['email'] ?? '')),
|
||||||
|
'phone' => $this->normalizePhone((string) ($sender['phone'] ?? '')),
|
||||||
|
'country' => strtoupper(trim((string) ($sender['countryCode'] ?? 'PL'))) ?: 'PL',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $order
|
||||||
|
* @param array<string, mixed> $formData
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
private function buildRecipient(array $order, array $formData, string $receiverPointId): array
|
||||||
|
{
|
||||||
|
$addresses = is_array($order['addresses'] ?? null) ? $order['addresses'] : [];
|
||||||
|
$delivery = [];
|
||||||
|
$customer = [];
|
||||||
|
foreach ($addresses as $addr) {
|
||||||
|
$type = (string) ($addr['address_type'] ?? '');
|
||||||
|
if ($type === 'delivery') {
|
||||||
|
$delivery = is_array($addr) ? $addr : [];
|
||||||
|
} elseif ($type === 'customer') {
|
||||||
|
$customer = is_array($addr) ? $addr : [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$name = $this->firstNonEmpty([
|
||||||
|
$formData['receiver_name'] ?? null,
|
||||||
|
$delivery['name'] ?? null,
|
||||||
|
$customer['name'] ?? null,
|
||||||
|
$delivery['company_name'] ?? null,
|
||||||
|
$customer['company_name'] ?? null,
|
||||||
|
'Klient',
|
||||||
|
]);
|
||||||
|
$company = $this->firstNonEmpty([
|
||||||
|
$formData['receiver_company'] ?? null,
|
||||||
|
$delivery['company_name'] ?? null,
|
||||||
|
$customer['company_name'] ?? null,
|
||||||
|
]);
|
||||||
|
$streetLine = $this->firstNonEmpty([
|
||||||
|
$formData['receiver_street'] ?? null,
|
||||||
|
$this->composeStreet($delivery),
|
||||||
|
$this->composeStreet($customer),
|
||||||
|
]);
|
||||||
|
$parsed = $this->splitStreetAndNumber($streetLine);
|
||||||
|
$postcode = $this->firstNonEmpty([
|
||||||
|
$formData['receiver_postal_code'] ?? null,
|
||||||
|
$delivery['zip_code'] ?? null,
|
||||||
|
$customer['zip_code'] ?? null,
|
||||||
|
]);
|
||||||
|
$city = $this->firstNonEmpty([
|
||||||
|
$formData['receiver_city'] ?? null,
|
||||||
|
$delivery['city'] ?? null,
|
||||||
|
$customer['city'] ?? null,
|
||||||
|
]);
|
||||||
|
$country = strtoupper($this->firstNonEmpty([
|
||||||
|
$formData['receiver_country_code'] ?? null,
|
||||||
|
$delivery['country'] ?? null,
|
||||||
|
$customer['country'] ?? null,
|
||||||
|
'PL',
|
||||||
|
]));
|
||||||
|
$phone = $this->firstNonEmpty([
|
||||||
|
$formData['receiver_phone'] ?? null,
|
||||||
|
$delivery['phone'] ?? null,
|
||||||
|
$customer['phone'] ?? null,
|
||||||
|
]);
|
||||||
|
$email = $this->firstNonEmpty([
|
||||||
|
$formData['receiver_email'] ?? null,
|
||||||
|
$delivery['email'] ?? null,
|
||||||
|
$customer['email'] ?? null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($name === '' || $postcode === '' || $city === '') {
|
||||||
|
throw new ShipmentException('Brak wymaganych danych adresowych odbiorcy (imie/kod pocztowy/miasto).');
|
||||||
|
}
|
||||||
|
if ($receiverPointId === '' && $parsed['street'] === '') {
|
||||||
|
throw new ShipmentException('Brak ulicy odbiorcy (wymagana dla uslug kurierskich).');
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'company' => $company,
|
||||||
|
'person' => $name,
|
||||||
|
'street' => $parsed['street'],
|
||||||
|
'housenumber' => $parsed['house'],
|
||||||
|
'flatnumber' => $parsed['flat'],
|
||||||
|
'postcode' => $postcode,
|
||||||
|
'city' => $city,
|
||||||
|
'email' => $email,
|
||||||
|
'phone' => $this->normalizePhone($phone),
|
||||||
|
'country' => $country !== '' ? $country : 'PL',
|
||||||
|
'point_id' => $receiverPointId,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $formData
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
private function buildPickup(array $formData): array
|
||||||
|
{
|
||||||
|
$date = trim((string) ($formData['pickup_date'] ?? ''));
|
||||||
|
if ($date === '' || preg_match('/^\d{4}-\d{2}-\d{2}$/', $date) !== 1) {
|
||||||
|
$date = $this->nextBusinessDay();
|
||||||
|
}
|
||||||
|
$from = trim((string) ($formData['pickup_time_from'] ?? '10:00'));
|
||||||
|
$to = trim((string) ($formData['pickup_time_to'] ?? '16:00'));
|
||||||
|
$noCourierOrder = !empty($formData['no_courier_order']);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'pickupdate' => $date,
|
||||||
|
'pickuptimefrom' => $from,
|
||||||
|
'pickuptimeto' => $to,
|
||||||
|
'nocourierorder' => $noCourierOrder,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function nextBusinessDay(): string
|
||||||
|
{
|
||||||
|
$ts = time();
|
||||||
|
do {
|
||||||
|
$ts = strtotime('+1 day', $ts);
|
||||||
|
$dow = (int) date('N', $ts ?: time());
|
||||||
|
} while ($dow >= 6);
|
||||||
|
return date('Y-m-d', $ts ?: time());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{street: string, house: string, flat: string}
|
||||||
|
*/
|
||||||
|
private function splitStreetAndNumber(string $streetLine): array
|
||||||
|
{
|
||||||
|
$street = trim($streetLine);
|
||||||
|
if ($street === '') {
|
||||||
|
return ['street' => '', 'house' => '', 'flat' => ''];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wzorce: "Marszalkowska 10/5", "Marszalkowska 10 m. 5", "Marszalkowska 10A"
|
||||||
|
if (preg_match('/^(.*?)\s+(\d+[A-Za-z]?)(?:\s*[\/\-]\s*|\s*m\.?\s*)?(\d+[A-Za-z]?)?$/u', $street, $matches) === 1) {
|
||||||
|
return [
|
||||||
|
'street' => trim((string) $matches[1]),
|
||||||
|
'house' => trim((string) ($matches[2] ?? '')),
|
||||||
|
'flat' => trim((string) ($matches[3] ?? '')),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['street' => $street, 'house' => '', 'flat' => ''];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $address
|
||||||
|
*/
|
||||||
|
private function composeStreet(array $address): string
|
||||||
|
{
|
||||||
|
$street = trim((string) ($address['street_name'] ?? ''));
|
||||||
|
$number = trim((string) ($address['street_number'] ?? ''));
|
||||||
|
if ($street === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return $number !== '' ? trim($street . ' ' . $number) : $street;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<int, mixed> $candidates
|
||||||
|
*/
|
||||||
|
private function firstNonEmpty(array $candidates): string
|
||||||
|
{
|
||||||
|
foreach ($candidates as $candidate) {
|
||||||
|
$value = trim((string) $candidate);
|
||||||
|
if ($value !== '') {
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizePhone(string $phone): string
|
||||||
|
{
|
||||||
|
$digits = preg_replace('/\D+/', '', $phone) ?? '';
|
||||||
|
if ($digits === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
// Polkurier akceptuje cyfry. Usun prefiks 48 jezeli jest podwojny.
|
||||||
|
if (str_starts_with($digits, '48') && strlen($digits) === 11) {
|
||||||
|
$digits = substr($digits, 2);
|
||||||
|
}
|
||||||
|
return $digits;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $carrier
|
||||||
|
*/
|
||||||
|
private function detectPickupPointSupport(string $code, array $carrier): bool
|
||||||
|
{
|
||||||
|
$haystack = strtolower($code . ' ' . trim((string) ($carrier['name'] ?? '')));
|
||||||
|
return str_contains($haystack, 'paczkomat')
|
||||||
|
|| str_contains($haystack, 'parcel')
|
||||||
|
|| str_contains($haystack, 'inpost')
|
||||||
|
|| str_contains($haystack, 'orlen')
|
||||||
|
|| str_contains($haystack, 'pocztex')
|
||||||
|
|| str_contains($haystack, 'kurier48')
|
||||||
|
|| str_contains($haystack, 'punkt');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolvePointCourierKey(string $code): ?string
|
||||||
|
{
|
||||||
|
$lower = strtolower($code);
|
||||||
|
if (str_contains($lower, 'inpost') || str_contains($lower, 'paczkomat')) {
|
||||||
|
return 'inpost';
|
||||||
|
}
|
||||||
|
if (str_contains($lower, 'orlen')) {
|
||||||
|
return 'orlen';
|
||||||
|
}
|
||||||
|
if (str_contains($lower, 'pocztex')) {
|
||||||
|
return 'pocztex';
|
||||||
|
}
|
||||||
|
if (str_contains($lower, 'kurier48')) {
|
||||||
|
return 'kurier48';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* polkurier API wymaga lowercase z dozwolonego zbioru:
|
||||||
|
* [box, envelope, palette, small_parcel, parcel_size_20].
|
||||||
|
* Mapuje istniejace orderPRO wartosci (PACKAGE/BOX/ENVELOPE/...) na format polkurier.
|
||||||
|
*/
|
||||||
|
private function normalizeShipmentType(string $input): string
|
||||||
|
{
|
||||||
|
$raw = strtolower(trim($input));
|
||||||
|
$allowed = ['box', 'envelope', 'palette', 'small_parcel', 'parcel_size_20'];
|
||||||
|
if (in_array($raw, $allowed, true)) {
|
||||||
|
return $raw;
|
||||||
|
}
|
||||||
|
|
||||||
|
$aliases = [
|
||||||
|
'package' => 'box',
|
||||||
|
'parcel' => 'box',
|
||||||
|
'paczka' => 'box',
|
||||||
|
'koperta' => 'envelope',
|
||||||
|
'paleta' => 'palette',
|
||||||
|
'mala_paczka' => 'small_parcel',
|
||||||
|
'small' => 'small_parcel',
|
||||||
|
];
|
||||||
|
return $aliases[$raw] ?? 'box';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveCarrierLabel(string $courierCode): string
|
||||||
|
{
|
||||||
|
foreach ($this->getDeliveryServices() as $service) {
|
||||||
|
if (strcasecmp((string) ($service['id'] ?? ''), $courierCode) === 0) {
|
||||||
|
return (string) ($service['name'] ?? $courierCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $courierCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
120
src/Modules/Shipments/PolkurierTrackingService.php
Normal file
120
src/Modules/Shipments/PolkurierTrackingService.php
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Modules\Shipments;
|
||||||
|
|
||||||
|
use App\Modules\Settings\PolkurierApiClient;
|
||||||
|
use App\Modules\Settings\PolkurierIntegrationRepository;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* polkurier.pl Tracking service (Phase 128).
|
||||||
|
*
|
||||||
|
* Cron pinguje API polkurier po status zamowienia (apimetod=get_status) i zwraca dane do
|
||||||
|
* `ShipmentTrackingHandler`, ktory zapisuje znormalizowany status do `shipment_packages.delivery_status`
|
||||||
|
* przez `delivery_status_mappings(provider='polkurier')`. Wpisy mappings sa seedowane w migracji Phase 128
|
||||||
|
* po obserwacji realnych statusow z live testu na #114/#115.
|
||||||
|
*/
|
||||||
|
final class PolkurierTrackingService implements ShipmentTrackingInterface
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly PolkurierApiClient $apiClient,
|
||||||
|
private readonly PolkurierIntegrationRepository $integrationRepository,
|
||||||
|
private readonly DeliveryStatusMappingRepository $mappingRepository
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function supports(string $provider): bool
|
||||||
|
{
|
||||||
|
return strtolower(trim($provider)) === 'polkurier';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $package
|
||||||
|
* @return array{status: string, status_raw: string, description: string}|null
|
||||||
|
*/
|
||||||
|
public function getDeliveryStatus(array $package): ?array
|
||||||
|
{
|
||||||
|
$orderno = trim((string) ($package['shipment_id'] ?? $package['command_id'] ?? ''));
|
||||||
|
if ($orderno === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$credentials = $this->resolveCredentials();
|
||||||
|
if ($credentials === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$response = $this->apiClient->getStatus($credentials['login'], $credentials['api_token'], $orderno);
|
||||||
|
} catch (Throwable) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$rawStatus = $this->extractRawStatus($response);
|
||||||
|
$statusName = trim((string) (
|
||||||
|
$response['status']
|
||||||
|
?? $response['status_name']
|
||||||
|
?? $response['statusname']
|
||||||
|
?? $rawStatus
|
||||||
|
));
|
||||||
|
|
||||||
|
if ($rawStatus === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$normalized = $this->normalizeStatus($rawStatus);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'status' => $normalized,
|
||||||
|
'status_raw' => $rawStatus,
|
||||||
|
'description' => $statusName !== '' ? $statusName : $rawStatus,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed>|mixed $response
|
||||||
|
*/
|
||||||
|
private function extractRawStatus(mixed $response): string
|
||||||
|
{
|
||||||
|
if (!is_array($response)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Polkurier potrafi zwracac liste statusow (per orderno z get_label-style query)
|
||||||
|
if (isset($response[0]) && is_array($response[0])) {
|
||||||
|
$response = $response[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
$candidate = trim((string) (
|
||||||
|
$response['statuscode']
|
||||||
|
?? $response['status_code']
|
||||||
|
?? $response['statusCode']
|
||||||
|
?? $response['status']
|
||||||
|
?? ''
|
||||||
|
));
|
||||||
|
return $candidate;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizeStatus(string $rawStatus): string
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$overrides = $this->mappingRepository->getAllOverrides();
|
||||||
|
} catch (Throwable) {
|
||||||
|
$overrides = [];
|
||||||
|
}
|
||||||
|
return DeliveryStatus::normalizeWithOverrides('polkurier', $rawStatus, $overrides);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{login: string, api_token: string, default_label_format: string, integration_id: int}|null
|
||||||
|
*/
|
||||||
|
private function resolveCredentials(): ?array
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
return $this->integrationRepository->getCredentials();
|
||||||
|
} catch (Throwable) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -102,6 +102,19 @@ final class ShipmentController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$polkurierServices = [];
|
||||||
|
$polkurierProvider = $this->providerRegistry->get('polkurier');
|
||||||
|
if ($polkurierProvider !== null) {
|
||||||
|
try {
|
||||||
|
$polkurierServices = $polkurierProvider->getDeliveryServices();
|
||||||
|
} catch (Throwable $exception) {
|
||||||
|
if ($deliveryServicesError === '') {
|
||||||
|
$deliveryServicesError = $exception->getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ($inpostServices === []) {
|
if ($inpostServices === []) {
|
||||||
$inpostServices = array_values(array_filter(
|
$inpostServices = array_values(array_filter(
|
||||||
$deliveryServices,
|
$deliveryServices,
|
||||||
@@ -157,6 +170,7 @@ final class ShipmentController
|
|||||||
'company' => $company,
|
'company' => $company,
|
||||||
'deliveryServices' => $deliveryServices,
|
'deliveryServices' => $deliveryServices,
|
||||||
'apaczkaServices' => $apaczkaServices,
|
'apaczkaServices' => $apaczkaServices,
|
||||||
|
'polkurierServices' => $polkurierServices,
|
||||||
'deliveryServicesError' => $deliveryServicesError,
|
'deliveryServicesError' => $deliveryServicesError,
|
||||||
'existingPackages' => $existingPackages,
|
'existingPackages' => $existingPackages,
|
||||||
'flashSuccess' => $flashSuccess,
|
'flashSuccess' => $flashSuccess,
|
||||||
@@ -220,6 +234,10 @@ final class ShipmentController
|
|||||||
'receiver_point_id' => (string) $request->input('receiver_point_id', ''),
|
'receiver_point_id' => (string) $request->input('receiver_point_id', ''),
|
||||||
'sender_point_id' => (string) $request->input('sender_point_id', ''),
|
'sender_point_id' => (string) $request->input('sender_point_id', ''),
|
||||||
'weekend_delivery' => (string) $request->input('weekend_delivery', ''),
|
'weekend_delivery' => (string) $request->input('weekend_delivery', ''),
|
||||||
|
'service_code' => (string) $request->input('service_code', ''),
|
||||||
|
'pickup_date' => (string) $request->input('pickup_date', ''),
|
||||||
|
'pickup_time_from' => (string) $request->input('pickup_time_from', ''),
|
||||||
|
'pickup_time_to' => (string) $request->input('pickup_time_to', ''),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$packageId = (int) ($result['package_id'] ?? 0);
|
$packageId = (int) ($result['package_id'] ?? 0);
|
||||||
@@ -544,4 +562,5 @@ final class ShipmentController
|
|||||||
$parcelName = trim((string) ($deliveryAddr['parcel_name'] ?? ''));
|
$parcelName = trim((string) ($deliveryAddr['parcel_name'] ?? ''));
|
||||||
return $parcelId !== '' || $parcelName !== '';
|
return $parcelId !== '' || $parcelName !== '';
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
@@ -0,0 +1,174 @@
|
|||||||
|
"""
|
||||||
|
Generator PSD - Akrylowa statuetka podziekowanie dla Rodzicow ze zdjeciem (Wzor 3).
|
||||||
|
|
||||||
|
Struktura PSD: w grupie "Warstwa 1" znajduja sie:
|
||||||
|
- warstwy tekstowe: naglowek, para_i_data, tresc
|
||||||
|
- Smart Object zdjecia: zdjecie
|
||||||
|
|
||||||
|
Podmiana zdjecia:
|
||||||
|
- jesli --zdjecie wskazuje na istniejacy plik -> zostaje wstawione,
|
||||||
|
przeskalowane "cover" do oryginalnych boundsow placeholdera i wycentrowane
|
||||||
|
- jesli zdjecie nie zostalo podane lub plik nie istnieje -> wstawiamy
|
||||||
|
zielony prostokat (#00FF00) jako sygnal, ze klient nie przeslal foto
|
||||||
|
|
||||||
|
Wymaga: uruchomiony Adobe Photoshop, photoshop-python-api, Pillow.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import photoshop.api as ps
|
||||||
|
|
||||||
|
|
||||||
|
PROJEKT_DIR = os.path.join(
|
||||||
|
r"d:\pomysloweprezenty.pl\projekty\ślub - podziękowania",
|
||||||
|
"Akrylowa statuetka podziękowanie dla rodziców ze zdjęciem - Wzór 3",
|
||||||
|
)
|
||||||
|
SZABLON_PATH = os.path.join(
|
||||||
|
PROJEKT_DIR,
|
||||||
|
"Akrylowa statuetka podziękowanie dla rodziców ze zdjęciem - Wzór 3.psd",
|
||||||
|
)
|
||||||
|
GOTOWE_DIR = os.path.join(PROJEKT_DIR, "_gotowe")
|
||||||
|
|
||||||
|
ASSETS_DIR = os.path.join(os.path.dirname(__file__), "_assets")
|
||||||
|
GREEN_PLACEHOLDER_PATH = os.path.join(ASSETS_DIR, "green_placeholder.png")
|
||||||
|
|
||||||
|
GROUP_NAME = "Warstwa 1"
|
||||||
|
PHOTO_SLOT = "zdjecie"
|
||||||
|
|
||||||
|
NAGLOWEK_DEFAULT = "Kochani Rodzice!"
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_green_placeholder():
|
||||||
|
if os.path.exists(GREEN_PLACEHOLDER_PATH):
|
||||||
|
return GREEN_PLACEHOLDER_PATH
|
||||||
|
os.makedirs(ASSETS_DIR, exist_ok=True)
|
||||||
|
from PIL import Image
|
||||||
|
Image.new("RGB", (1000, 1000), (0, 255, 0)).save(GREEN_PLACEHOLDER_PATH)
|
||||||
|
return GREEN_PLACEHOLDER_PATH
|
||||||
|
|
||||||
|
|
||||||
|
def set_text(layer, new_text):
|
||||||
|
"""Zmienia tekst zachowujac srodek bounding boxa."""
|
||||||
|
b = [float(x) for x in layer.bounds]
|
||||||
|
cx, cy = (b[0] + b[2]) / 2, (b[1] + b[3]) / 2
|
||||||
|
layer.textItem.contents = new_text
|
||||||
|
b2 = [float(x) for x in layer.bounds]
|
||||||
|
cx2, cy2 = (b2[0] + b2[2]) / 2, (b2[1] + b2[3]) / 2
|
||||||
|
dx, dy = cx - cx2, cy - cy2
|
||||||
|
if dx or dy:
|
||||||
|
layer.translate(dx, dy)
|
||||||
|
|
||||||
|
|
||||||
|
def set_layer_text(container, layer_name, new_text):
|
||||||
|
if new_text is None:
|
||||||
|
return
|
||||||
|
layer = container.artLayers[layer_name]
|
||||||
|
old = layer.textItem.contents
|
||||||
|
set_text(layer, new_text)
|
||||||
|
print(f' {layer_name}: "{old[:50]}" -> "{new_text[:50]}"')
|
||||||
|
|
||||||
|
|
||||||
|
def replace_smart_object_contents(app, file_path):
|
||||||
|
desc = ps.ActionDescriptor()
|
||||||
|
desc.putPath(app.charIDToTypeID("null"), file_path)
|
||||||
|
desc.putInteger(app.charIDToTypeID("PgNm"), 1)
|
||||||
|
app.executeAction(app.stringIDToTypeID("placedLayerReplaceContents"), desc)
|
||||||
|
|
||||||
|
|
||||||
|
def replace_photo_cover(app, so_layer, photo_path):
|
||||||
|
"""Podmienia zawartosc Smart Object i skaluje 'cover' do oryginalnych boundsow."""
|
||||||
|
target = [float(x) for x in so_layer.bounds]
|
||||||
|
target_w = target[2] - target[0]
|
||||||
|
target_h = target[3] - target[1]
|
||||||
|
target_cx = (target[0] + target[2]) / 2
|
||||||
|
target_cy = (target[1] + target[3]) / 2
|
||||||
|
|
||||||
|
app.activeDocument.activeLayer = so_layer
|
||||||
|
replace_smart_object_contents(app, photo_path)
|
||||||
|
|
||||||
|
new_b = [float(x) for x in so_layer.bounds]
|
||||||
|
new_w = new_b[2] - new_b[0]
|
||||||
|
new_h = new_b[3] - new_b[1]
|
||||||
|
if new_w <= 0 or new_h <= 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
scale = max(target_w / new_w, target_h / new_h) * 100.0
|
||||||
|
so_layer.resize(scale, scale, ps.AnchorPosition.MiddleCenter)
|
||||||
|
|
||||||
|
cur = [float(x) for x in so_layer.bounds]
|
||||||
|
cur_cx = (cur[0] + cur[2]) / 2
|
||||||
|
cur_cy = (cur[1] + cur[3]) / 2
|
||||||
|
so_layer.translate(target_cx - cur_cx, target_cy - cur_cy)
|
||||||
|
|
||||||
|
|
||||||
|
def swap_photo_slot(app, container, slot_name, photo_path):
|
||||||
|
so_layer = container.artLayers[slot_name]
|
||||||
|
if photo_path and os.path.isfile(photo_path):
|
||||||
|
print(f" {slot_name}: {photo_path}")
|
||||||
|
replace_photo_cover(app, so_layer, photo_path)
|
||||||
|
else:
|
||||||
|
if photo_path:
|
||||||
|
print(f" {slot_name}: UWAGA plik nie istnieje ({photo_path}) - zielony placeholder")
|
||||||
|
else:
|
||||||
|
print(f" {slot_name}: brak - zielony placeholder")
|
||||||
|
replace_photo_cover(app, so_layer, ensure_green_placeholder())
|
||||||
|
|
||||||
|
|
||||||
|
def generate(klient, para, data, tresc=None, naglowek=None, zdjecie=None):
|
||||||
|
os.makedirs(GOTOWE_DIR, exist_ok=True)
|
||||||
|
output_path = os.path.join(GOTOWE_DIR, f"{klient}.psd")
|
||||||
|
|
||||||
|
para_i_data = f"{para}\r{data}"
|
||||||
|
|
||||||
|
app = ps.Application()
|
||||||
|
doc = app.open(SZABLON_PATH)
|
||||||
|
print(f"Otwarto szablon: {doc.name}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
group = doc.layerSets[GROUP_NAME]
|
||||||
|
|
||||||
|
print(" Podmiana tekstow:")
|
||||||
|
set_layer_text(group, "naglowek", naglowek or NAGLOWEK_DEFAULT)
|
||||||
|
set_layer_text(group, "para_i_data", para_i_data)
|
||||||
|
set_layer_text(group, "tresc", tresc)
|
||||||
|
|
||||||
|
print(" Podmiana zdjecia:")
|
||||||
|
swap_photo_slot(app, group, PHOTO_SLOT, zdjecie)
|
||||||
|
|
||||||
|
psd_opts = ps.PhotoshopSaveOptions()
|
||||||
|
doc.saveAs(output_path, psd_opts, True)
|
||||||
|
print(f"Zapisano: {output_path}")
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
doc.close(ps.SaveOptions.DoNotSaveChanges)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
print("Gotowe!")
|
||||||
|
return output_path
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Generator PSD - Akrylowa statuetka podziekowanie dla Rodzicow Wzor 3"
|
||||||
|
)
|
||||||
|
parser.add_argument("--klient", required=True, help="Nazwa pliku wyjsciowego (bez rozszerzenia)")
|
||||||
|
parser.add_argument("--para", required=True, help="Imiona pary mlodej, np. 'Karolina i Kamil'")
|
||||||
|
parser.add_argument("--data", required=True, help="Data uroczystosci, np. '06.06.2026'")
|
||||||
|
parser.add_argument("--naglowek", default=None, help="Naglowek (opcjonalnie, domyslnie 'Kochani Rodzice!')")
|
||||||
|
parser.add_argument("--tresc", default=None, help="Tresc podziekowania (opcjonalnie, domyslnie z szablonu)")
|
||||||
|
parser.add_argument("--zdjecie", default=None, help="Sciezka do zdjecia (opcjonalnie; brak = zielony placeholder)")
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
generate(
|
||||||
|
klient=args.klient,
|
||||||
|
para=args.para,
|
||||||
|
data=args.data,
|
||||||
|
tresc=args.tresc,
|
||||||
|
naglowek=args.naglowek,
|
||||||
|
zdjecie=args.zdjecie,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
"""
|
||||||
|
Generator PSD - Akrylowa statuetka podziekowanie dla Rodzicow ze zdjeciem (Wzor 5).
|
||||||
|
|
||||||
|
Struktura PSD: w grupie "Warstwa 1" znajduja sie:
|
||||||
|
- warstwy tekstowe: naglowek, para, data, tresc
|
||||||
|
- Smart Object zdjecia: zdjecie
|
||||||
|
|
||||||
|
Podmiana zdjecia:
|
||||||
|
- jesli --zdjecie wskazuje na istniejacy plik -> zostaje wstawione,
|
||||||
|
przeskalowane "cover" do oryginalnych boundsow placeholdera i wycentrowane
|
||||||
|
- jesli zdjecie nie zostalo podane lub plik nie istnieje -> wstawiamy
|
||||||
|
zielony prostokat (#00FF00) jako sygnal, ze klient nie przeslal foto
|
||||||
|
|
||||||
|
Wymaga: uruchomiony Adobe Photoshop, photoshop-python-api, Pillow.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import photoshop.api as ps
|
||||||
|
|
||||||
|
|
||||||
|
PROJEKT_DIR = os.path.join(
|
||||||
|
r"d:\pomysloweprezenty.pl\projekty\ślub - podziękowania",
|
||||||
|
"Akrylowa statuetka podziękowanie dla rodziców ze zdjęciem - Wzór 5",
|
||||||
|
)
|
||||||
|
SZABLON_PATH = os.path.join(
|
||||||
|
PROJEKT_DIR,
|
||||||
|
"Akrylowa statuetka podziękowanie dla rodziców ze zdjęciem - Wzór 5.psd",
|
||||||
|
)
|
||||||
|
GOTOWE_DIR = os.path.join(PROJEKT_DIR, "_gotowe")
|
||||||
|
|
||||||
|
ASSETS_DIR = os.path.join(os.path.dirname(__file__), "_assets")
|
||||||
|
GREEN_PLACEHOLDER_PATH = os.path.join(ASSETS_DIR, "green_placeholder.png")
|
||||||
|
|
||||||
|
GROUP_NAME = "Warstwa 1"
|
||||||
|
PHOTO_SLOT = "zdjecie"
|
||||||
|
|
||||||
|
NAGLOWEK_DEFAULT = "Kochani Rodzice"
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_green_placeholder():
|
||||||
|
if os.path.exists(GREEN_PLACEHOLDER_PATH):
|
||||||
|
return GREEN_PLACEHOLDER_PATH
|
||||||
|
os.makedirs(ASSETS_DIR, exist_ok=True)
|
||||||
|
from PIL import Image
|
||||||
|
Image.new("RGB", (1000, 1000), (0, 255, 0)).save(GREEN_PLACEHOLDER_PATH)
|
||||||
|
return GREEN_PLACEHOLDER_PATH
|
||||||
|
|
||||||
|
|
||||||
|
def set_text(layer, new_text):
|
||||||
|
"""Zmienia tekst zachowujac srodek bounding boxa."""
|
||||||
|
b = [float(x) for x in layer.bounds]
|
||||||
|
cx, cy = (b[0] + b[2]) / 2, (b[1] + b[3]) / 2
|
||||||
|
layer.textItem.contents = new_text
|
||||||
|
b2 = [float(x) for x in layer.bounds]
|
||||||
|
cx2, cy2 = (b2[0] + b2[2]) / 2, (b2[1] + b2[3]) / 2
|
||||||
|
dx, dy = cx - cx2, cy - cy2
|
||||||
|
if dx or dy:
|
||||||
|
layer.translate(dx, dy)
|
||||||
|
|
||||||
|
|
||||||
|
def set_layer_text(container, layer_name, new_text):
|
||||||
|
if new_text is None:
|
||||||
|
return
|
||||||
|
layer = container.artLayers[layer_name]
|
||||||
|
old = layer.textItem.contents
|
||||||
|
set_text(layer, new_text)
|
||||||
|
print(f' {layer_name}: "{old[:50]}" -> "{new_text[:50]}"')
|
||||||
|
|
||||||
|
|
||||||
|
def replace_smart_object_contents(app, file_path):
|
||||||
|
desc = ps.ActionDescriptor()
|
||||||
|
desc.putPath(app.charIDToTypeID("null"), file_path)
|
||||||
|
desc.putInteger(app.charIDToTypeID("PgNm"), 1)
|
||||||
|
app.executeAction(app.stringIDToTypeID("placedLayerReplaceContents"), desc)
|
||||||
|
|
||||||
|
|
||||||
|
def replace_photo_cover(app, so_layer, photo_path):
|
||||||
|
"""Podmienia zawartosc Smart Object i skaluje 'cover' do oryginalnych boundsow."""
|
||||||
|
target = [float(x) for x in so_layer.bounds]
|
||||||
|
target_w = target[2] - target[0]
|
||||||
|
target_h = target[3] - target[1]
|
||||||
|
target_cx = (target[0] + target[2]) / 2
|
||||||
|
target_cy = (target[1] + target[3]) / 2
|
||||||
|
|
||||||
|
app.activeDocument.activeLayer = so_layer
|
||||||
|
replace_smart_object_contents(app, photo_path)
|
||||||
|
|
||||||
|
new_b = [float(x) for x in so_layer.bounds]
|
||||||
|
new_w = new_b[2] - new_b[0]
|
||||||
|
new_h = new_b[3] - new_b[1]
|
||||||
|
if new_w <= 0 or new_h <= 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
scale = max(target_w / new_w, target_h / new_h) * 100.0
|
||||||
|
so_layer.resize(scale, scale, ps.AnchorPosition.MiddleCenter)
|
||||||
|
|
||||||
|
cur = [float(x) for x in so_layer.bounds]
|
||||||
|
cur_cx = (cur[0] + cur[2]) / 2
|
||||||
|
cur_cy = (cur[1] + cur[3]) / 2
|
||||||
|
so_layer.translate(target_cx - cur_cx, target_cy - cur_cy)
|
||||||
|
|
||||||
|
|
||||||
|
def swap_photo_slot(app, container, slot_name, photo_path):
|
||||||
|
so_layer = container.artLayers[slot_name]
|
||||||
|
if photo_path and os.path.isfile(photo_path):
|
||||||
|
print(f" {slot_name}: {photo_path}")
|
||||||
|
replace_photo_cover(app, so_layer, photo_path)
|
||||||
|
else:
|
||||||
|
if photo_path:
|
||||||
|
print(f" {slot_name}: UWAGA plik nie istnieje ({photo_path}) - zielony placeholder")
|
||||||
|
else:
|
||||||
|
print(f" {slot_name}: brak - zielony placeholder")
|
||||||
|
replace_photo_cover(app, so_layer, ensure_green_placeholder())
|
||||||
|
|
||||||
|
|
||||||
|
def generate(klient, para, data, tresc=None, naglowek=None, zdjecie=None):
|
||||||
|
os.makedirs(GOTOWE_DIR, exist_ok=True)
|
||||||
|
output_path = os.path.join(GOTOWE_DIR, f"{klient}.psd")
|
||||||
|
|
||||||
|
app = ps.Application()
|
||||||
|
doc = app.open(SZABLON_PATH)
|
||||||
|
print(f"Otwarto szablon: {doc.name}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
group = doc.layerSets[GROUP_NAME]
|
||||||
|
|
||||||
|
print(" Podmiana tekstow:")
|
||||||
|
set_layer_text(group, "naglowek", naglowek or NAGLOWEK_DEFAULT)
|
||||||
|
set_layer_text(group, "para", para)
|
||||||
|
set_layer_text(group, "data", data)
|
||||||
|
set_layer_text(group, "tresc", tresc)
|
||||||
|
|
||||||
|
print(" Podmiana zdjecia:")
|
||||||
|
swap_photo_slot(app, group, PHOTO_SLOT, zdjecie)
|
||||||
|
|
||||||
|
psd_opts = ps.PhotoshopSaveOptions()
|
||||||
|
doc.saveAs(output_path, psd_opts, True)
|
||||||
|
print(f"Zapisano: {output_path}")
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
doc.close(ps.SaveOptions.DoNotSaveChanges)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
print("Gotowe!")
|
||||||
|
return output_path
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Generator PSD - Akrylowa statuetka podziekowanie dla Rodzicow ze zdjeciem Wzor 5"
|
||||||
|
)
|
||||||
|
parser.add_argument("--klient", required=True, help="Nazwa pliku wyjsciowego (bez rozszerzenia)")
|
||||||
|
parser.add_argument("--para", required=True, help="Imiona pary mlodej, np. 'Magdalena i Robert'")
|
||||||
|
parser.add_argument("--data", required=True, help="Data uroczystosci, np. '27.06.2026'")
|
||||||
|
parser.add_argument("--naglowek", default=None, help="Naglowek (opcjonalnie, domyslnie 'Kochani Rodzice')")
|
||||||
|
parser.add_argument("--tresc", default=None, help="Tresc podziekowania (opcjonalnie, domyslnie z szablonu)")
|
||||||
|
parser.add_argument("--zdjecie", default=None, help="Sciezka do zdjecia (opcjonalnie; brak = zielony placeholder)")
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
generate(
|
||||||
|
klient=args.klient,
|
||||||
|
para=args.para,
|
||||||
|
data=args.data,
|
||||||
|
tresc=args.tresc,
|
||||||
|
naglowek=args.naglowek,
|
||||||
|
zdjecie=args.zdjecie,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user