plan(07-pre-expansion-fixes): create 5 plans for pre-expansion fixes

07-01: Performance — N+1 subqueries, information_schema static, DB indexes
07-02: Stability — SSL verification (4 clients), cron throttle→DB, migration 000014b
07-03: UX — orderpro_to_allegro disable, orders list items 14-17
07-04: Tests — AllegroTokenManager + AllegroOrderImportService unit tests
07-05: InPost ShipmentProviderInterface (replaces allegro_wza workaround)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-13 17:44:42 +01:00
parent e01b68f559
commit 03c18f6782
7 changed files with 1226 additions and 17 deletions

View File

@@ -6,13 +6,26 @@ orderPRO to narzędzie do wielokanałowego zarządzania sprzedażą. Projekt prz
## Current Milestone
Brak aktywnego milestone.
**v0.2 Pre-Expansion Fixes** (v0.2.0)
Status: 🔄 In Progress
Phases: 0/1 complete
Run `/paul:discuss-milestone` lub `/paul:milestone` aby zdefiniować v0.2.
## Phases
## Next Milestone
| Phase | Name | Plans | Status | Completed |
|-------|------|-------|--------|-----------|
| 7 | Pre-Expansion Fixes | 0/5 | 🔄 Planning | — |
> Niezdefiniowany. Uruchom `/paul:discuss-milestone` aby omówić zakres lub `/paul:milestone` aby stworzyć bezpośrednio.
## Phase Details
### Phase 7 — Pre-Expansion Fixes
Naprawa krytycznych problemów wydajnościowych, bezpieczeństwa i UX przed rozbudową aplikacji o nowe integracje i funkcje.
- **Plan 07-01** — Performance: N+1 subqueries + information_schema cache + DB indexes — *Not started*
- **Plan 07-02** — Stability: SSL verification + cron throttle DB + migration 000014b — *Not started*
- **Plan 07-03** — UX: orderpro_to_allegro disable + lista zamówień (items 14-17) — *Not started*
- **Plan 07-04** — Tests: AllegroTokenManager + AllegroOrderImportService unit tests — *Not started*
- **Plan 07-05** — InPost: ShipmentProviderInterface implementation — *Not started*
## Completed Milestones

View File

@@ -5,25 +5,26 @@
See: .paul/PROJECT.md (updated 2026-03-12)
**Core value:** Sprzedawca może obsługiwać zamówienia ze wszystkich kanałów sprzedaży i nadawać przesyłki bez przełączania się między platformami.
**Current focus:** Awaiting next milestone. v0.1 complete — uruchom /paul:discuss-milestone lub /paul:milestone.
**Current focus:** Faza 07 — Pre-Expansion Fixes. 5 planów utworzonych, gotowe do APPLY.
## Current Position
Milestone: Awaiting next milestone
Phase: None active
Plan: None
Status: Milestone v0.1 Initial Release complete — ready for next
Last activity: 2026-03-13 — Milestone v0.1 completed
Milestone: v0.2 Pre-Expansion Fixes
Phase: 7 of TBD (07-pre-expansion-fixes) — Planning
Plan: 07-01 do 07-05 CREATED, awaiting approval
Status: PLANy gotowe — wybrać plan do APPLY
Last activity: 2026-03-13 — Faza 07 zaplanowana (5 planów)
Progress:
- v0.1 Initial Release: [██████████] 100% ✓
- v0.2 Pre-Expansion Fixes: [░░░░░░░░░░] 0% (0/5 planów)
## Loop Position
Current loop state:
```
PLAN ──▶ APPLY ──▶ UNIFY
○ ○ [Milestone complete — ready for next]
○ ○ [07-01 plan gotowy — zacznij od /paul:apply 07-01]
```
## Accumulated Context
@@ -91,13 +92,15 @@ Brak.
## Session Continuity
Last session: 2026-03-13
Stopped at: Milestone v0.1 Initial Release complete — /paul:complete-milestone executed
Next action: /paul:discuss-milestone
Resume file: .paul/MILESTONES.md
Stopped at: Faza 07 zaplanowana — 5 planów gotowych do wykonania
Next action: /paul:apply .paul/phases/07-pre-expansion-fixes/07-01-PLAN.md
Resume file: .paul/phases/07-pre-expansion-fixes/07-01-PLAN.md
Resume context:
- v0.1: 6 faz, 15 planów zamknięte
- Deferred: AllegroImportScheduleService (AllegroIntegrationController 25 metod), CI/CD SonarQube GitHub Actions
- git tag: v0.1.0 na main
- 07-01: Performance (N+1 subqueries, information_schema static, DB indexes)
- 07-02: SSL verification + cron throttle DB + migration 000014b
- 07-03: UX fixes (orderpro_to_allegro disable, items 14-17) — ma checkpoint
- 07-04: Tests (AllegroTokenManager + AllegroOrderImportService)
- 07-05: InPost ShipmentProviderInterface — ma checkpoint:decision
---
*STATE.md — Updated after every significant action*

View File

@@ -0,0 +1,220 @@
---
phase: 07-pre-expansion-fixes
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- src/Modules/Orders/OrdersRepository.php
- database/migrations/20260313_000048_add_orders_performance_indexes.sql
autonomous: true
---
<objective>
## Goal
Wyeliminować dwa bottlenecki wydajnościowe w module Orders: 4 correlated subqueries na każdy wiersz listy zamówień oraz zapytanie do information_schema przy każdym żądaniu HTTP.
## Purpose
Lista zamówień jest głównym widokiem aplikacji. Przy 50 wierszach na stronę, correlated subqueries oznaczają 200+ dodatkowych zapytań per page load. `canResolveMappedMedia()` uderza w `information_schema` przy każdym żądaniu do OrdersRepository — notoriously slow na MySQL. Oba problemy narastają z liczbą zamówień.
## Output
- `OrdersRepository::buildListSql()` — 4 subqueries zastąpione aggregating LEFT JOIN
- `OrdersRepository::canResolveMappedMedia()` — wynik cachowany w `static` property
- Nowa migracja z brakującymi indeksami dla `orders` table
</objective>
<context>
## Project Context
@.paul/PROJECT.md
## Source Files
@src/Modules/Orders/OrdersRepository.php
</context>
<acceptance_criteria>
## AC-1: Brak correlated subqueries w liście zamówień
```gherkin
Given OrdersRepository::buildListSql() zawiera 4 correlated subqueries (items_count, items_qty, shipments_count, documents_count)
When metoda buduje SQL dla listy zamówień
Then SQL nie zawiera "(SELECT COUNT(*) FROM order_items" ani podobnych correlated subqueries
AND zwracane kolumny items_count, items_qty, shipments_count, documents_count nadal istnieją
AND logika transformOrderRow() w pełni działa (te pola nadal trafiają do wyniku)
```
## AC-2: information_schema nie jest odpytywany per-request
```gherkin
Given canResolveMappedMedia() używa instance property $this->supportsMappedMedia jako cache
When OrdersRepository jest instanciowany dwukrotnie w tej samej sesji PHP (np. dwa wywołania repository)
Then information_schema.COLUMNS jest zapytany co najwyżej raz na cykl PHP (static property)
```
## AC-3: Brakujące indeksy dodane migracją
```gherkin
Given tabela orders nie ma indeksów na kolumnach source, external_status_id, ordered_at
When tworzona jest nowa migracja i wykonana przez migrator
Then tabela orders ma indeksy na: source, external_status_id, ordered_at
AND migracja jest idempotentna (IF NOT EXISTS lub ADD INDEX IF NOT EXISTS lub ALTER IGNORE)
```
</acceptance_criteria>
<tasks>
<task type="auto">
<name>Task 1: Zamień 4 correlated subqueries na aggregating LEFT JOINs w buildListSql()</name>
<files>src/Modules/Orders/OrdersRepository.php</files>
<action>
W metodzie `buildListSql()` (linie ~135-171) zamień:
```sql
(SELECT COUNT(*) FROM order_items oi WHERE oi.order_id = o.id) AS items_count,
(SELECT COALESCE(SUM(oi.quantity), 0) FROM order_items oi WHERE oi.order_id = o.id) AS items_qty,
(SELECT COUNT(*) FROM order_shipments sh WHERE sh.order_id = o.id) AS shipments_count,
(SELECT COUNT(*) FROM order_documents od WHERE od.order_id = o.id) AS documents_count
```
Na:
```sql
COALESCE(oi_agg.items_count, 0) AS items_count,
COALESCE(oi_agg.items_qty, 0) AS items_qty,
COALESCE(sh_agg.shipments_count, 0) AS shipments_count,
COALESCE(od_agg.documents_count, 0) AS documents_count
```
I dodaj do klauzuli FROM (po istniejących LEFT JOINach, przed WHERE):
```sql
LEFT JOIN (
SELECT order_id,
COUNT(*) AS items_count,
COALESCE(SUM(quantity), 0) AS items_qty
FROM order_items
GROUP BY order_id
) oi_agg ON oi_agg.order_id = o.id
LEFT JOIN (
SELECT order_id, COUNT(*) AS shipments_count
FROM order_shipments
GROUP BY order_id
) sh_agg ON sh_agg.order_id = o.id
LEFT JOIN (
SELECT order_id, COUNT(*) AS documents_count
FROM order_documents
GROUP BY order_id
) od_agg ON od_agg.order_id = o.id
```
UWAGA: buildListSql() generuje SQL jako string — upewnij się że nowe JOINy są wstawione
przed `$whereSql` (który jest konkatenowany na końcu z klauzulą WHERE).
Sprawdź jak wygląda $whereSql — czy zawiera "WHERE" czy tylko "AND ..."?
Jeśli WHERE jest w $whereSql, nowe JOINy idą bezpośrednio przed nim.
NIE zmieniaj metody transformOrderRow() — klucze tablicy pozostają te same.
</action>
<verify>
php -l src/Modules/Orders/OrdersRepository.php
grep -c "SELECT COUNT\|SELECT COALESCE\|subquery" src/Modules/Orders/OrdersRepository.php
# Powinno zwrócić 0 dla wzorców correlated subquery w buildListSql
</verify>
<done>AC-1 satisfied: buildListSql() używa LEFT JOIN zamiast correlated subqueries</done>
</task>
<task type="auto">
<name>Task 2: Zamień instance property na static w canResolveMappedMedia()</name>
<files>src/Modules/Orders/OrdersRepository.php</files>
<action>
Znajdź deklarację instance property (okolice klasy):
```php
private ?bool $supportsMappedMedia = null;
```
Zmień na:
```php
private static ?bool $supportsMappedMedia = null;
```
W metodzie `canResolveMappedMedia()` zmień referencje z `$this->supportsMappedMedia` na
`self::$supportsMappedMedia` (we wszystkich miejscach metody — przypisanie i odczyt).
To jedyna zmiana. Nie modyfikuj logiki zapytania ani żadnej innej metody.
</action>
<verify>
php -l src/Modules/Orders/OrdersRepository.php
grep -n "supportsMappedMedia" src/Modules/Orders/OrdersRepository.php
# Powinno pokazać: private static ?bool i self::$supportsMappedMedia
</verify>
<done>AC-2 satisfied: canResolveMappedMedia() używa static property</done>
</task>
<task type="auto">
<name>Task 3: Migracja z brakującymi indeksami dla tabeli orders</name>
<files>database/migrations/20260313_000048_add_orders_performance_indexes.sql</files>
<action>
Stwórz plik `database/migrations/20260313_000048_add_orders_performance_indexes.sql`.
Przed pisaniem migracji, sprawdź jakie indeksy już istnieją:
```bash
grep -n "INDEX\|KEY " database/migrations/20260302_000018_create_orders_tables_and_schedule.sql
grep -rn "ADD INDEX\|ADD KEY" database/migrations/ | grep -i "order"
```
Migracja powinna dodać brakujące indeksy:
```sql
-- Indeksy dla typowych filtrów i sortowań na liście zamówień
ALTER TABLE orders
ADD INDEX IF NOT EXISTS orders_source_idx (source),
ADD INDEX IF NOT EXISTS orders_external_status_idx (external_status_id),
ADD INDEX IF NOT EXISTS orders_ordered_at_idx (ordered_at),
ADD INDEX IF NOT EXISTS orders_source_status_idx (source, external_status_id);
-- Indeks dla allegro status mapping lookup (JOIN w buildListSql)
ALTER TABLE allegro_order_status_mappings
ADD INDEX IF NOT EXISTS allegro_status_code_idx (allegro_status_code)
-- tylko jeśli ten indeks nie istnieje (sprawdź migration 000038)
```
WAŻNE: Sprawdź aktualny schemat przed dodaniem indeksu — nie twórz duplikatów.
Użyj `IF NOT EXISTS` gdzie MySQL to obsługuje (MySQL 8.0+), albo sformułuj jako
osobne polecenia z IGNORE: `ALTER TABLE orders ADD IGNORE INDEX ...`
Sprawdź też `database/migrations/20260308_000038_ensure_order_status_mappings_table.sql`
aby wiedzieć jakie indeksy allegro_order_status_mappings już ma.
</action>
<verify>
php -l database/migrations/20260313_000048_add_orders_performance_indexes.sql 2>/dev/null || echo "SQL file - no php lint needed"
# Sprawdź czy plik istnieje i nie jest pusty
cat database/migrations/20260313_000048_add_orders_performance_indexes.sql
</verify>
<done>AC-3 satisfied: migracja z indeksami na source, external_status_id, ordered_at istnieje</done>
</task>
</tasks>
<boundaries>
## DO NOT CHANGE
- Logika filtrowania ($whereSql) — tylko zmiana mechanizmu agregacji, nie filtrów
- Metoda transformOrderRow() — zwracane klucze muszą pozostać identyczne
- Inne metody OrdersRepository poza buildListSql() i canResolveMappedMedia()
- Istniejące pliki migracji — tylko NOWY plik 000048
## SCOPE LIMITS
- Tylko optymalizacja zapytania list; nie zmieniamy zapytań detail/find
- Nie dodajemy nowych kolumn ani nie zmieniamy schematu tabel — tylko indeksy
- Nie implementujemy cache'owania na poziomie Redis/Memcached — tylko static property
</boundaries>
<verification>
Przed zamknięciem planu:
- [ ] php -l src/Modules/Orders/OrdersRepository.php — brak błędów
- [ ] grep buildListSql src/Modules/Orders/OrdersRepository.php — brak "SELECT COUNT(*) FROM order_items WHERE oi.order_id"
- [ ] grep "supportsMappedMedia" src/Modules/Orders/OrdersRepository.php — pokazuje "static"
- [ ] Plik migracji 000048 istnieje i jest nieopusty
- [ ] Lista zamówień ładuje się bez błędów PHP
</verification>
<success_criteria>
- 4 correlated subqueries usunięte z buildListSql()
- canResolveMappedMedia() z static property
- Migracja 000048 z indeksami na source, external_status_id, ordered_at
- Zero błędów składniowych PHP
</success_criteria>
<output>
Po zakończeniu utwórz `.paul/phases/07-pre-expansion-fixes/07-01-SUMMARY.md`
</output>

View File

@@ -0,0 +1,288 @@
---
phase: 07-pre-expansion-fixes
plan: 02
type: execute
wave: 1
depends_on: []
files_modified:
- src/Modules/Settings/AllegroApiClient.php
- src/Modules/Settings/AllegroOAuthClient.php
- src/Modules/Settings/ShopproApiClient.php
- src/Modules/Settings/ApaczkaApiClient.php
- src/Core/Application.php
- database/migrations/20260313_000049_add_cron_last_run_at_setting.sql
- .env.example
autonomous: true
---
<objective>
## Goal
Naprawić trzy problemy stabilności przed rozbudową: zduplikowany numer migracji 000014, brak weryfikacji SSL w 4 klientach API, cron throttle przechowywany w sesji zamiast bazie danych.
## Purpose
Każdy nowy klient API dodany bez wzorca SSL verification powiela podatność. Cron throttle w sesji powoduje wielokrotne uruchamianie crona przy wielu aktywnych sesjach. Duplikat migracji wprowadza niejednoznaczność przy deploy na nowe środowisko. Wszystkie trzy to fundament do naprawy przed dodawaniem nowych integracji.
## Output
- 4 pliki ApiClient z `CURLOPT_SSL_VERIFYPEER => true` + `CURLOPT_CAINFO`
- `.env.example` z `CURL_CA_BUNDLE_PATH`
- `Application::isWebCronThrottled()` czytający timestamp z `app_settings` zamiast `$_SESSION`
- Nowa migracja seed dla klucza `cron_web_last_run_at` w `app_settings`
- Migracja 000014b (rename duplikatu)
</objective>
<context>
## Project Context
@.paul/PROJECT.md
## Source Files
@src/Modules/Settings/AllegroApiClient.php
@src/Modules/Settings/AllegroOAuthClient.php
@src/Core/Application.php
@.env.example
</context>
<acceptance_criteria>
## AC-1: SSL weryfikowany w każdym cURL wywołaniu
```gherkin
Given 4 klasy ApiClient (AllegroApiClient, AllegroOAuthClient, ShopproApiClient, ApaczkaApiClient) wykonują cURL bez CURLOPT_SSL_VERIFYPEER
When dowolny ApiClient wykonuje zapytanie HTTP
Then CURLOPT_SSL_VERIFYPEER jest ustawione na true
AND CURLOPT_CAINFO wskazuje na ścieżkę z .env (lub systemowy bundle jako fallback)
AND .env.example zawiera CURL_CA_BUNDLE_PATH z komentarzem
```
## AC-2: Web cron throttle oparty na DB, nie sesji
```gherkin
Given Application::isWebCronThrottled() zapisuje/czyta timestamp z $_SESSION['cron_web_last_run_at']
When isWebCronThrottled() jest wywołane
Then timestamp jest czytany z app_settings (klucz: cron_web_last_run_at) zamiast $_SESSION
AND zapis po uruchomieniu crona idzie do app_settings (UPDATE lub INSERT ON DUPLICATE KEY)
AND $_SESSION['cron_web_last_run_at'] nie jest już używany
```
## AC-3: Migracja 000014 zdeduplikowana
```gherkin
Given dwa pliki mają sekwencję 000014 w nazwie
When sprawdzasz listę migracji
Then jeden z nich jest przemianowany na 000014b
AND plik zawiera idempotentny INSERT (ON DUPLICATE KEY UPDATE lub INSERT IGNORE)
żeby powtórne uruchomienie na istniejącej bazie nie powodowało błędu
```
</acceptance_criteria>
<tasks>
<task type="auto">
<name>Task 1: Dodaj SSL verification do 4 ApiClient klas</name>
<files>
src/Modules/Settings/AllegroApiClient.php,
src/Modules/Settings/AllegroOAuthClient.php,
src/Modules/Settings/ShopproApiClient.php,
src/Modules/Settings/ApaczkaApiClient.php,
.env.example
</files>
<action>
Dla każdej z 4 klas:
1. Odczytaj plik — zidentyfikuj wszystkie `curl_setopt_array($ch, [...])` wywołania
2. Do każdego array dodaj (jeśli nie ma):
```php
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
CURLOPT_CAINFO => $this->getCaBundlePath(),
```
3. Dodaj prywatną metodę pomocniczą do każdej klasy:
```php
private function getCaBundlePath(): string
{
$envPath = (string) ($_ENV['CURL_CA_BUNDLE_PATH'] ?? '');
if ($envPath !== '' && is_file($envPath)) {
return $envPath;
}
// Windows XAMPP fallback
$xamppCa = 'C:/xampp/php/extras/ssl/cacert.pem';
if (is_file($xamppCa)) {
return $xamppCa;
}
// System default (Linux/macOS)
return '/etc/ssl/certs/ca-certificates.crt';
}
```
Jeśli wszystkie 4 klasy mają wspólny trait lub klasę bazową — wydziel metodę tam.
Jeśli nie — dodaj do każdej osobno (duplikacja jest OK, unikamy przedwczesnej abstrakcji).
4. W `.env.example` dodaj sekcję:
```
# SSL/TLS — ścieżka do CA bundle dla cURL
# Windows XAMPP: C:/xampp/php/extras/ssl/cacert.pem
# Linux: /etc/ssl/certs/ca-certificates.crt
CURL_CA_BUNDLE_PATH=
```
WAŻNE: Nie dodawaj CURLOPT_SSL_VERIFYPEER => false nigdzie. Jeśli CA bundle nie istnieje,
metoda getCaBundlePath() zwraca ścieżkę — PHP zgłosi błąd cURL jeśli plik nie istnieje.
To jest pożądane zachowanie (fail-fast, nie silent MITM).
</action>
<verify>
php -l src/Modules/Settings/AllegroApiClient.php
php -l src/Modules/Settings/AllegroOAuthClient.php
php -l src/Modules/Settings/ShopproApiClient.php
php -l src/Modules/Settings/ApaczkaApiClient.php
grep -n "CURLOPT_SSL_VERIFYPEER" src/Modules/Settings/AllegroApiClient.php
grep -c "CURLOPT_SSL_VERIFYPEER" src/Modules/Settings/ShopproApiClient.php
</verify>
<done>AC-1 satisfied: wszystkie 4 ApiClienty mają CURLOPT_SSL_VERIFYPEER => true</done>
</task>
<task type="auto">
<name>Task 2: Cron throttle z $_SESSION → app_settings DB</name>
<files>
src/Core/Application.php,
database/migrations/20260313_000049_add_cron_last_run_at_setting.sql
</files>
<action>
W `Application.php`, znajdź metodę `isWebCronThrottled()` (linie ~268-285).
Obecna logika:
```php
$lastRunAt = isset($_SESSION['cron_web_last_run_at']) ? (int) $_SESSION['cron_web_last_run_at'] : 0;
// ...
$_SESSION['cron_web_last_run_at'] = $now;
```
Zastąp odczyt sesji odczytem z DB:
```php
$lastRunAt = $this->getWebCronLastRunAt();
```
I zapis sesji zapisem do DB:
```php
$this->setWebCronLastRunAt($now);
```
Dodaj prywatne metody pomocnicze:
```php
private function getWebCronLastRunAt(): int
{
try {
$stmt = $this->db->pdo()->prepare(
"SELECT setting_value FROM app_settings WHERE setting_key = 'cron_web_last_run_at'"
);
$stmt->execute();
$value = $stmt->fetchColumn();
return $value !== false ? (int) $value : 0;
} catch (Throwable) {
return 0;
}
}
private function setWebCronLastRunAt(int $timestamp): void
{
try {
$this->db->pdo()->prepare(
"INSERT INTO app_settings (setting_key, setting_value, created_at, updated_at)
VALUES ('cron_web_last_run_at', :ts, NOW(), NOW())
ON DUPLICATE KEY UPDATE setting_value = :ts, updated_at = NOW()"
)->execute(['ts' => (string) $timestamp]);
} catch (Throwable) {
// throttle failure is non-critical — GET_LOCK is the real guard
}
}
```
Sprawdź jak Application uzyskuje dostęp do PDO — jeśli przez `$this->db->pdo()` lub
`$this->db->query()` — dostosuj. Sprawdź jak CronHandlerFactory i inne miejsca używają DB.
Usuń odwołania do `$_SESSION['cron_web_last_run_at']` całkowicie.
Stwórz migrację `20260313_000049_add_cron_last_run_at_setting.sql`:
```sql
INSERT INTO app_settings (setting_key, setting_value, created_at, updated_at)
VALUES ('cron_web_last_run_at', '0', NOW(), NOW())
ON DUPLICATE KEY UPDATE updated_at = updated_at;
```
</action>
<verify>
php -l src/Core/Application.php
grep -n "SESSION.*cron_web\|cron_web.*SESSION" src/Core/Application.php
# Powinno zwrócić 0 wyników
grep -n "getWebCronLastRunAt\|setWebCronLastRunAt" src/Core/Application.php
</verify>
<done>AC-2 satisfied: isWebCronThrottled() nie używa $_SESSION, czyta/zapisuje app_settings</done>
</task>
<task type="auto">
<name>Task 3: Rename duplikatu migracji 000014</name>
<files>
database/migrations/20260301_000014b_add_products_sku_format_setting.sql
</files>
<action>
Migracja `20260301_000014_add_products_sku_format_setting.sql` ma ten sam numer co
`20260227_000014_create_product_integration_translations.sql`.
Ponieważ migrator śledzi migracje po **pełnej nazwie pliku** (nie numerze sekwencji),
rename jest bezpieczny dla nowych instalacji. Dla istniejących instalacji plik pod nową
nazwą będzie wyglądał jako nowy — dlatego zawartość musi być idempotentna.
Kroki:
1. Odczytaj zawartość `20260301_000014_add_products_sku_format_setting.sql`
2. Sprawdź czy INSERT używa `ON DUPLICATE KEY UPDATE` lub `INSERT IGNORE`
- Jeśli nie: zmień INSERT na idempotentny (INSERT IGNORE lub ON DUPLICATE KEY)
3. Utwórz nowy plik `20260301_000014b_add_products_sku_format_setting.sql` z zaktualizowaną treścią
4. Usuń stary plik `20260301_000014_add_products_sku_format_setting.sql`
UWAGA: Użyj `git mv` (przez bash) żeby git śledził rename:
```bash
git mv "database/migrations/20260301_000014_add_products_sku_format_setting.sql" \
"database/migrations/20260301_000014b_add_products_sku_format_setting.sql"
```
Potem edytuj plik (jeśli INSERT wymaga korekty na idempotentny).
</action>
<verify>
ls database/migrations/ | grep "000014"
# Powinno pokazać: 20260227_000014_create... i 20260301_000014b_add...
# NIE powinno być: 20260301_000014_add... (stary plik)
</verify>
<done>AC-3 satisfied: brak duplikatu, jeden plik przemianowany na 000014b</done>
</task>
</tasks>
<boundaries>
## DO NOT CHANGE
- Logika biznesowa żadnego ApiClienta — tylko dodanie SSL options do istniejących curl_setopt_array
- GET_LOCK logika w Application.php — nie ruszamy mutex, tylko session→DB dla timestamp
- Inne klucze w app_settings
- Istniejące migracje poza rename 000014
## SCOPE LIMITS
- Nie implementuj rotacji CSRF tokenów (osobny deferred concern)
- Nie dodawaj path traversal check dla label files (osobny deferred concern)
- CA bundle path — środowisko aplikacyjne, nie certyfikat self-signed
</boundaries>
<verification>
Przed zamknięciem planu:
- [ ] php -l na wszystkich 4 ApiClient plikach
- [ ] php -l src/Core/Application.php
- [ ] grep "SSL_VERIFYPEER" — każdy ApiClient ma ten wpis
- [ ] grep "SESSION.*cron_web" Application.php — 0 wyników
- [ ] ls database/migrations/ | grep "000014" — tylko 000014 i 000014b
- [ ] Plik migracji 000049 istnieje
</verification>
<success_criteria>
- 4 ApiClienty z SSL verification
- .env.example z CURL_CA_BUNDLE_PATH
- Application::isWebCronThrottled() bez $_SESSION
- Migracja 000049 (cron_web_last_run_at seed)
- Rename 000014 → 000014b
- Zero błędów składniowych PHP
</success_criteria>
<output>
Po zakończeniu utwórz `.paul/phases/07-pre-expansion-fixes/07-02-SUMMARY.md`
</output>

View File

@@ -0,0 +1,286 @@
---
phase: 07-pre-expansion-fixes
plan: 03
type: execute
wave: 1
depends_on: []
files_modified:
- src/Modules/Settings/AllegroStatusSyncService.php
- resources/views/settings/allegro.php
- resources/views/orders/list.php
- resources/scss/modules/_orders.scss
autonomous: false
---
<objective>
## Goal
Naprawić 5 problemów UX/behawioralnych widocznych dla użytkownika: cichy no-op synchronizacji statusów, kolejność source/ID w liście zamówień, brak kolorowania statusów, błędna etykieta źródła, zbyt jasne obramowanie pól formularza.
## Purpose
Użytkownik konfiguruje synchronizację "orderPRO → Allegro" i aplikacja loguje "sukces" mimo że nic nie robi. To aktywnie wprowadza w błąd. Pozostałe 4 to widoczne błędy UX (todo.md items 14-17) które obniżają czytelność głównego widoku listy zamówień — ekranu który użytkownik widzi najczęściej.
## Output
- AllegroStatusSyncService: `ok: false` dla nieopracowanego kierunku + UI option disabled
- Lista zamówień: source przed ID, integracja zamiast "shopPRO", "ID:" prefix
- Lista zamówień: statusy kolorowane zgodnie z ustawieniami (jeśli mapping istnieje)
- SCSS: ciemniejsze obramowanie inputów/select/textarea
</objective>
<context>
## Project Context
@.paul/PROJECT.md
## Source Files
@src/Modules/Settings/AllegroStatusSyncService.php
@resources/views/settings/allegro.php
@resources/views/orders/list.php
@resources/scss/modules/_orders.scss
</context>
<acceptance_criteria>
## AC-1: orderpro_to_allegro nie daje false-positive sukcesu
```gherkin
Given użytkownik ustawił kierunek sync "orderPRO Allegro" w ustawieniach
When cron uruchamia synchronizację statusów
Then serwis zwraca ['ok' => false, 'message' => 'Kierunek orderPRO -> Allegro nie jest jeszcze wdrożony.']
AND log crona odróżnia to od sukcesów (ok: false)
```
## AC-2: Opcja sync direction "orderPRO → Allegro" oznaczona jako niedostępna
```gherkin
Given w formularzu ustawień Allegro jest select z opcjami kierunku synchronizacji
When użytkownik otwiera formularz ustawień
Then opcja "orderPRO Allegro" jest widoczna ale wyraźnie oznaczona jako "(wkrótce)" lub disabled
AND formularz nie pozwala zapisać tej opcji bez ostrzeżenia
```
## AC-3: W liście zamówień source wyświetla się przed ID, z etykietą "ID:"
```gherkin
Given wiersz zamówienia w div.orders-ref__meta pokazuje kolejność: [ID][source]
When renderowana jest lista zamówień
Then kolejność to: [source][ID], a ID ma prefix "ID:"
AND dla zamówień z integracji shopPRO wyświetlana jest konkretna nazwa integracji (nie "shopPRO")
```
## AC-4: Statusy zamówień kolorowane na liście
```gherkin
Given administrator skonfigurował kolory statusów w ustawieniach (tabela allegro_order_status_mappings lub odpowiednik)
When renderowana jest lista zamówień z zamówieniami mającymi zmapowane statusy
Then komórka/badge statusu ma inline styl lub klasę CSS odpowiadającą skonfigurowanemu kolorowi
AND dla niemapowanych statusów wyświetlany jest tekst bez koloru (neutralny fallback)
```
## AC-5: Ciemniejsze obramowanie pól formularza
```gherkin
Given input, select, textarea mają jasne obramowanie (zbyt słabo widoczne)
When użytkownik wyświetla dowolny formularz w aplikacji
Then obramowanie pól jest zauważalnie ciemniejsze (wyższy kontrast z tłem)
```
</acceptance_criteria>
<tasks>
<task type="auto">
<name>Task 1: AllegroStatusSyncService — ok:false + UI disabled dla orderpro_to_allegro</name>
<files>
src/Modules/Settings/AllegroStatusSyncService.php,
resources/views/settings/allegro.php
</files>
<action>
**W AllegroStatusSyncService.php (linie ~39-45):**
Zmień:
```php
if ($direction === self::DIRECTION_ORDERPRO_TO_ALLEGRO) {
return [
'ok' => true, // ← BUG: powinno być false
...
];
}
```
Na:
```php
if ($direction === self::DIRECTION_ORDERPRO_TO_ALLEGRO) {
return [
'ok' => false,
'direction' => $direction,
'processed' => 0,
'message' => 'Kierunek orderPRO -> Allegro nie jest jeszcze wdrożony.',
];
}
```
**W resources/views/settings/allegro.php (linie ~260-267):**
Znajdź select `status_sync_direction`. Zmień opcję `orderpro_to_allegro`:
```php
<option value="orderpro_to_allegro"
<?= $statusSyncDirection === 'orderpro_to_allegro' ? ' selected' : '' ?>
disabled>
<?= $e($t('settings.allegro.settings.status_sync_direction_orderpro_to_allegro')) ?>
(wkrótce)
</option>
```
Dodaj `disabled` do atrybutu opcji. Jeśli aktualnie wybrana wartość to `orderpro_to_allegro`
(istniejące ustawienie w DB), disabled option z selected nadal wyświetla się poprawnie —
użytkownik widzi co było wybrane, ale nie może ponownie wybrać.
NIE zmieniaj logiki AllegroStatusSyncService poza tym early return.
</action>
<verify>
php -l src/Modules/Settings/AllegroStatusSyncService.php
grep -A5 "DIRECTION_ORDERPRO_TO_ALLEGRO" src/Modules/Settings/AllegroStatusSyncService.php
# Powinno pokazać ok: false
grep -A3 "orderpro_to_allegro" resources/views/settings/allegro.php
# Powinno pokazać disabled
</verify>
<done>AC-1 i AC-2 satisfied: ok:false + opcja UI disabled</done>
</task>
<task type="auto">
<name>Task 2: Lista zamówień — source przed ID, etykieta integracji, prefix "ID:"</name>
<files>resources/views/orders/list.php</files>
<action>
Przeczytaj plik `resources/views/orders/list.php` dokładnie.
Znajdź fragment renderujący `orders-ref__meta` (lub analogiczny div z source i order ID).
Obecna kolejność (wg todo.md item 15):
```html
<div class="orders-ref__meta">
<span>[ID zamówienia]</span>
<span>[source: "allegro"/"shopPRO"]</span>
</div>
```
**Zmiana 1 (item 15): Odwróć kolejność** — source przed ID:
```html
<div class="orders-ref__meta">
<span>[source]</span>
<span>ID: [ID zamówienia]</span>
</div>
```
**Zmiana 2 (item 17): Zamiast "shopPRO" pokaż konkretną nazwę integracji.**
Sprawdź jakie pole w danych zamówienia zawiera nazwę integracji.
Odczytaj co jest dostępne w `$order` array w widoku — szukaj pól: `integration_name`,
`source`, `source_name`, `integration_id`.
Jeśli `$order['source']` zawiera np. "shoppro" a konkretna nazwa integracji jest dostępna
jako osobne pole — użyj tej nazwy dla shopPRO, zachowaj "allegro" dla Allegro.
Jeśli nie ma pola z konkretną nazwą integracji w danych zwracanych przez OrdersRepository,
sprawdź co jest dostępne i użyj dostępnego pola. Nie dodawaj nowych DB queries w widoku.
Dodaj prefix "ID: " przed source_order_id lub external_order_id (którego używa widok).
</action>
<verify>
php -l resources/views/orders/list.php
grep -n "orders-ref__meta\|source_order_id\|source.*span\|ID:" resources/views/orders/list.php
</verify>
<done>AC-3 satisfied: source przed ID, prefix "ID:", integracja zamiast "shopPRO"</done>
</task>
<task type="checkpoint:human-verify" gate="blocking">
<what-built>
1. AllegroStatusSyncService zwraca ok:false dla orderpro_to_allegro
2. UI opcja "orderPRO → Allegro" oznaczona jako disabled/(wkrótce)
3. Lista zamówień: source przed ID, prefix "ID:", etykieta integracji
</what-built>
<how-to-verify>
1. Uruchom aplikację (XAMPP)
2. Przejdź do Ustawień → Integracje → Allegro → zakładka synchronizacji
- Sprawdź że opcja "orderPRO → Allegro" jest wyświetlona jako disabled/(wkrótce)
3. Przejdź do listy zamówień (/orders)
- Sprawdź kolejność: source powinien być przed ID
- Sprawdź prefix "ID:"
- Sprawdź że shopPRO pokazuje konkretną nazwę integracji
4. Wpisz "approved" lub opisz co nie gra
</how-to-verify>
<resume-signal>Wpisz "approved" aby kontynuować do kolorowania statusów i SCSS, lub opisz problemy</resume-signal>
</task>
<task type="auto">
<name>Task 3: Statusy kolorowane na liście + ciemniejsze obramowanie formularzy</name>
<files>
resources/views/orders/list.php,
resources/scss/modules/_orders.scss
</files>
<action>
**Item 16: Kolorowanie statusów.**
Przeczytaj `resources/views/orders/list.php` — znajdź gdzie renderowany jest status zamówienia.
Sprawdź co jest w `$order['effective_status_id']` lub analogicznym polu.
W OrdersRepository::buildListSql() jest już JOIN do `allegro_order_status_mappings` —
sprawdź jakie kolumny są pobierane z tego JOINu (np. `asm.color` czy `asm.label_color`).
Jeśli kolor jest już dostępny w danych widoku jako pole `status_color` lub `color` —
dodaj do elementu statusu inline style:
```php
<?php $statusColor = $order['status_color'] ?? ''; ?>
<span class="order-status"
<?= $statusColor !== '' ? 'style="background-color:' . $e($statusColor) . '"' : '' ?>>
<?= $e($order['effective_status_id'] ?? $order['external_status_id'] ?? '') ?>
</span>
```
Jeśli kolor nie jest dostępny w danych widoku — sprawdź OrdersRepository::transformOrderRow()
i buildListSql() co jest pobierane z asm (allegro_order_status_mappings JOIN). Jeśli kolumna
`color` istnieje w tabeli — dodaj ją do SELECT i transformOrderRow(). Jeśli tabela nie ma
kolumny `color` — nie dodawaj, zaznacz w SUMMARY że wymaga migracji.
**Item 14: Ciemniejsze obramowanie inputów.**
Znajdź w `resources/scss/modules/_orders.scss` lub `resources/scss/_global.scss`
lub analogicznym pliku SCSS definicję borderów dla `input, select, textarea`.
Zmień kolor border na ciemniejszy o ok. 30-40% (np. z `#ddd` na `#aaa` lub z `#ccc` na `#999`).
Sprawdź najpierw które pliki SCSS definiują style formularzy — może to być inny plik niż _orders.scss.
Szukaj: `grep -rn "border.*input\|input.*border\|\.form-control" resources/scss/`
</action>
<verify>
php -l resources/views/orders/list.php
# Znajdź plik SCSS z border input i zweryfikuj zmianę:
grep -rn "form-control\|input.*border\|border.*input" resources/scss/ | head -10
</verify>
<done>AC-4 i AC-5 satisfied: statusy kolorowane, obramowania ciemniejsze</done>
</task>
</tasks>
<boundaries>
## DO NOT CHANGE
- Logika synchronizacji statusów poza early return (nie implementuj orderpro_to_allegro)
- Inne widoki poza orders/list.php i settings/allegro.php
- Publiczne API controllera AllegroStatusSyncService
- Routing i nazwy tras
## SCOPE LIMITS
- Nie implementuj synchronizacji orderPRO→Allegro — tylko disable UI + ok:false
- Kolorowanie statusów tylko jeśli kolor jest dostępny bez dodatkowych DB queries w widoku
- Border color — tylko zmiana istniejącej wartości, nie nowy system designu
- Nie ruszaj resources/views/settings/shoppro.php (ma analogiczną opcję — osobny scope)
</boundaries>
<verification>
Przed zamknięciem planu:
- [ ] php -l AllegroStatusSyncService.php — brak błędów
- [ ] grep ok.*false AllegroStatusSyncService — widoczny w early return
- [ ] grep disabled resources/views/settings/allegro.php — opcja UI
- [ ] php -l resources/views/orders/list.php — brak błędów
- [ ] Checkpoint human-verify zaliczony
</verification>
<success_criteria>
- AllegroStatusSyncService: ok:false dla orderpro_to_allegro
- UI: opcja disabled
- Lista zamówień: source | "ID: [id]" w poprawnej kolejności
- Lista zamówień: integracja zamiast "shopPRO"
- Statusy: kolorowane gdy kolor dostępny
- Formularze: ciemniejsze obramowanie
</success_criteria>
<output>
Po zakończeniu utwórz `.paul/phases/07-pre-expansion-fixes/07-03-SUMMARY.md`
</output>

View File

@@ -0,0 +1,179 @@
---
phase: 07-pre-expansion-fixes
plan: 04
type: tdd
wave: 2
depends_on: []
files_modified:
- tests/Unit/AllegroTokenManagerTest.php
- tests/Unit/AllegroOrderImportServiceTest.php
autonomous: true
---
<objective>
## Goal
Dodać testy jednostkowe dla dwóch krytycznych ścieżek: logiki odświeżania tokenów OAuth (AllegroTokenManager) i happy path importu zamówień Allegro (AllegroOrderImportService). PHPUnit jest już skonfigurowany — testy muszą przejść `vendor/bin/phpunit`.
## Purpose
AllegroTokenManager zawiera złożone edge case'y (token expired, refresh token empty, write-then-re-read) które są krytyczne dla działania wszystkich integracji Allegro. Bez testów każda zmiana w okolicach token managementu jest ryzykowna. AllegroOrderImportService importSingleOrder() ma catch(Throwable) w kilku miejscach — bez testów błędy mogą być swallowane po cichu.
## Output
- `tests/Unit/AllegroTokenManagerTest.php` — 5+ testów pokrywających logikę tokenów
- `tests/Unit/AllegroOrderImportServiceTest.php` — 3+ testy happy path + jedna ścieżka błędu
</objective>
<context>
## Project Context
@.paul/PROJECT.md
## Source Files
@src/Modules/Settings/AllegroTokenManager.php
@src/Modules/Settings/AllegroOrderImportService.php
@tests/bootstrap.php
@phpunit.xml
</context>
<acceptance_criteria>
## AC-1: AllegroTokenManager — logika refresh pokryta testami
```gherkin
Given AllegroTokenManager zarządza tokenami OAuth z logiką: odśwież jeśli wygaśnie w ciągu 5 min
When testy jednostkowe są uruchamiane przez PHPUnit
Then istnieje test dla: token świeży (brak refresh)
AND test dla: token wygasły lub wygaśnie za < 5 min (refresh triggered)
AND test dla: brak refresh token (oczekiwany wyjątek lub ok:false)
AND wszystkie testy przechodzą: vendor/bin/phpunit tests/Unit/AllegroTokenManagerTest.php
```
## AC-2: AllegroOrderImportService — import happy path pokryty
```gherkin
Given AllegroOrderImportService::importSingleOrder() pobiera zamówienie i zapisuje je do DB
When testy jednostkowe są uruchamiane
Then istnieje test dla: import sukces (pełne zamówienie ze wszystkimi polami)
AND test dla: import zwraca dane zamówienia (assert na kluczowe pola odpowiedzi)
AND testy nie uderzają w prawdziwą bazę ani API (mocki/stubs)
AND testy przechodzą: vendor/bin/phpunit tests/Unit/AllegroOrderImportServiceTest.php
```
## AC-3: Wszystkie nowe testy przechodzą
```gherkin
Given nowe pliki testowe istnieją w tests/Unit/
When uruchamiasz vendor/bin/phpunit
Then zero FAILURES, zero ERRORS dla nowych test files
```
</acceptance_criteria>
<tasks>
<task type="auto">
<name>Task 1: Testy dla AllegroTokenManager</name>
<files>
tests/Unit/AllegroTokenManagerTest.php,
src/Modules/Settings/AllegroTokenManager.php
</files>
<action>
Przeczytaj `src/Modules/Settings/AllegroTokenManager.php` dokładnie.
Zrozum: konstruktor, zależności, logika `resolveToken()`, kiedy refresh jest wywoływany.
Stwórz `tests/Unit/AllegroTokenManagerTest.php`:
```php
<?php
declare(strict_types=1);
namespace Tests\Unit;
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\MockObject\MockObject;
// Dodaj odpowiednie use statements dla AllegroTokenManager i jego zależności
```
Wymagane scenariusze testowe (co najmniej):
1. **Token świeży** — expires_at > now + 5min → resolveToken() zwraca token bez refresh
2. **Token wygaśnie za < 5 min** — expires_at < now + 300s → resolveToken() wywołuje refresh
3. **Token już wygasły** — expires_at < now → resolveToken() wywołuje refresh
4. **Brak refresh token** — token wygasły, refresh_token pusty → oczekiwany wyjątek lub failure signal
5. **Write-then-re-read** — po refresh, token jest odczytany z repo (nie z odpowiedzi API)
Używaj PHPUnit Mock Objects dla zależności (AllegroOAuthClient, repository).
Sprawdź które dependency injection AllegroTokenManager przyjmuje w konstruktorze.
Mockuj zewnętrzne zależności — testy NIE mogą uderzać w Allegro API ani DB.
Stosuj `setUp()` żeby nie powtarzać kodu inicjalizacyjnego.
</action>
<verify>
php -l tests/Unit/AllegroTokenManagerTest.php
vendor/bin/phpunit tests/Unit/AllegroTokenManagerTest.php --testdox
</verify>
<done>AC-1 i AC-3 satisfied: 5+ testów AllegroTokenManager, wszystkie zielone</done>
</task>
<task type="auto">
<name>Task 2: Testy dla AllegroOrderImportService</name>
<files>
tests/Unit/AllegroOrderImportServiceTest.php,
src/Modules/Settings/AllegroOrderImportService.php
</files>
<action>
Przeczytaj `src/Modules/Settings/AllegroOrderImportService.php` dokładnie.
Zidentyfikuj: zależności konstruktora, `importSingleOrder()` flow, co zwraca.
Stwórz `tests/Unit/AllegroOrderImportServiceTest.php`.
Wymagane scenariusze (co najmniej):
1. **Happy path** — API zwraca poprawne zamówienie → importSingleOrder() zwraca sukces
- Mock AllegroApiClient który zwraca fixture z polami zamówienia
- Assert że wynik zawiera ['ok' => true] lub odpowiednik sukcesu
- Assert że OrdersRepository::upsert (lub odpowiednia metoda) była wywołana
2. **401 retry** — jeśli importSingleOrder() ma logikę retry przy 401 → test że retry jest wywoływany
3. **API error** — AllegroApiClient rzuca wyjątek → importSingleOrder() zwraca ['ok' => false]
lub propaguje wyjątek (sprawdź aktualną semantykę)
Uwaga na catch(Throwable) bloki — sprawdź czy są testowane i czy swallują w sposób
widoczny (logowanie) czy całkowicie cichy. Jeśli całkowicie cichy — zanotuj w SUMMARY.
Używaj fixture danych (tablica PHP) dla response API — nie potrzebujesz realnej struktury
API, wystarczy minimum wymagane przez metodę mapującą.
</action>
<verify>
php -l tests/Unit/AllegroOrderImportServiceTest.php
vendor/bin/phpunit tests/Unit/AllegroOrderImportServiceTest.php --testdox
</verify>
<done>AC-2 i AC-3 satisfied: 3+ testów AllegroOrderImportService, wszystkie zielone</done>
</task>
</tasks>
<boundaries>
## DO NOT CHANGE
- Logika produkcyjna AllegroTokenManager i AllegroOrderImportService — tylko testy
- Istniejące pliki w tests/ — bootstrap.php, inne pliki Unit (jeśli istnieją)
- phpunit.xml — nie modyfikuj konfiguracji, tylko dodaj nowe pliki testowe
## SCOPE LIMITS
- Tylko testy jednostkowe z mockami — bez testów integracyjnych z realnym DB/API
- Nie dodawaj testów dla innych klas poza AllegroTokenManager i AllegroOrderImportService
- Jeśli zależności są trudne do mockowania — użyj minimalnego zestawu testów (happy path + 1 error path)
- ShopproOrdersSyncService — nie w tym planie
</boundaries>
<verification>
Przed zamknięciem planu:
- [ ] php -l tests/Unit/AllegroTokenManagerTest.php
- [ ] php -l tests/Unit/AllegroOrderImportServiceTest.php
- [ ] vendor/bin/phpunit tests/Unit/ — zero FAILURES, zero ERRORS
- [ ] AllegroTokenManagerTest.php: min. 4 metody testowe
- [ ] AllegroOrderImportServiceTest.php: min. 3 metody testowe
</verification>
<success_criteria>
- tests/Unit/AllegroTokenManagerTest.php: 5+ testów, zielone
- tests/Unit/AllegroOrderImportServiceTest.php: 3+ testów, zielone
- vendor/bin/phpunit --testdox: czyste wyjście dla obu plików
</success_criteria>
<output>
Po zakończeniu utwórz `.paul/phases/07-pre-expansion-fixes/07-04-SUMMARY.md`
</output>

View File

@@ -0,0 +1,220 @@
---
phase: 07-pre-expansion-fixes
plan: 05
type: execute
wave: 2
depends_on: []
files_modified:
- src/Modules/Settings/InpostShipmentService.php
- src/Modules/Shipments/ShipmentController.php
- src/Modules/Shipments/ShipmentProviderRegistry.php
- src/Core/Application.php
autonomous: false
---
<objective>
## Goal
Zastąpić workaround `inpost → allegro_wza` prawdziwym `InpostShipmentService implements ShipmentProviderInterface`, korzystającym z credentiali z `InpostIntegrationRepository`.
## Purpose
Obecnie InPost shipments są tworzone przez Allegro WZA API, co oznacza że użytkownik potrzebuje aktywnej integracji Allegro żeby nadawać przez InPost. To jest cichy bloker dla użytkowników z InPost-only. Każdy nowy przewoźnik dodany bez własnego providera będzie powielał ten pattern.
## Output
- `src/Modules/Settings/InpostShipmentService.php` — implementacja ShipmentProviderInterface
- `ShipmentController.php` — usunięty workaround remap `inpost → allegro_wza`
- Wiring w Application.php / ShipmentProviderRegistry
</objective>
<context>
## Project Context
@.paul/PROJECT.md
## Source Files
@src/Modules/Shipments/ShipmentProviderInterface.php
@src/Modules/Shipments/ShipmentController.php
@src/Modules/Settings/InpostIntegrationRepository.php
@src/Modules/Settings/AllegroShipmentService.php
</context>
<acceptance_criteria>
## AC-1: InpostShipmentService implementuje ShipmentProviderInterface
```gherkin
Given ShipmentProviderInterface definiuje kontrakt dla providerów przesyłek
When tworzysz InpostShipmentService
Then klasa implementuje ShipmentProviderInterface
AND php -l nie zgłasza błędów
AND klasa poprawnie typuje wszystkie wymagane metody interfejsu
```
## AC-2: InPost shipments tworzone przez InPost API (nie Allegro WZA)
```gherkin
Given użytkownik ma skonfigurowane credentiale InPost (token w InpostIntegrationRepository)
AND nie ma skonfigurowanej integracji Allegro
When tworzy przesyłkę InPost przez ShipmentController
Then ShipmentController używa InpostShipmentService (nie AllegroShipmentService)
AND workaround "if (inpost) { providerCode = allegro_wza }" jest usunięty
```
## AC-3: Brak regresji dla istniejących Allegro WZA shipments
```gherkin
Given użytkownik tworzy przesyłkę przez Allegro WZA (provider_code = allegro_wza)
When ShipmentController przetwarza żądanie
Then flow Allegro WZA działa dokładnie jak przed zmianami
AND InPost nie jest mylony z Allegro WZA
```
</acceptance_criteria>
<tasks>
<task type="checkpoint:decision" gate="blocking">
<decision>Który endpoint InPost API do tworzenia paczek?</decision>
<context>
InPost ma dwa API:
A) Allegro WZA (paczkomaty przez Allegro) — obecny workaround, wymaga Allegro auth
B) InPost ShipX API (natywne API InPost) — bezpośrednie, wymaga tokenu InPost
InpostIntegrationRepository przechowuje credentiale — sprawdź jakie pola.
Odczytaj: src/Modules/Settings/InpostIntegrationRepository.php
Opcja A: Zaimplementuj natywny InPost ShipX API (wymaga dokumentacji i tokenu)
Opcja B: Wydziel logikę Allegro WZA do oddzielnego providera bez remapu,
a InPost nadal przez Allegro WZA ale bez tajnego remapu (jawny config)
</context>
<options>
<option id="shipx">
<name>Natywny InPost ShipX API</name>
<pros>Pełna niezależność od Allegro; własne credentiale wystarczą; długoterminowo poprawne</pros>
<cons>Wymaga znajomości ShipX API; potrzebne credentiale testowe do weryfikacji</cons>
</option>
<option id="allegro-wza-explicit">
<name>Allegro WZA ale jawnie skonfigurowany (bez tajnego remapu)</name>
<pros>Szybkie; nie wymaga nowej integracji API; eliminuje workaround kod</pros>
<cons>Nadal wymaga integracji Allegro; nie rozwiązuje problemu InPost-only użytkowników</cons>
</option>
</options>
<resume-signal>Wybierz: "shipx" lub "allegro-wza-explicit", lub opisz inne podejście</resume-signal>
</task>
<task type="auto">
<name>Task 1: Implementuj InpostShipmentService zgodnie z wybraną opcją</name>
<files>
src/Modules/Settings/InpostShipmentService.php,
src/Modules/Settings/InpostIntegrationRepository.php
</files>
<action>
Przeczytaj `src/Modules/Shipments/ShipmentProviderInterface.php` — zrozum wymagany kontrakt.
Przeczytaj `src/Modules/Settings/AllegroShipmentService.php` — jako wzorzec implementacji.
Przeczytaj `src/Modules/Settings/InpostIntegrationRepository.php` — jakie credentiale są dostępne.
**Jeśli wybrano "shipx" (natywny InPost ShipX API):**
- Stwórz `InpostShipmentService implements ShipmentProviderInterface`
- Konstruktor przyjmuje `InpostIntegrationRepository` (dla tokenu)
- Metoda create(): wywołuje ShipX API endpoint tworzenia przesyłki
- Metoda getLabel(): pobiera etykietę przez ShipX API
- Używaj cURL (jak inne ApiClienty) z SSL verification
- Endpointy ShipX: `https://api-shipx-pl.easypack24.net/v1/`
**Jeśli wybrano "allegro-wza-explicit":**
- Stwórz `InpostShipmentService` który wrapuje `AllegroShipmentService`
- Zamiast tajnego remapu — jawna delegacja
- Konstruktor przyjmuje `AllegroShipmentService $allegroService`
- Każda metoda deleguje do $allegroService
W obu przypadkach: namespace App\Modules\Settings, php -l musi przejść.
</action>
<verify>
php -l src/Modules/Settings/InpostShipmentService.php
grep "implements ShipmentProviderInterface" src/Modules/Settings/InpostShipmentService.php
</verify>
<done>AC-1 satisfied: InpostShipmentService implementuje ShipmentProviderInterface</done>
</task>
<task type="auto">
<name>Task 2: Usuń workaround z ShipmentController, podłącz InpostShipmentService</name>
<files>
src/Modules/Shipments/ShipmentController.php,
src/Core/Application.php
</files>
<action>
**W ShipmentController.php:**
Znajdź linie ~164-166:
```php
if ($providerCode === 'inpost') {
$providerCode = 'allegro_wza';
}
```
Usuń ten blok. ShipmentProviderRegistry powinien teraz zawierać 'inpost' jako zarejestrowany provider.
Sprawdź czy jest podobny remap w innych miejscach ShipmentController (linia ~239, ~286) — usuń wszystkie.
**W Application.php (lub ShipmentProviderRegistry):**
Znajdź gdzie rejestrowane są shipment providers.
Dodaj rejestrację InpostShipmentService pod kluczem 'inpost':
```php
$inpostService = new InpostShipmentService(...); // odpowiednie zależności
$providerRegistry->register('inpost', $inpostService);
```
Sprawdź jak AllegroShipmentService jest rejestrowany — użyj tego samego wzorca.
NIE usuwaj rejestracji allegro_wza — nadal musi działać.
</action>
<verify>
php -l src/Modules/Shipments/ShipmentController.php
php -l src/Core/Application.php
grep -n "inpost.*allegro_wza\|allegro_wza.*inpost" src/Modules/Shipments/ShipmentController.php
# Powinno zwrócić 0 — remap usunięty
</verify>
<done>AC-2 i AC-3 satisfied: workaround usunięty, InpostShipmentService zarejestrowany</done>
</task>
<task type="checkpoint:human-verify" gate="blocking">
<what-built>
InpostShipmentService zaimplementowany i podłączony.
Workaround remap inpost→allegro_wza usunięty.
</what-built>
<how-to-verify>
1. Uruchom aplikację (XAMPP)
2. Przejdź do tworzenia przesyłki InPost
3. Sprawdź że przesyłka InPost jest tworzona (lub zwraca czytelny błąd bez PHP errors)
4. Sprawdź że tworzenie przesyłki Allegro WZA nadal działa
5. Sprawdź logi PHP — brak Fatal errors
</how-to-verify>
<resume-signal>Wpisz "approved" jeśli działa, lub opisz błędy</resume-signal>
</task>
</tasks>
<boundaries>
## DO NOT CHANGE
- AllegroShipmentService — nie modyfikuj istniejącej logiki
- ShipmentProviderInterface — nie zmieniaj kontraktu
- Routing (routes/web.php) — nie zmieniaj URL endpointów
## SCOPE LIMITS
- Nie implementuj InPost tracking/status sync — tylko create shipment + label
- Nie zmieniaj UI wyboru przewoźnika — tylko backend provider
- Apaczka, InPost paczkomat przez Allegro WZA nadal działa niezależnie
</boundaries>
<verification>
Przed zamknięciem planu:
- [ ] php -l InpostShipmentService.php
- [ ] php -l ShipmentController.php
- [ ] grep inpost.*allegro_wza ShipmentController.php — 0 wyników
- [ ] Checkpoint human-verify zaliczony
</verification>
<success_criteria>
- InpostShipmentService.php istnieje i implementuje ShipmentProviderInterface
- Remap usunięty z ShipmentController
- Allegro WZA flow bez regresji
- Checkpoint human-verify: approved
</success_criteria>
<output>
Po zakończeniu utwórz `.paul/phases/07-pre-expansion-fixes/07-05-SUMMARY.md`
</output>