diff --git a/.paul/PROJECT.md b/.paul/PROJECT.md index fdd7fc5..ca3825b 100644 --- a/.paul/PROJECT.md +++ b/.paul/PROJECT.md @@ -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] 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] 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 @@ -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 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 | +| 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`, `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 diff --git a/.paul/ROADMAP.md b/.paul/ROADMAP.md index 19f3209..09d4c8f 100644 --- a/.paul/ROADMAP.md +++ b/.paul/ROADMAP.md @@ -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) | | 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) | +| 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): +- polkurier TrackingService + `delivery_status_mappings` (provider='polkurier') +- polkurier paczkomaty (`InpostParcelMachines` / `PocztexPostOffices` / `Kurier48PostOffices` z SDK polkuriera) - Eksport XLSX listy wystawionych faktur (analogicznie do paragonow) - Idempotencja podwojnego POST do Fakturowni (INVOICE-IDEMP-115) - Event automatyzacji `invoice.created` (jezeli operator chce wysylac faktury mailem) diff --git a/.paul/STATE.md b/.paul/STATE.md index c06e157..9b1caaa 100644 --- a/.paul/STATE.md +++ b/.paul/STATE.md @@ -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 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 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 diff --git a/.paul/changelog/2026-05-14.md b/.paul/changelog/2026-05-14.md new file mode 100644 index 0000000..b5bcabd --- /dev/null +++ b/.paul/changelog/2026-05-14.md @@ -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` diff --git a/.paul/codebase/architecture.md b/.paul/codebase/architecture.md index e9a5275..0368997 100644 --- a/.paul/codebase/architecture.md +++ b/.paul/codebase/architecture.md @@ -343,6 +343,116 @@ tests/ ### IntegrationsHubController - 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": "", "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`** (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: }`. + - `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/`. + +### 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). +- `
` z dynamicznym `` 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 ### SmsConversationService (`src/Modules/Sms/SmsConversationService.php`) diff --git a/.paul/codebase/db_schema.md b/.paul/codebase/db_schema.md index 5de3c6b..b45981c 100644 --- a/.paul/codebase/db_schema.md +++ b/.paul/codebase/db_schema.md @@ -1,6 +1,6 @@ # 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)` +**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 @@ -460,6 +480,18 @@ Indexes: `shipment_packages_order_idx`, `shipment_packages_status_idx`, `shipmen 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 @@ -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)`. **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. diff --git a/.paul/codebase/tech_changelog.md b/.paul/codebase/tech_changelog.md index 5cb8377..d9f5117 100644 --- a/.paul/codebase/tech_changelog.md +++ b/.paul/codebase/tech_changelog.md @@ -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 `[N]` 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) `
` 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) + ` + +Przyklad inicjalizacji i uycia mapy w JavaScript: +const map = new POLKURIER.PointsMap({ + + token: 'eyJ0eXAiOiUzI1NiJ9.eyJpc3MiOieqSFy5wAC04eNF61bmvvyWg', + // Pozostale parametry konfiguracyjne opisane zostaly + // w tabeli Opcje konfiguracji mapy +}); +map.showMapDialog(); // Otwarcie okna modalnego z map +map.onConfirm(function (point) { + // Dzialania po wybraniu punktu, np. ustawienie ID punktu w formularzu + map.closeMapDialog(); // Zamknicie mapy +}); + +Konstruktor obiektu POLKURIER.PointsMap przyjmuje jako argument obiekt konfiguracyjny z nastpujcymi +parametrami: + +Opcje konfiguracji mapy + +Parametr Typ Wyma Warto Opis + gany domylna + +token String TAK `' Token Dostpowy, ktry naley pobra za pomoc + metody get_map_token + +searchQuery String NIE `' Domylna fraza wyszukiwania. Jeli bdzie to ID + punktu, to mapa zostanie wycentrowana na + wskazanym punkcie. + +functions Array na mapie. Funkcje opisane zostaly w tabeli + COURIER_POINT_FUNCTION + +providers Array NIE undefined Tablica kodw kurierw do wywietlenia na licie. + Kurierw pobra mona za pomoc moetody + available_carriers. Jeli warto ta jest undefined + lub null to wywietla wszystkich kurierw. + +selectedProviders Array NIE undefined Tablica kodw kurierw, ktrzy s domylnie + zaznaczeni. Kurierw pobra mona za pomoc + moetody available_carriers. Jeli warto ta jest + undefined lub null to zaznacza wszystkich. + +latitude Float NIE 52.2252606 Domylna szeroko geograficzna, na ktrej + wycentruje si mapa po otwarciu. + + 49 + longitude Float NIE 20.9986249 Domylna dlugo geograficzna, na ktrej wycentruje + si mapa po otwarciu. + +zoom Float NIE 13 Domylne przyblienie mapy. +showSelectButton Bool + NIE true Czy wywietli przycisk ,,Wybierz" po klikniciu w + punkt na mapie. Jeli false to mapa bdzie w trybie + tylko do odczytu + +showList Bool NIE true Czy pokaza list punkw obok mapy +showSearchInput Bool + NIE true Czy pokaza pole wyszukiwania + +Instancja obiektu mapy posiada nastpujce metody: + +Metody mapy Argumenty Dane zwracane Opis +Metoda + +showMapView HTMLElement targetElement Wywietla mapk we + wskazanym elemencie + HTML + +showMapDialog Otwiera okno z mapk + +closeMapDialog Zamyka okno z mapk + +on String Nazwa eventu Dodaje nasluch eventw. + Dostpne zdarzenia + Function Funkcja obslugujca event zostaly opisane w tabeli + Zdarzenia poniej + +onConfirm Function Funkcja obslugujca event Callback po wybraniu + punktu + +onSelectPoint Function Funkcja obslugujca event Callback po zaznaczeniu + punktu na mapie lub + licie + +getSelectedPoint Object Zwraca wybrany punkt + | null + +setSearchQuery String searchQuery Ustawia fraz + wyszukiwania + +setAvailableProviders Array availableProviders Ustawia dostpnych + przewonikw + +setSelectedProviders Array selectedProviders Ustawia wybranych + przewonikw + +setMapPosition Float Szeroko geograficzna Ustawia pozycj mapy + Float Dlugo geograficzna + Float Zblienie mapy + +getMapPosition Array Szeroko + geograficzna, Dlugo + geograficzna, Zblienie mapy + +getPointFunction Array Zwraca wymagane + funkcje punktu + +setPointFunctionsFilter Array Ustawia wymagane + funkcje punktu + +getProviderName String Zwraca nazw + wybranego przewonika + +destroy Niszczy widok mapy + + 50 + Obiekt mapy posiada nastpujce metody statyczne: + +Metody STATYCZNE mapy + +Metoda Argumenty Dane zwracane Opis + Promise Zwraca ID providera dla + wybranego przewonika. +getAvailableProviders Promise> Jeli dany przewonik nie + obsluguje punktw to + metoda zwrci warto + undefined + + Zwraca tablic + wszystkich + dostpnych + providerw. + +Instancja mapy emituje zdarzenia, do ktrych mona doda wlasne funkcje obslugujce za pomoc metody +on(...) (Opis w tabeli ,,Metody mapy") + +map.on('select:point', function (point) { + // ... + +}); + +Zdarzenia Argumenty Opis +Nazwa +dialog:open Otwarcie okna z map +dialog:closed +confirm:point Zamknicie okna z map + +select:point Object | Zatwierdzenie wybrania punktu (Kliknicie + null ,,Wybierz") +input:searchQuery + Object | Zaznaczeniu punktu na mapie lub licie + null + + InputEvent Zdarzenie JS po wpisaniu Wpisanie frazy wyszukiwania + tekstu + + 51 + Definicje + +Rodzaj przesylek SHIPMENT_TYPE + +Nazwa Opis +envelope koperty +box paczki +palette palety + +Typy przesylek PACK_TYPE Opis + Standardowa + Nazwa Niestandardowa + ST Pl paleta + NST Paleta przemyslowa + PPAL Dluyca + PAL + DLU + +Typ zwrotu pobrania codtype, COD_TYPE + +Nazwa Opis +S Zwrot pobrania od 5 do 7 dni roboczych w zalenoci od przewonika +1D Zwrot pobrania w 1 dzie roboczy od daty dostarczenia przesylki +4D Zwrot pobrania w 4 dni robocze od daty dostarczenia przesylki +16D Zwrot w 16 dni robocze od daty dostarczenia przesylki + +Sposb zwrotu pobrania return_cod + +Nazwa Opis +BA Przelew na konto bankowe +PO Przekaz pocztowy na adres nadawcy +MB Pobranie przekazane do skarbonki + +Status zlecenia ORDER_STATUS + +Lista statusw + +Kod Nazwa Opis + Zlecenie zostalo prawidlowo zapisane i oczekuje na platno +O Oczekuje Zlecenie zostalo prawidlowo zapisane. Wygenerowano list przewozowy. + Przesylka oczekuje na odbir przez kuriera +P Potwierdzone Przesylka anulowana + Przesylka odebrana od nadawcy przez kuriera i jest w drodze do adresata +A Anulowane Przesylka dotarla do adresata + Odbiorca odmwil odebrania przesylki ktra zostanie zwrcona nadawcy. +WP W przewozie Status informuje e pojawily si problemy z dostarczeniem przesylki. W + przypadku wystpienia tego statusu zalecamy kontakt z BOK w celu szybkiego +D Dostarczona wyjanienia z przewonikiem + Zamwienie z podjazdem zbiorczym. Kod uywany wylcznie w atrybucie +Z Zwrot do nadawcy + status metody get_orders w celu wyszukania zlece. Nie wystpuje jako +W Wyjtek samodzielny status zlecenia. + +PZ Podjazd zbiorczy + +Status pobrania COD_STATUS + +Kod Nazwa Opis +WAITING Oczekuje Przesylka oczekuje na nadanie + + 52 + IN_TRANSIT W przewozie Przesylka jest w przewozie +MONEY_BOX W skarbonce Kwota pobrania zostala dodana do salda skarbonki +PAYMENT_PENDING Do wyplaty Kwota pobrania zostala zlecona do wyplaty +COMPENSATION Kompensata Wykonana zostala kompensata +PAID Wyplacone Kwota pobrania zostala wyplacona +SETTLED Rozliczone Kwota pobrania zostal rozliczona + +Status operacji na pobraniu COD_OPERATION_REASON + +Kod Opis +COMPENSATION Kompensata +CUSTOMER_INVOICE_PAYMENT Platno za faktur +CUSTOMER_PREPAID_PAYMENT Doladowanie przedplaty + +CUSTOMER_MONEY_BOX_FEE Oplata manipulacyjna za wyplat ze skarbonki +CUSTOMER_PAID Wyplata do klienta + +Uslugi dodatkowe ADDITIONAL_SERVICE Opis + Dokumenty zwrotne. + Nazwa Podjazd kuriera z wydrukowanym listem przewozowym. + ROD Dostawa weekendowa. + COURIER_WITH_LABEL Powiadomienie SMS o nadaniu przesylki + WEEK_COLLECTION Powiadomienie SMS o nadaniu przesylki z wlasn nazw + SMS_NOTIFICATION_RECIPIENT Nadanie bez etykiety + SMS_NOTIFICATION_RECIPIENT_WITH_NAME Ostronie + LABELLESS Dorczenie do rk wlasnych + HANDLE_WITH_CARE Dodatkowe sprawdzenie zawartoci + DOSTAWA_DO_RAK_WLASNYCH Awizacja telefoniczne przed dorczeniem + CHECK_CONTENT Inny adres nadawcy na etykiecie + PHONE_NOTIFICATION_RECIPIENT Opony + COVER_ADDRESS_SENDER Wniesienie przesylki + TIRES Dorczenie w sobot + BRINGING_DELIVERED_PARCEL Awizacja telefoniczna odbioru + SATURDAY_DELIVERY Dostawa w okrelonych godzinach + PHONE_NOTIFICATION_COLLECTION Dorczenie do rk wlasnych + DELIVERY_TO_TIME + HAND_DELIVERY + + 53 + \ No newline at end of file diff --git a/.paul/phases/129-order-user-notes/129-01-PLAN.md b/.paul/phases/129-order-user-notes/129-01-PLAN.md new file mode 100644 index 0000000..be9c06e --- /dev/null +++ b/.paul/phases/129-order-user-notes/129-01-PLAN.md @@ -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 +--- + + +## 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). + + + +## 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). + + +- **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. + + + + + +## 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=, 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 `[2]` 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`. +``` + + + + + + + Task 1: Migracja DB + extend `order_notes` o pola user-authored + database/migrations/20260514_000116_extend_order_notes_user_authored.sql, .paul/codebase/db_schema.md + + 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. + + php bin/migrate.php → migration logged; `DESCRIBE order_notes;` pokazuje nowe kolumny i FK; re-run migracji = no-op (idempotent guard). + AC-1 satisfied: kolumny dodane, FK aktywny, indeks utworzony, schema doc zaktualizowany. + + + + Task 2: OrderNotesService + repository extension + routes + Controller actions + src/Modules/Orders/OrderNotesService.php, src/Modules/Orders/OrdersRepository.php, src/Modules/Orders/OrdersController.php, routes/web.php + + 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 ? ' [' . $userNotesCount . ']' : '';` — 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'`. + + + `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. + + AC-2, AC-3, AC-6 satisfied: CRUD działa, autoryzacja po `user_id` egzekwowana, subquery count w listingu bez wpływu na paginację. + + + + Task 3: UI — sekcja notatek w show.php, badge na list.php, JS edit modal, SCSS, i18n + 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 + + 1) `resources/views/orders/show.php` (sekcja "Wiadomosci i zalaczniki", linia ~449–463): + - Zmień blok renderowania na 2 sub-listy: + a) `
` — header "Notatki", iteracja po `$userNotesList` (passed z controllera). Każda notatka: `
` z `
data | autor
`, `
body
`, oraz `
` z przyciskami `Edytuj` / `Usuń` widocznymi gdy `(int)($note['user_id'] ?? 0) === $currentUserId`. Przycisk "Usuń" jako `
` + 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 `` z CSRF `_token`, ` +
+ + +
+
+ +
+ + +
+ + +
+ +
+
+
+ + +
+

-
+
|
+
diff --git a/resources/views/settings/polkurier.php b/resources/views/settings/polkurier.php new file mode 100644 index 0000000..58c985a --- /dev/null +++ b/resources/views/settings/polkurier.php @@ -0,0 +1,110 @@ + + +
+

+

+ + +
+ + + +
+ + + +
+ +
+ +
+

+ +
+ : + + | + : + +
+ +
+ + + + + + + + + + +
+ +
+
+
+ +
+

+

+ +
+ +
+ +
+
+ + +
+ ' . $e(strtoupper($lastTestStatus)) . ''; + } + if ($lastTestHttpCode !== null) { + $parts[] = 'HTTP ' . $e((string) $lastTestHttpCode) . ''; + } + if ($lastTestMessage !== '') { + $parts[] = $e($lastTestMessage); + } + $messageHtml = implode(' · ', $parts); + $dismissible = false; + include dirname(__DIR__) . '/components/alert.php'; + unset($messageHtml); + ?> +
+ +
diff --git a/resources/views/shipments/prepare.php b/resources/views/shipments/prepare.php index 8283901..6c3292c 100644 --- a/resources/views/shipments/prepare.php +++ b/resources/views/shipments/prepare.php @@ -6,6 +6,7 @@ $prefs = is_array($preferences ?? null) ? $preferences : []; $comp = is_array($company ?? null) ? $company : []; $services = is_array($deliveryServices ?? null) ? $deliveryServices : []; $apaczkaSvcList = is_array($apaczkaServices ?? null) ? $apaczkaServices : []; +$polkurierSvcList = is_array($polkurierServices ?? null) ? $polkurierServices : []; $packages = is_array($existingPackages ?? null) ? $existingPackages : []; $pendingPrintIds = is_array($pendingPrintPackageIds ?? null) ? $pendingPrintPackageIds : []; $servicesError = (string) ($deliveryServicesError ?? ''); @@ -105,6 +106,7 @@ $defaultCodAmount = $isCod ? number_format($totalWithTax, 2, '.', '') : '0'; +
Metoda z zamowienia: :
@@ -213,12 +215,46 @@ $defaultCodAmount = $isCod ? number_format($totalWithTax, 2, '.', '') : '0';
+
+ +
Brak uslug polkurier (sprawdz konfiguracje w Ustawienia → Integracje → polkurier).
+ + +
Dla uslug paczkomatowych wpisz ID punktu w pole "Punkt odbioru" w sekcji Adres odbiorcy ponizej (np. POP-RZE54).
+ +
+
Wybierz przewoznika
- + +