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:
@@ -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
|
||||
|
||||
|
||||
@@ -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*
|
||||
|
||||
220
.paul/phases/07-pre-expansion-fixes/07-01-PLAN.md
Normal file
220
.paul/phases/07-pre-expansion-fixes/07-01-PLAN.md
Normal 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>
|
||||
288
.paul/phases/07-pre-expansion-fixes/07-02-PLAN.md
Normal file
288
.paul/phases/07-pre-expansion-fixes/07-02-PLAN.md
Normal 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>
|
||||
286
.paul/phases/07-pre-expansion-fixes/07-03-PLAN.md
Normal file
286
.paul/phases/07-pre-expansion-fixes/07-03-PLAN.md
Normal 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>
|
||||
179
.paul/phases/07-pre-expansion-fixes/07-04-PLAN.md
Normal file
179
.paul/phases/07-pre-expansion-fixes/07-04-PLAN.md
Normal 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>
|
||||
220
.paul/phases/07-pre-expansion-fixes/07-05-PLAN.md
Normal file
220
.paul/phases/07-pre-expansion-fixes/07-05-PLAN.md
Normal 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>
|
||||
Reference in New Issue
Block a user