---
phase: 130-polkurier-delivery-status-mappings
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
  - src/Modules/Shipments/DeliveryStatus.php
  - src/Modules/Settings/DeliveryStatusesController.php
  - src/Modules/Settings/DeliveryStatusMappingController.php
  - src/Modules/Shipments/DeliveryStatusMappingRepository.php
autonomous: true
delegation: off
---

<objective>
## Goal
Eksponuj `polkurier` w UI `/settings/delivery-statuses?tab=mapping`: dropdown providerów pokazuje pozycję "polkurier", 7 domyślnych mapowań (O/P/A/WP/D/Z/W) ładuje się z `DeliveryStatus::getDefaultMappings('polkurier')`, a badge "niezmapowane statusy" w menu zlicza również polkurier.

## Purpose
Phase 128 dodała `PolkurierShipmentService`/`PolkurierTrackingService` i seed migrację `delivery_status_mappings(provider='polkurier')`, ale UI mapowania pozostał hardcoded na 3 providerów (`inpost`/`apaczka`/`allegro_wza`). Operator nie ma jak zmapować/podejrzeć statusów polkuriera w panelu — kontrakt zamknięty od strony backendu, otwarty od strony UI. Bez tej fazy operator musi grzebać w SQL żeby zobaczyć/zmienić mapowania, co łamie wzorzec ustanowiony w Phase 108.

## Output
- `POLKURIER_MAP` + `POLKURIER_DESCRIPTIONS` w `DeliveryStatus.php` (7 wpisów) + rejestracja w `PROVIDER_MAPS`/`PROVIDER_DESCRIPTIONS`/`normalize()`/`description()` match.
- `'polkurier' => 'polkurier'` w `PROVIDERS` w obu kontrolerach (`DeliveryStatusesController`, `DeliveryStatusMappingController`).
- `'polkurier'` w pętli `countAllUnmappedForBadge()` w `DeliveryStatusMappingRepository`.
</objective>

<context>
<clarifications>
- **Źródło defaultów** — Skąd UI ma czerpać 7 domyślnych mapowań polkurier (O/P/A/WP/D/Z/W)?
  → Odpowiedź: Hardcoded w `DeliveryStatus.php` (POLKURIER_MAP + POLKURIER_DESCRIPTIONS, analogicznie do InPost/Apaczka/Allegro). DB seed migracji z Phase 128 nadal dostępny jako override.
- **Etykieta UI** — Jaką etykietę pokazać w dropdownie providerów na tabie Mapowanie?
  → Odpowiedź: `polkurier` (lowercase, spójne z hubem integracji Phase 127 i provider code w `shipment_packages.provider`).
- **Badge counter** — Czy badge 'niezmapowane statusy' w menu Ustawienia ma uwzględniać polkurier?
  → Odpowiedź: Tak — dodać `polkurier` do pętli `countAllUnmappedForBadge()`.
</clarifications>

## Project Context
@.paul/PROJECT.md
@.paul/ROADMAP.md
@.paul/STATE.md
@.paul/codebase/architecture.md
@.paul/codebase/db_schema.md

## Source Files
@src/Modules/Shipments/DeliveryStatus.php
@src/Modules/Settings/DeliveryStatusesController.php
@src/Modules/Settings/DeliveryStatusMappingController.php
@src/Modules/Shipments/DeliveryStatusMappingRepository.php
@resources/views/settings/delivery-statuses.php

## Prior Work
@.paul/phases/128-polkurier-shipment-service/128-01-SUMMARY.md
@.paul/phases/108-delivery-status-management/108-02-SUMMARY.md
</context>

<acceptance_criteria>

## AC-1: Polkurier widoczny w dropdownie providerów
```gherkin
Given operator jest zalogowany i otwiera `/settings/delivery-statuses?tab=mapping`
When dropdown "Provider" jest rozwinięty
Then na liście widoczne są 4 pozycje: InPost, Apaczka, Allegro oraz polkurier
```

## AC-2: 7 domyślnych mapowań polkurier
```gherkin
Given operator wybiera "polkurier" w dropdownie providerów na tabie Mapowanie
When tabela mapowań się ładuje (bez uruchamiania migracji seed Phase 128)
Then widoczne jest dokładnie 7 wierszy z raw statusami: O, P, A, WP, D, Z, W
  And każdy wiersz pokazuje znormalizowany status zgodny z dokumentacją polkurier v1.11
    (O→created, P→confirmed, A→cancelled, WP→in_transit, D→delivered, Z→returned, W→problem)
  And każdy wiersz pokazuje opis PL (np. "Oczekuje na płatność", "Dostarczona")
  And wiersze NIE są oznaczone jako "custom" (is_custom=false) — to są defaulty z kodu
```

## AC-3: Badge "niezmapowane" zlicza polkurier
```gherkin
Given w `shipment_packages` istnieje wiersz z `provider='polkurier'` i `delivery_status_raw='X'`
  And kod 'X' nie jest w domyślnych 7 ani w override'ach `delivery_status_mappings`
When sidebar Ustawień się renderuje (badge "niezmapowane")
Then licznik z `countAllUnmappedForBadge()` wzrasta o 1 z tytułu polkurier
```

## AC-4: Override DB nadpisuje hardcoded default
```gherkin
Given operator zapisuje override dla `provider='polkurier'`, `raw_status='D'` z `normalized_status='problem'`
When operator odświeża tab Mapowanie z `provider=polkurier`
Then wiersz "D" pokazuje normalized='problem' (z DB) zamiast 'delivered' (z kodu)
  And wiersz jest oznaczony jako custom (is_custom=true)
```

## AC-5: Zero regresji dla istniejących providerów
```gherkin
Given operator otwiera `/settings/delivery-statuses?tab=mapping&provider=inpost`
When tabela się ładuje
Then liczba i treść wierszy InPost/Apaczka/Allegro pozostaje identyczna jak przed zmianami
  And `DeliveryStatus::normalize('inpost', $raw)` zwraca te same wartości
```

</acceptance_criteria>

<tasks>

<task type="auto">
  <name>Task 1: Hardcoded POLKURIER_MAP + POLKURIER_DESCRIPTIONS w DeliveryStatus.php</name>
  <files>src/Modules/Shipments/DeliveryStatus.php</files>
  <action>
    Dodaj dwie nowe stałe klasowe analogicznie do `INPOST_MAP`/`INPOST_DESCRIPTIONS`:

    ```php
    private const POLKURIER_MAP = [
        'O'  => self::CREATED,
        'P'  => self::CONFIRMED,
        'A'  => self::CANCELLED,
        'WP' => self::IN_TRANSIT,
        'D'  => self::DELIVERED,
        'Z'  => self::RETURNED,
        'W'  => self::PROBLEM,
    ];

    private const POLKURIER_DESCRIPTIONS = [
        'O'  => 'Oczekuje na płatność',
        'P'  => 'Potwierdzone, list wygenerowany',
        'A'  => 'Anulowane',
        'WP' => 'W przewozie',
        'D'  => 'Dostarczona',
        'Z'  => 'Zwrot do nadawcy',
        'W'  => 'Wyjątek',
    ];
    ```

    Następnie zarejestruj `'polkurier'` w trzech miejscach:
    1. `PROVIDER_MAPS` (po `'allegro_wza'`) — `'polkurier' => self::POLKURIER_MAP,`
    2. `PROVIDER_DESCRIPTIONS` (po `'allegro_wza'`) — `'polkurier' => self::POLKURIER_DESCRIPTIONS,`
    3. `normalize()` match expression — dodaj `'polkurier' => self::POLKURIER_MAP,`
    4. `description()` match expression — dodaj `'polkurier' => self::POLKURIER_DESCRIPTIONS,`

    Treść 7 wpisów MUSI być identyczna z migracją Phase 128
    (`database/migrations/20260514_000115_seed_polkurier_delivery_status_mappings.sql`).
    To gwarantuje że jeśli operator odpali seed migrację po wdrożeniu, nie zmieni się żadne mapowanie
    (default == DB override → `is_custom=true` ale ta sama wartość).

    Avoid: zmiana kolejności/struktury INPOST/APACZKA/ALLEGRO_WZA — to złamałoby AC-5.
  </action>
  <verify>
    php -r "require 'vendor/autoload.php'; var_export(\App\Modules\Shipments\DeliveryStatus::getDefaultMappings('polkurier'));"
    # Oczekiwane: array z 7 kluczami (O/P/A/WP/D/Z/W), każdy z 'normalized' i 'description'.
  </verify>
  <done>AC-2, AC-5 satisfied: 7 defaultów polkurier z poprawnym normalized+description; existing providers nietknięte.</done>
</task>

<task type="auto">
  <name>Task 2: Dodaj 'polkurier' do PROVIDERS w obu kontrolerach + badge counter</name>
  <files>src/Modules/Settings/DeliveryStatusesController.php, src/Modules/Settings/DeliveryStatusMappingController.php, src/Modules/Shipments/DeliveryStatusMappingRepository.php</files>
  <action>
    1. `DeliveryStatusesController.php` (linie 22-26): dodaj `'polkurier' => 'polkurier',` jako 4. wpis w stałej `PROVIDERS`. Zachowaj kolejność: inpost, apaczka, allegro_wza, polkurier.

    2. `DeliveryStatusMappingController.php` (linie 22-26): identyczna zmiana w analogicznej stałej `PROVIDERS`.

    3. `DeliveryStatusMappingRepository.php` linia 158 — zmień:
       ```php
       $providers = ['inpost', 'apaczka', 'allegro_wza'];
       ```
       na:
       ```php
       $providers = ['inpost', 'apaczka', 'allegro_wza', 'polkurier'];
       ```

    Po tej zmianie `index()` w obu kontrolerach automatycznie zaakceptuje `?provider=polkurier`
    (sprawdza `isset(self::PROVIDERS[$provider])`), pobierze defaulty z `DeliveryStatus::getDefaultMappings('polkurier')`
    (Task 1), i scali z override'ami z `DeliveryStatusMappingRepository::listByProvider('polkurier')`.

    Widok `resources/views/settings/delivery-statuses.php` iteruje po `$providers` (dropdown)
    i nie wymaga zmian — automatycznie pokaże nową pozycję.

    Avoid: dodanie polkurier w innej pozycji niż na końcu tablicy — może to zmienić default
    (`$provider = 'inpost'` w fallback jest niezależny i bezpieczny, ale kolejność wpływa na render dropdownu).
  </action>
  <verify>
    # 1. Sprawdź dropdown:
    curl -s -b "session.cookie" https://orderpro.projectpro.pl/settings/delivery-statuses?tab=mapping | grep -c 'value="polkurier"'
    # Oczekiwane: 1

    # 2. Sprawdź że provider=polkurier renderuje 7 wierszy:
    curl -s -b "session.cookie" 'https://orderpro.projectpro.pl/settings/delivery-statuses?tab=mapping&provider=polkurier' | grep -E 'raw_status.*(O|P|A|WP|D|Z|W)' | wc -l
    # Oczekiwane: 7

    # 3. Smoke regresji — InPost dalej działa:
    curl -s -b "session.cookie" 'https://orderpro.projectpro.pl/settings/delivery-statuses?tab=mapping&provider=inpost' | grep -c 'value="inpost"'
    # Oczekiwane: 1 (selected)
  </verify>
  <done>AC-1, AC-3, AC-4, AC-5 satisfied: dropdown pokazuje polkurier, 7 defaultów się renderuje, badge zlicza polkurier, override DB nadpisuje default, istniejące providery bez regresji.</done>
</task>

</tasks>

<boundaries>

## DO NOT CHANGE
- `database/migrations/20260514_000115_seed_polkurier_delivery_status_mappings.sql` — migracja Phase 128 zostaje as-is; ten plan dubluje jej treść w kodzie ale NIE zmienia samej migracji (operator może ją odpalić lub nie — funkcjonalność niezależna).
- Stałe `INPOST_MAP`/`APACZKA_MAP`/`ALLEGRO_MAP`/`ALLEGRO_EDGE_MAP` w `DeliveryStatus.php` — żadnych edycji wartości lub kolejności.
- `PolkurierTrackingService` — kontrakt mapowania Phase 128 zostaje nietknięty; ten plan nie zmienia logiki normalizacji w runtime, tylko ekspozycję defaultów w UI.
- Schemat tabeli `delivery_status_mappings` — brak migracji w tym planie.
- `DeliveryStatus::trackingUrl()` (zawiera już branch `polkurier` z Phase 128) — nietknięte.

## SCOPE LIMITS
- Brak dodatkowych mapowań polkurier (np. nieudokumentowanych w v1.11) — tylko 7 oficjalnych kodów z dokumentacji.
- Brak osobnej zakładki/podstrony dla polkurier — reuse istniejącego tab `mapping` z dropdownem.
- Brak zmian w `PROJECT.md`/`ROADMAP.md` — to robi UNIFY.
- Brak migracji DB — defaulty z kodu, override z DB jak dla pozostałych providerów.
- Brak zmian w widoku `delivery-statuses.php` — dropdown iteruje po `$providers` z controllera.

</boundaries>

<verification>
Przed declared complete:
- [ ] `DeliveryStatus::getDefaultMappings('polkurier')` zwraca 7 wpisów z poprawnymi normalized+description (AC-2).
- [ ] Dropdown providerów w `/settings/delivery-statuses?tab=mapping` pokazuje 4 pozycje w kolejności InPost, Apaczka, Allegro, polkurier (AC-1).
- [ ] Selekcja `?provider=polkurier` ładuje 7 wierszy mapowań bez fatal errora (AC-2).
- [ ] Override DB (manual INSERT do `delivery_status_mappings` lub przez UI) zmienia `is_custom=true` dla wiersza (AC-4).
- [ ] `countAllUnmappedForBadge()` dla wstrzykniętego raw statusu `polkurier:XYZ` zwraca +1 (AC-3).
- [ ] Smoke regresji: `?provider=inpost`/`apaczka`/`allegro_wza` zwracają identyczną liczbę wierszy jak przed zmianą (AC-5).
- [ ] `php -l` przechodzi dla wszystkich 4 zmienionych plików.
</verification>

<success_criteria>
- 4 pliki zmodyfikowane (3 controllery + repo + DeliveryStatus.php — łącznie 4 fizyczne pliki, 5 punktów edycji).
- AC-1..AC-5 zweryfikowane.
- Brak zmian schematu DB.
- Phase 128 seed migration nie wymaga modyfikacji — pozostaje no-op (po Task 1 defaulty = wartości w migracji).
- Manual smoke na `/settings/delivery-statuses?tab=mapping&provider=polkurier` po deploy.
</success_criteria>

<output>
After completion, create `.paul/phases/130-polkurier-delivery-status-mappings/130-01-SUMMARY.md`
</output>
