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:
@@ -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": "<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
|
||||
|
||||
### SmsConversationService (`src/Modules/Sms/SmsConversationService.php`)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
**Co zrobiono:**
|
||||
|
||||
Reference in New Issue
Block a user