Compare commits

...

11 Commits

Author SHA1 Message Date
d858f74de3 fix: mojibake w labelkach formularza edycji kategorii (v0.352)
Plik admin/templates/shop-category/category-edit.php zawieral 9 wystapien
double-encoded UTF-8 (UTF-8 zinterpretowanego jako Win-1252 i ponownie
zakodowanego do UTF-8). Etykiety pol formularza pokazywaly mojibake typu
"Treść" zamiast "Treść", "JÄ™zyk domyĹ›lny" zamiast "Język domyślny" itp.

Podmiana 7 unikalnych fraz na poprawne polskie znaki. Grep mojibake po
calym repo (admin/templates/, templates/, autoload/) potwierdza ze to
byl jedyny dotkniety plik.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 22:55:27 +02:00
18bdf8384f build: ver_0.351 - fix metatagow na stronach kategorii/produktu 2026-05-13 22:38:49 +02:00
6c924ae542 fix: wyciek meta_title homepage na podstrony kategorii/artykulu/produktu (v0.351)
LayoutEngine::show() w 3 galeziach (category/article/product) nadpisywal
tylko $page['language']['title'], a meta_title homepage zylo dalej i wygrywalo
w substytucji [TITLE]. Wyodrebniono applyEntityMetaToPage() jako pure-function
ktora zawsze nadpisuje meta_title/meta_keywords/meta_description encji
(nawet pustym/null), eliminujac wyciek.

5 nowych testow w LayoutEngineMetaTagsTest. Suita 846 testow zielona.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 22:38:02 +02:00
255456dcc5 update 2026-04-30 02:39:43 +02:00
e84edb2354 build: ver_0.350 - fix linkow produktow z permutacja w feedzie Google
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-30 02:38:49 +02:00
fba215b372 fix: linki produktow z permutacja atrybutow w feedzie Google (v0.350)
Separator URL miedzy parami attr-val zmieniony z "/" na "_" w generatorze
feedu (ProductRepository::appendCombinationToXml). Wzorzec routingu
pp_routes rozszerzony do [0-9_-]+ w Helpers::htacces (oba warianty:
seo_link i fallback p-id-name). LayoutEngine konwertuje "_" -> "|"
przed wywolaniem ProductRepository::findCached — format DB pozostaje "|".
Partial product-attribute.php preselectuje wartosc z permutation_hash
URL (forced_value_id), co poprawia UX wejscia z linka feedu.

Suita: 834 -> 841 testow (+7), 2330 assertions.

Wymagane akcje na produkcji po deployu: regeneracja pp_routes
(Helpers::htacces), wyczyszczenie klucza pp_routes:all w Redis,
regeneracja google-feed.xml, resubmit feedu w GMC.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-30 01:58:29 +02:00
Jacek
0de47f4e62 update 2026-04-20 23:45:19 +02:00
Jacek
abb794de36 build: ver_0.349 - koszt transportu na podsumowaniu koszyka 2026-04-20 20:51:24 +02:00
Jacek
eee22ef1c4 fix: poprawny koszt transportu na /koszyk-podsumowanie
Na podstronie /koszyk-podsumowanie transport z flaga delivery_free=1
byl pokazywany zawsze za 0,00 zl, niezaleznie od wartosci koszyka.
Teraz kontroler wylicza transport_cost_effective i free_delivery_applies
uwzgledniajac prog settings.free_delivery, a szablon uzywa tych kluczy.

- Nowa chroniona metoda ShopBasketController::calculateTransportCostForSummary
- Dodane 6 testow jednostkowych (ShopBasketControllerSummaryViewTest)
- Suita: 834 testy / 2318 assertions OK

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 20:50:15 +02:00
Jacek
494cb580d3 update 2026-04-19 11:22:36 +02:00
Jacek
23bd85a04a build: ver_0.348 - etykiety niestandardowe produktów 2026-04-19 11:12:28 +02:00
48 changed files with 2670 additions and 70 deletions

1
.gitignore vendored
View File

@@ -1 +1,2 @@
.vscode/ftp-kr.sync.cache.json
temp/

View File

@@ -14,7 +14,7 @@ Właściciel sklepu internetowego ma pełną kontrolę nad sprzedażą online -
|-----------|-------|
| Version | 0.333 |
| Status | Production |
| Last Updated | 2026-04-19 |
| Last Updated | 2026-04-30 |
## Requirements
@@ -31,6 +31,8 @@ Właściciel sklepu internetowego ma pełną kontrolę nad sprzedażą online -
- [x] Ochrona przed podwójnym składaniem zamówienia
- [x] Domain-Driven Architecture (migracja z legacy zakończona)
- [x] Szybka edycja custom_label_0..4 na liscie produktow admina (toggle sesyjny + autocomplete)
- [x] Poprawna kalkulacja kosztu transportu na /koszyk-podsumowanie (fix delivery_free bez uwzglednienia progu)
- [x] Linki produktów z permutacją w feedzie Google działają (separator `_` w URL, konwersja `_``|` w warstwie front, regex `[0-9_-]+` w pp_routes)
### Active (In Progress)
@@ -81,12 +83,14 @@ Właściciel sklepu internetowego ma pełną kontrolę nad sprzedażą online -
| Własny silnik zamiast frameworka | Pełna kontrola, brak narzutów | - | Active |
| `id` w tabbed FormEdit przez `hiddenFields` | Zapobiega insert zamiast update przy edycji encji | 2026-04-18 | Active |
| Inline custom labels w product list przez sesyjny toggle | Szybszy workflow dla Google XML bez wejscia w edycje produktu | 2026-04-19 | Active |
| Kalkulacja kosztu transportu na /koszyk-podsumowanie w kontrolerze (nie w szablonie) | Spojnosc logiki progu darmowej dostawy miedzy /koszyk i /koszyk-podsumowanie | 2026-04-20 | Active |
| Separator URL permutacji `_` zamiast `/` (DB pozostaje `|`) | Jeden segment URL dopasowywalny przez pp_routes; konwersja `_``|` w warstwie front | 2026-04-30 | Active |
## Success Metrics
| Metric | Target | Current | Status |
|--------|--------|---------|--------|
| Testy | >800 | 821 | On track |
| Testy | >800 | 841 | On track |
| Pokrycie architektury DDD | 100% | 100% | Achieved |
## Tech Stack
@@ -115,4 +119,4 @@ Quick Reference:
---
*PROJECT.md - Updated when requirements or context change*
*Last updated: 2026-04-19 after Phase 16*
*Last updated: 2026-04-30 after Phase 18*

View File

@@ -37,6 +37,10 @@ Status: Planning
| 8 | Apilo orders not sending — diagnoza i naprawa | 1 | Done | 2026-03-16 |
| 9 | Apilo email notification + infinite retry | 1 | Done | 2026-03-19 |
| 15 | Scontainers edit saves as new record | 1 | Done | 2026-04-18 |
| 17 | Cart summary transport cost fix | 1 | Done | 2026-04-20 |
| 18 | Google feed permutation URL fix | 1 | Done | 2026-04-30 |
| 19 | Frontend meta tags fix (category + product) | 1 | Done | 2026-05-13 |
| 20 | Category edit mojibake fix (UI labels) | 1 | Done | 2026-05-13 |
## Feature
@@ -118,5 +122,32 @@ Status: Planning
**Scope:** Dodac przycisk "Pokaz etykiety niestandardowe" obok "Dodaj produkt", zapisywac jego stan w sesji, pokazac 5 pol custom label pod nazwa produktu, zapisac wartosci do bazy i zapewnic podpowiedzi z juz istniejacych wartosci.
### Phase 17 - Cart summary transport cost fix
**Problem:** Na /koszyk-podsumowanie kazdy transport z flaga `delivery_free = 1` pokazywany jest za 0,00 zl, niezaleznie od tego czy koszyk osiagnal prog darmowej dostawy `settings.free_delivery`. Szablon summary-view.php sprawdza tylko flage, nie wartosc koszyka. Suma koncowa zamowienia jest zaniżona.
**Scope:** Przekazac z `ShopBasketController::summaryView()` do szablonu wyliczony `transport_cost_effective` i flage `free_delivery_applies` uwzgledniajaca prog. Zaktualizowac summary-view.php aby uzywal tych kluczy zamiast surowej flagi `delivery_free`. Test jednostkowy dla logiki wyliczenia.
### Phase 18 — Google feed permutation URL fix
**Problem:** URL produktu z permutacją atrybutów w feedzie Google miał format `/slug/20-170/21-175` (slash między parami). Wzorzec routingu `pp_routes` używa `[0-9-]+`, który nie obejmuje `/`, więc URL nie matchuje żadnej trasy i `index.php` ładuje stronę główną. Klienci z GMC trafiają na home zamiast na produkt z wybraną kombinacją.
**Scope:** Zmienić separator z `/` na `_` w generatorze feedu (`ProductRepository::appendCombinationToXml`), rozszerzyć regex routingu o `_` (`Helpers`), dodać konwersję `_``|` w warstwie front (`LayoutEngine`), preselekcja wartości atrybutu w partialu na podstawie `permutation_hash` z URL. Plus unit testy regex + generator linku.
### Phase 19 — Frontend meta tags fix
**Problem:** Strony kategorii (np. `/sen-i-otulenie`) i strony produktów (np. `/kocyk-niemowlaka-szczeniak-z-balonikiem-fuksja`) renderują `<title>` strony głównej oraz literalne wartości `content="keywords"`/`content="description"` zamiast właściwych metatagów SEO z bazy. Niepoprawne meta blokują indeksację Google i Merchant Center.
**Scope:** Diagnostyka (pp_routes + meta w DB + sesyjny $page), checkpoint:decision z 4 opcjami fixu (routes/engine/data/session), implementacja wybranej opcji w `LayoutEngine.php` lub `index.php`, test jednostkowy, human-verify na 3 URL-ach.
---
*Last updated: 2026-04-19 (Phase 16 complete)*
### Phase 20 — Category edit mojibake fix
**Problem:** Etykiety formularza edycji kategorii (`/admin/shop_category/category_edit/id=X`) pokazują double-encoded UTF-8 mojibake: `TreĹć` zamiast "Treść", `JÄ™zyk domyĹlny` zamiast "Język domyślny", `Sortowanie produktĂłw` zamiast "Sortowanie produktów" itp.
**Root cause:** Plik `admin/templates/shop-category/category-edit.php` został kiedyś zapisany z UTF-8 zinterpretowanym jako Windows-1252 i ponownie zakodowanym do UTF-8. 9 wystąpień. To JEDYNY taki plik w całym repo — grep `Ä[\x{84}-\x{99}]|Ĺ|Ăł` po całym `admin/templates/`, `templates/`, `autoload/` nie wskazuje innych dotknietych plików.
**Scope:** Podmienić 9 hardcoded labelek na poprawne polskie znaki (Treść, Język domyślny, Tytuł, rozwinięcie, Sortowanie produktów, Wyświetlić podkategorie, Blokuj indeksację). Brak zmian w logice/HTML structure, plik zostaje UTF-8 bez BOM. Human-verify w panelu admina.
---
*Last updated: 2026-05-13 (Phase 20 complete)*

View File

@@ -2,22 +2,22 @@
## Project Reference
See: .paul/PROJECT.md (updated 2026-04-18)
See: .paul/PROJECT.md (updated 2026-04-30)
**Core value:** Właściciel sklepu ma pełną kontrolę nad sprzedażą online w jednym systemie pisanym od podstaw, bez narzutów zewnętrznych platform.
**Current focus:** Phase 16 complete - loop closed
**Current focus:** Phase 20 complete loop closed
## Current Position
Milestone: Feature
Phase: 16 of 16 (Product list custom labels quick edit) - Complete
Plan: 16-01 complete
Status: UNIFY complete, ready for next PLAN loop
Last activity: 2026-04-19 - Closed loop for .paul/phases/16-product-list-custom-labels/16-01-PLAN.md
Milestone: Hotfix
Phase: 20 of 20 (Category edit mojibake fix) Complete
Plan: 20-01 complete
Status: UNIFY complete, ready for next PLAN loop (transition-phase git commit pending)
Last activity: 2026-05-13 — Closed loop for .paul/phases/20-category-edit-mojibake-fix/20-01-PLAN.md
Progress:
- Milestone: [##########] 100%
- Phase 16: [##########] 100%
- Milestone: [##########] 100% (Hotfix rolling)
- Phase 20: [##########] 100%
## Loop Position
@@ -42,10 +42,36 @@ Phase 13: PLAN --> APPLY --> UNIFY ✓ ✓ ✓ [COMPLETE - 2026-03-25]
Phase 14: PLAN --> APPLY --> UNIFY ✓ ✓ ✓ [COMPLETE - 2026-04-16]
Phase 15: PLAN --> APPLY --> UNIFY ✓ ✓ ✓ [COMPLETE - 2026-04-18]
Phase 16: PLAN --> APPLY --> UNIFY ✓ ✓ ✓ [COMPLETE - 2026-04-19]
Phase 17: PLAN --> APPLY --> UNIFY ✓ ✓ ✓ [COMPLETE - 2026-04-20]
Phase 18: PLAN --> APPLY --> UNIFY ✓ ✓ ✓ [COMPLETE - 2026-04-30]
Phase 19: PLAN --> APPLY --> UNIFY ✓ ✓ ✓ [COMPLETE - 2026-05-13]
Phase 20: PLAN --> APPLY --> UNIFY ✓ ✓ ✓ [COMPLETE - 2026-05-13]
```
## Accumulated Context
### Decisions
- 2026-05-13: Phase 20 loop closed with SUMMARY at .paul/phases/20-category-edit-mojibake-fix/20-01-SUMMARY.md
- 2026-05-13: Transition-phase git commit for Phase 20 not executed in this UNIFY run (deferred — pattern z faz 15-19)
- 2026-05-13: Phase 20 APPLY complete — 9 wystąpień mojibake w admin/templates/shop-category/category-edit.php naprawionych (Treść, Język domyślny x2, rozwinięcie, Sortowanie produktów, Wyświetlić podkategorie, Tytuł kategorii, Blokuj indeksację); suita 846 zielona; human-verify approved
- 2026-05-13: Created Phase 20 plan at .paul/phases/20-category-edit-mojibake-fix/20-01-PLAN.md — fix mojibake w labelkach formularza edycji kategorii (double-encoded UTF-8 w admin/templates/shop-category/category-edit.php, 9 wystąpień, jedyny dotknięty plik w repo)
- 2026-05-13: Phase 19 loop closed with SUMMARY at .paul/phases/19-frontend-meta-tags-fix/19-01-SUMMARY.md
- 2026-05-13: Transition-phase git commit for Phase 19 not executed in this UNIFY run (deferred — pattern z faz 15/16/17/18)
- 2026-05-13: Phase 19 APPLY complete — LayoutEngine.php zmodyfikowany (3 gałęzie + nowa metoda applyEntityMetaToPage), test LayoutEngineMetaTagsTest 5 testów/18 asercji, suita 846 zielona; weryfikacja na produkcji (curl) pokazuje poprawne tytuły dla /sen-i-otulenie, /kocyk-niemowlaka-... i /
- 2026-05-13: Phase 19 checkpoint:decision — wybrano `fix-engine-detection`. Root cause: LayoutEngine::show() w gałęziach category/article/product nadpisuje $page['language']['title'] ale NIE $page['language']['meta_title']. Wartość meta_title homepage ('Sklep z akcesoriami...') wycieka do linii 332. Dane DB klienta (literalne 'description'/'keywords' w kategorii) to oddzielny issue — admin uzupełnia.
- 2026-05-13: Created Phase 19 plan at .paul/phases/19-frontend-meta-tags-fix/19-01-PLAN.md — fix metatagów <title>/<meta description>/<meta keywords> dla kategorii i produktu (homepage tytuł wycieka na wszystkie podstrony)
- 2026-05-13: Phase 19 — najpierw diagnostyka (pp_routes + DB meta + session $page), checkpoint:decision, potem fix; ustalanie root cause przed implementacją (3 hipotezy: pp_routes destination bez category=/product=, sesyjny $page bleed, lub literalne zaślepki w DB)
- 2026-04-30: Phase 18 loop closed with SUMMARY at .paul/phases/18-google-feed-permutation-url-fix/18-01-SUMMARY.md
- 2026-04-30: Transition-phase git commit for Phase 18 not executed in this UNIFY run (deferred — pattern z faz 15/16/17)
- 2026-04-30: Phase 18 APPLY complete — 4 pliki silnika + 2 nowe pliki testów (HelpersRoutingTest 4 testy, ProductFeedLinkTest 3 testy); suita 841 zielona
- 2026-04-30: Created Phase 18 plan at .paul/phases/18-google-feed-permutation-url-fix/18-01-PLAN.md
- 2026-04-30: Phase 18 — separator URL permutacji `/``_`; konwersja `_``|` w warstwie front; regex `[0-9_-]+` w pp_routes
- 2026-04-30: Phase 18 — override /feature-dev (hotfix z konkretną instrukcją), brak redirectów 301, brak automatycznych akcji post-deploy
- 2026-04-20: Phase 17 loop closed with SUMMARY at .paul/phases/17-cart-summary-transport-cost-fix/17-01-SUMMARY.md
- 2026-04-20: Transition-phase git commit for Phase 17 not executed in this UNIFY run (deferred)
- 2026-04-20: Phase 17 APPLY complete - human-verify checkpoint approved, 834 testow zielonych (6 nowych)
- 2026-04-20: Created Phase 17 plan at .paul/phases/17-cart-summary-transport-cost-fix/17-01-PLAN.md
- 2026-04-20: Phase 17 bug root cause - summary-view.php blindly shows 0 zl gdy transport.delivery_free=1 bez sprawdzenia progu settings.free_delivery
- 2026-04-20: Phase 17 fix - nowa chroniona metoda ShopBasketController::calculateTransportCostForSummary zwraca transport_cost_effective + free_delivery_applies; szablon uzywa tych kluczy zamiast delivery_free
- 2026-04-19: Created Phase 16 plan at .paul/phases/16-product-list-custom-labels/16-01-PLAN.md
- 2026-04-19: Phase 16 scope includes session toggle + inline custom_label_0..4 edit + suggestions on product list
- 2026-04-19: Override approved by user - proceeded without required /feature-dev skill in Phase 16 APPLY
@@ -82,17 +108,17 @@ None.
### Blockers/Concerns
None.
### Skill Audit (Phase 16)
### Skill Audit (Phase 20)
| Expected | Invoked | Notes |
|----------|---------|-------|
| /feature-dev | ○ | User-approved override during APPLY |
| /koniec-pracy | ○ | Marked as available by user; execute on session close workflow |
| /feature-dev | ○ | Trivialny fix labelek — override per pattern poprzednich faz |
| /koniec-pracy | ○ | Pending — uruchomić przy release update package |
## Session Continuity
Last session: 2026-04-19
Stopped at: Phase 16 complete, loop closed
Next action: Start next milestone or create next phase plan
Resume file: .paul/phases/16-product-list-custom-labels/16-01-SUMMARY.md
Last session: 2026-05-13
Stopped at: Phase 20 complete, loop closed
Next action: Start next phase plan (transition-phase git commit pending), lub uruchomić /koniec-pracy jeśli zamykamy sesję
Resume file: .paul/phases/20-category-edit-mojibake-fix/20-01-SUMMARY.md
---
*STATE.md — Updated after every significant action*

View File

@@ -0,0 +1,19 @@
# 2026-04-20
## Co zrobiono
- [Phase 17, Plan 01] Naprawa kosztu transportu na /koszyk-podsumowanie — transport z flaga delivery_free=1 pokazuje teraz rzeczywisty koszt ponizej progu settings.free_delivery, a 0,00 zl dopiero po osiagnieciu progu
- Dodana chroniona metoda `ShopBasketController::calculateTransportCostForSummary` wyliczajaca `transport_cost_effective` i `free_delivery_applies`
- Szablon `templates/shop-basket/summary-view.php` uzywa tych kluczy zamiast surowej flagi `delivery_free`
- Nowy plik testow `ShopBasketControllerSummaryViewTest` (6 testow, 12 assertions)
- Pelna suita PHPUnit: 834/834 OK (2318 assertions)
## Zmienione pliki
- `autoload/front/Controllers/ShopBasketController.php`
- `templates/shop-basket/summary-view.php`
- `tests/Unit/front/Controllers/ShopBasketControllerSummaryViewTest.php`
- `.paul/phases/17-cart-summary-transport-cost-fix/17-01-PLAN.md`
- `.paul/phases/17-cart-summary-transport-cost-fix/17-01-SUMMARY.md`
- `.paul/STATE.md`
- `.paul/ROADMAP.md`

View File

@@ -0,0 +1,25 @@
# 2026-04-30
## Co zrobiono
- [Phase 18, Plan 01] Fix linków produktów z permutacją atrybutów w feedzie Google
- Separator URL między parami `attr-val` zmieniony z `/` na `_` w `ProductRepository::appendCombinationToXml`
- Wzorzec routingu `pp_routes` rozszerzony o `_` (`[0-9-]+``[0-9_-]+`) w `Helpers::htacces`
- Konwersja `_``|` w `LayoutEngine` przed wywołaniem `ProductRepository::findCached`
- Preselekcja wartości atrybutu na podstawie `permutation_hash` z URL w partialu `product-attribute.php`
- 2 nowe pliki testów: `HelpersRoutingTest` (4 testy) + `ProductFeedLinkTest` (3 testy via Reflection)
- Suita PHPUnit: 834 → 841 zielonych
## Zmienione pliki
- `autoload/Domain/Product/ProductRepository.php`
- `autoload/Shared/Helpers/Helpers.php`
- `autoload/front/LayoutEngine.php`
- `templates/shop-product/_partial/product-attribute.php`
- `tests/Unit/Shared/Helpers/HelpersRoutingTest.php`
- `tests/Unit/Domain/Product/ProductFeedLinkTest.php`
- `.paul/STATE.md`
- `.paul/PROJECT.md`
- `.paul/ROADMAP.md`
- `.paul/phases/18-google-feed-permutation-url-fix/18-01-PLAN.md`
- `.paul/phases/18-google-feed-permutation-url-fix/18-01-SUMMARY.md`

View File

@@ -0,0 +1,27 @@
# 2026-05-13
## Co zrobiono
- [Phase 19, Plan 01] Fix metatagów na stronach kategorii/artykułu/produktu — eliminacja wycieku meta_title homepage
- Task 1: Diagnostyka produkcyjnej DB (pp_routes + pp_shop_categories_langs + pp_shop_products_langs + pp_pages_langs) — wynik w DIAGNOSTICS.md
- Task 2 (checkpoint:decision): Wybrano fix-engine-detection (root cause w kodzie, nie w danych)
- Task 3: Wyodrębniono `\front\LayoutEngine::applyEntityMetaToPage()` jako pure-function; 3 gałęzie (category/article/product) wywołują helper; suita 846 testów zielona (5 nowych w LayoutEngineMetaTagsTest)
- Task 4 (human-verify): Weryfikacja curl na produkcji — 3 strony pokazują 3 różne `<title>`, homepage meta nie wycieka
- .gitignore — dodano `temp/` (skrypty diagnostyczne z DB credentials)
- [Phase 20, Plan 01] Fix mojibake (double-encoded UTF-8) w labelkach formularza edycji kategorii
- Task 1: Podmiana 9 wystąpień mojibake na poprawne polskie znaki (Treść, Język domyślny, Tytuł kategorii, Sortowanie produktów, Wyświetlić podkategorie, Blokuj indeksację, Opis kategorii rozwinięcie)
- Task 2: Weryfikacja braku regresji (grep w całym repo — 0 innych plików dotkniętych), suita 846 zielona
- Task 3 (human-verify): Weryfikacja w panelu admina shoppro.project-dc.pl approved
## Zmienione pliki
- `autoload/front/LayoutEngine.php`
- `admin/templates/shop-category/category-edit.php`
- `tests/Unit/front/LayoutEngineMetaTagsTest.php` (nowy)
- `.paul/phases/19-frontend-meta-tags-fix/19-01-PLAN.md` (nowy)
- `.paul/phases/19-frontend-meta-tags-fix/19-01-SUMMARY.md` (nowy)
- `.paul/phases/19-frontend-meta-tags-fix/DIAGNOSTICS.md` (nowy)
- `.paul/STATE.md`
- `.paul/ROADMAP.md`
- `.gitignore`

View File

@@ -4,8 +4,8 @@
| Metric | Value |
|--------|-------|
| Total tests | **828** |
| Total assertions | **2306** |
| Total tests | **846** |
| Total assertions | **2348** |
| Framework | PHPUnit 9.6 (`phpunit.phar`) |
| Bootstrap | `tests/bootstrap.php` |
| Config | `phpunit.xml` |

View File

@@ -2,6 +2,39 @@
> Chronologiczny log zmian technicznych — co i dlaczego.
## v0.352 (2026-05-13)
- Naprawiono mojibake w labelkach formularza edycji kategorii w panelu admina (`/admin/shop_category/category_edit/id=X`). Plik `admin/templates/shop-category/category-edit.php` byl kiedys zapisany z UTF-8 zinterpretowanym jako Windows-1252 i ponownie zakodowanym do UTF-8 (classic double-encoding). 9 wystapien naprawione: `TreĹć`→Treść, `JÄ™zyk domyĹlny`→Język domyślny (x2), `rozwiniÄ™cie`→rozwinięcie, `Sortowanie produktĂłw`→Sortowanie produktów, `WyĹwietlić podkategorie`→Wyświetlić podkategorie, `TytuĹ kategorii (h1)`→Tytuł kategorii (h1), `Blokuj indeksacjÄ™`→Blokuj indeksację.
- Wykluczono regresje: grep `Ä[\\x{84}-\\x{99}]|Ĺ|Ăł|Ĺ›|Ĺ‚|Ä™|Ä…|ć|Ĺ„` w `admin/templates/`, `templates/`, `autoload/` zwraca 0 plikow — to byl jedyny przypadek mojibake w repo.
- Suita PHPUnit bez zmian: 846 testow / 2348 assertions.
## v0.351 (2026-05-13)
- Naprawiono wyciek metatagow ze strony glownej na podstrony kategorii/artykulu/produktu: `<title>` wszystkich podstron pokazywal tytul homepage ("Sklep z akcesoriami..."), bo `LayoutEngine::show()` nadpisywal w galezi kategorii/artykulu/produktu tylko `$page['language']['title']`, a `meta_title` z domyslnej strony zylo dalej i wygrywalo w linii substytucji `[TITLE]`.
- Wyodrebniono nowa metode publiczna `\front\LayoutEngine::applyEntityMetaToPage($page, $entityLanguage, $fallbackTitle)`: zawsze nadpisuje `meta_title`, `meta_keywords`, `meta_description` w `$page['language']` wartosciami encji (nawet pustym/null), eliminujac wyciek.
- `LayoutEngine.php`: 3 galezie (category, article, product) wywoluja helper zamiast inline'ow z nadpisywaniem czesci pol.
- Dodano 5 testow jednostkowych (`tests/Unit/front/LayoutEngineMetaTagsTest.php`) na pure-function helper: meta_title encji wygrywa, NULL czysci homepage, all-null product, null entity safe, empty page struct. Suita: 846 testow / 2348 assertions.
- Diagnostyka root cause na produkcyjnej DB: pp_routes mapuje poprawnie (`category=10`, `product=522`); literalne 'description'/'keywords' w `pp_shop_categories_langs.id=331` to dane klienta (admin uzupelnia w panelu), nie bug shopPRO.
- `.gitignore` rozszerzony o `temp/` (skrypty diagnostyczne z DB credentials).
- Wymagane akcje na produkcji po deployu: poczekac na TTL cache Redis (24h) lub wyczyscic klucze `pp_routes:all`, `front_category_details:*`, `shop\\product:*` — opcjonalne (fix jest w warstwie poza cache).
## v0.350 (2026-04-30)
- Naprawiono linki produktow z permutacja atrybutow w feedzie Google: separator par `attr-val` w URL zmieniony z `/` na `_`. Stary format `/slug/20-170/21-175` nie matchowal sie w `pp_routes` (regex `[0-9-]+` nie obejmuje `/`), wiec klienci z GMC ladowali na strone glowna zamiast na produkt.
- `ProductRepository::appendCombinationToXml`: `str_replace('|', '/', ...)` -> `str_replace('|', '_', ...)` w obu galeziach (z `seo_link` i fallback `p-id-name`).
- `Helpers::htacces`: regex routingu produktow z permutacja rozszerzony do `/([0-9_-]+)$` w obu wariantach.
- `LayoutEngine.php` (// PRODUKT): konwersja `_` -> `|` przed wywolaniem `ProductRepository::findCached` — format DB pozostaje bez zmian (`attr-val|attr-val`).
- `templates/shop-product/_partial/product-attribute.php`: preselekcja wartosci atrybutu na podstawie `permutation_hash` z URL (`$forced_value_id`); wartosc `is_default` uzywana tylko gdy URL nie wymusza wyboru. Dotyczy `checked` na inpucie i emisji bloku `fradio_label_click(...)`.
- Dodano 7 testow jednostkowych: `HelpersRoutingTest` (4 testy regex + assercje na zawartosci pliku) i `ProductFeedLinkTest` (3 testy `appendCombinationToXml` via `ReflectionMethod` z mockiem `TransportRepository`). Suita: 841 testow / 2330 assertions.
- Wymagane akcje na produkcji po deployu: regeneracja `pp_routes` (`Helpers::htacces()`), wyczyszczenie klucza `pp_routes:all` w Redis, regeneracja `google-feed.xml`, resubmit feedu w GMC.
## v0.349 (2026-04-20)
- Naprawiono wyswietlanie kosztu transportu na /koszyk-podsumowanie: transporty z `delivery_free=1` pokazuja teraz rzeczywisty koszt ponizej progu `settings.free_delivery`, a 0,00 zl dopiero po osiagnieciu progu (spojnie z lista na /koszyk).
- Dodano chroniona metode `ShopBasketController::calculateTransportCostForSummary()` wyliczajaca `transport_cost_effective` + `free_delivery_applies` — logika widokowa przeniesiona z szablonu do kontrolera.
- Szablon `templates/shop-basket/summary-view.php` uzywa nowych kluczy zamiast sprawdzania surowej flagi `delivery_free`.
- Dodano 6 testow jednostkowych (`ShopBasketControllerSummaryViewTest`) pokrywajacych AC + edge cases (prog rowny, prog 0, transport null). Suita: 834 testy / 2318 assertions.
## v0.348 (2026-04-19)
- Dodano przełącznik widoczności etykiet niestandardowych na liście produktów w panelu admina, z zapisem stanu w sesji.

View File

@@ -3686,3 +3686,8 @@ Dodać możliwość ustawienia limitu znaków w wiadomościach do produktu
- [ ] [MINOR] autoload/admin/Controllers/ShopProductController.php:971 - Rename function "generate_sku_code" to match the regular expression ^[a-z][a-zA-Z0-9]*$. (php:S100)
- [ ] [MINOR] autoload/admin/Controllers/ShopProductController.php:989 - Rename function "product_combination" to match the regular expression ^[a-z][a-zA-Z0-9]*$. (php:S100)
## SonarQube - v0.349 - brak nowych issues
## SonarQube - v0.350 - brak nowych issues

View File

@@ -0,0 +1,46 @@
{"ts":"2026-04-20T18:30:03Z","tool":"Bash","cmd":"mkdir -p \"C:/visual studio code/projekty/shopPRO/.paul/phases/17-cart-summary-transport-cost-fix\"\",\"description\":\"Create phase 17 directory\"},\"tool_response\":{\"stdout\":\"\",\"stderr\":\"\",","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-20T18:31:17Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\shopPRO\\\\.paul\\\\phases\\\\17-cart-summary-transport-cost-fix\\\\17-01-PLAN.md","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-20T18:31:29Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\shopPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-20T18:31:34Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\shopPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-20T18:31:40Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\shopPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-20T18:31:45Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\shopPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-20T18:31:52Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\shopPRO\\\\.paul\\\\ROADMAP.md","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-20T18:32:07Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\shopPRO\\\\.paul\\\\ROADMAP.md","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-20T18:33:13Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\shopPRO\\\\autoload\\\\front\\\\Controllers\\\\ShopBasketController.php","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-20T18:33:24Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\shopPRO\\\\templates\\\\shop-basket\\\\summary-view.php","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-20T18:34:04Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\shopPRO\\\\tests\\\\Unit\\\\front\\\\Controllers\\\\ShopBasketControllerSummaryViewTest.php","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-20T18:34:09Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/shopPRO\" && powershell -ExecutionPolicy Bypass -File ./test.ps1 tests/Unit/front/Controllers/ShopBasketControllerSummaryViewTest.php 2>&1 | tail -30\",\"timeout\":","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-20T18:34:15Z","tool":"Bash","cmd":"powershell -ExecutionPolicy Bypass -File \"C:/visual studio code/projekty/shopPRO/test.ps1\" tests/Unit/front/Controllers/ShopBasketControllerSummaryViewTest.php 2>&1 | tail -40\",\"timeout\":120000,\","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-20T18:34:32Z","tool":"Bash","cmd":"where php 2>&1 | head -3\",\"description\":\"Locate PHP binary\"},\"tool_response\":{\"stdout\":\"C:\\\\xampp\\\\php\\\\php.exe\",\"stderr\":\"\",\"interrupted\":false,\"isImage\":false,\"noOutputExpe","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-20T18:34:39Z","tool":"Bash","cmd":"\"C:/xampp/php/php.exe\" \"C:/visual studio code/projekty/shopPRO/phpunit.phar\" -c \"C:/visual studio code/projekty/shopPRO/phpunit.xml\" \"C:/visual studio code/projekty/shopPRO/tests/Unit/front/Con","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-20T18:34:45Z","tool":"Bash","cmd":"\"C:/xampp/php/php.exe\" \"C:/visual studio code/projekty/shopPRO/phpunit.phar\" -c \"C:/visual studio code/projekty/shopPRO/phpunit.xml\" 2>&1 | tail -15\",\"timeout\":300000,\"description\":\"Run fu","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-20T18:36:07Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\shopPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-20T18:36:12Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\shopPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-20T18:36:19Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\shopPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-20T18:36:27Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\shopPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-20T18:42:55Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\shopPRO\\\\.paul\\\\phases\\\\17-cart-summary-transport-cost-fix\\\\17-01-SUMMARY.md","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-20T18:43:09Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\shopPRO\\\\.paul\\\\changelog\\\\2026-04-20.md","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-20T18:43:21Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\shopPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-20T18:43:25Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\shopPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-20T18:43:31Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\shopPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-20T18:43:36Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\shopPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-20T18:43:40Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\shopPRO\\\\.paul\\\\ROADMAP.md","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-20T18:43:45Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\shopPRO\\\\.paul\\\\ROADMAP.md","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-20T18:43:51Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\shopPRO\\\\.paul\\\\PROJECT.md","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-20T18:43:55Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\shopPRO\\\\.paul\\\\PROJECT.md","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-20T18:44:02Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\shopPRO\\\\.paul\\\\PROJECT.md","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-20T18:44:05Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\shopPRO\\\\.paul\\\\PROJECT.md","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-20T18:44:12Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\shopPRO\\\\.paul\\\\PROJECT.md","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-20T18:46:36Z","tool":"Bash","cmd":"\"C:/xampp/php/php.exe\" \"C:/visual studio code/projekty/shopPRO/phpunit.phar\" -c \"C:/visual studio code/projekty/shopPRO/phpunit.xml\" 2>&1 | tail -5\",\"timeout\":300000,\"description\":\"Run ful","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-20T18:48:13Z","tool":"Bash","cmd":"cd \"/c/visual studio code/projekty/shopPRO\" && sonar-scanner 2>&1 | tail -20\",\"timeout\":600000,\"description\":\"Run SonarQube scan\"},\"tool_response\":{\"stdout\":\"[INFO] ScannerEngine: Depen","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-20T18:48:52Z","tool":"Bash","cmd":"git -C \"/c/visual studio code/projekty/shopPRO\" tag --sort=-v:refname 2>&1 | head -5\",\"description\":\"Get latest git tag\"},\"tool_response\":{\"stdout\":\"v0.348\\nv0.347\\nv0.346\\nv0.345\\nv0.","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-20T18:49:14Z","tool":"Bash","cmd":"printf '\\\\n## SonarQube - v0.349 - brak nowych issues\\\\n' >> \"/c/visual studio code/projekty/shopPRO/.paul/docs/TODO.md\"\",\"description\":\"Append Sonar status to TODO.md\"},\"tool_response\":{","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-20T18:49:49Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\shopPRO\\\\.paul\\\\docs\\\\TECH_CHANGELOG.md","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-20T18:49:58Z","tool":"Bash","cmd":"git -C \"/c/visual studio code/projekty/shopPRO\" status --short 2>&1\",\"description\":\"Show git status\"},\"tool_response\":{\"stdout\":\" M .paul/PROJECT.md\\n M .paul/ROADMAP.md\\n M .paul/STATE.","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-20T18:50:04Z","tool":"Bash","cmd":"cd \"/c/visual studio code/projekty/shopPRO\" && git add autoload/front/Controllers/ShopBasketController.php templates/shop-basket/summary-view.php tests/Unit/front/Controllers/ShopBasketControllerSum","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-20T18:50:16Z","tool":"Bash","cmd":"git -C \"/c/visual studio code/projekty/shopPRO\" commit -m \"$(cat <<'EOF'\\nfix: poprawny koszt transportu na /koszyk-podsumowanie\\n\\nNa podstronie /koszyk-podsumowanie transport z flaga delivery_","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-20T18:50:21Z","tool":"Bash","cmd":"git -C \"/c/visual studio code/projekty/shopPRO\" push 2>&1 | tail -5\",\"description\":\"Push to origin\"},\"tool_response\":{\"stdout\":\"remote: Failed to authenticate user\\nfatal: Authentication ","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-20T18:51:10Z","tool":"Bash","cmd":"git -C \"/c/visual studio code/projekty/shopPRO\" push 2>&1 | tail -5\",\"description\":\"Retry push to origin\"},\"tool_response\":{\"stdout\":\"remote: . Processing 1 references \\nremote: Pr","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-20T18:51:19Z","tool":"Bash","cmd":"git -C \"/c/visual studio code/projekty/shopPRO\" tag v0.349 && powershell.exe -ExecutionPolicy Bypass -File \"C:\\\\visual studio code\\\\projekty\\\\shopPRO\\\\build-update.ps1\" -FromTag v0.348 -To","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-20T18:51:24Z","tool":"Bash","cmd":"git -C \"/c/visual studio code/projekty/shopPRO\" add updates/0.30/ver_0.349.zip updates/0.30/ver_0.349_manifest.json updates/versions.php updates/changelog-data.html && git -C \"/c/visual studio code","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-20T18:51:34Z","tool":"Bash","cmd":"git -C \"/c/visual studio code/projekty/shopPRO\" push 2>&1 | tail -5; echo \"---tag---\"; git -C \"/c/visual studio code/projekty/shopPRO\" push origin v0.349 2>&1 | tail -5\",\"description\":\"Push ","cwd":"/c/visual studio code/projekty/shopPRO"}

View File

@@ -0,0 +1,92 @@
{"ts":"2026-04-29T23:42:49Z","tool":"Bash","cmd":"mkdir -p \"C:/visual studio code/projekty/shopPRO/.paul/phases/18-google-feed-permutation-url-fix\"\",\"description\":\"Create phase 18 directory\"},\"tool_response\":{\"stdout\":\"\",\"stderr\":\"\",","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-29T23:44:08Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\shopPRO\\\\.paul\\\\phases\\\\18-google-feed-permutation-url-fix\\\\18-01-PLAN.md","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-29T23:44:17Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\shopPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-29T23:44:21Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\shopPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-29T23:44:27Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\shopPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-29T23:44:32Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\shopPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-29T23:44:37Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\shopPRO\\\\.paul\\\\ROADMAP.md","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-29T23:44:47Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\shopPRO\\\\.paul\\\\ROADMAP.md","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-29T23:45:26Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\shopPRO\\\\autoload\\\\Domain\\\\Product\\\\ProductRepository.php","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-29T23:45:33Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\shopPRO\\\\autoload\\\\Shared\\\\Helpers\\\\Helpers.php","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-29T23:45:38Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\shopPRO\\\\autoload\\\\Shared\\\\Helpers\\\\Helpers.php","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-29T23:45:46Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\shopPRO\\\\autoload\\\\front\\\\LayoutEngine.php","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-29T23:46:09Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\shopPRO\\\\templates\\\\shop-product\\\\_partial\\\\product-attribute.php","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-29T23:47:37Z","tool":"Bash","cmd":"mkdir -p \"C:/visual studio code/projekty/shopPRO/tests/Unit/Shared/Helpers\"\",\"description\":\"Create Helpers test dir\"},\"tool_response\":{\"stdout\":\"\",\"stderr\":\"\",\"interrupted\":false,\"","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-29T23:47:57Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\shopPRO\\\\tests\\\\Unit\\\\Shared\\\\Helpers\\\\HelpersRoutingTest.php","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-29T23:48:21Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\shopPRO\\\\tests\\\\Unit\\\\Domain\\\\Product\\\\ProductFeedLinkTest.php","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-29T23:48:26Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/shopPRO\" && powershell -ExecutionPolicy Bypass -File ./test.ps1 tests/Unit/Shared/Helpers/HelpersRoutingTest.php 2>&1 | tail -40\",\"timeout\":120000,\"description","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-29T23:48:33Z","tool":"Bash","cmd":"where php 2>&1 | head -3\",\"description\":\"Locate php\"},\"tool_response\":{\"stdout\":\"C:\\\\xampp\\\\php\\\\php.exe\",\"stderr\":\"\",\"interrupted\":false,\"isImage\":false,\"noOutputExpected\":","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-29T23:48:38Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/shopPRO\" && /c/xampp/php/php.exe phpunit.phar tests/Unit/Shared/Helpers/HelpersRoutingTest.php 2>&1 | tail -30\",\"timeout\":120000,\"description\":\"Run HelpersRo","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-29T23:48:42Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/shopPRO\" && /c/xampp/php/php.exe phpunit.phar tests/Unit/Domain/Product/ProductFeedLinkTest.php 2>&1 | tail -50\",\"timeout\":120000,\"description\":\"Run ProductF","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-29T23:48:50Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/shopPRO\" && /c/xampp/php/php.exe phpunit.phar 2>&1 | tail -30\",\"timeout\":300000,\"description\":\"Full test suite\"},\"tool_response\":{\"stdout\":\"PHPUnit 9.6","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-29T23:49:03Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\shopPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-29T23:49:07Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\shopPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-29T23:49:12Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\shopPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-29T23:49:17Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\shopPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-29T23:50:50Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\shopPRO\\\\.paul\\\\phases\\\\18-google-feed-permutation-url-fix\\\\18-01-SUMMARY.md","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-29T23:50:58Z","tool":"Bash","cmd":"mkdir -p \"C:/visual studio code/projekty/shopPRO/.paul/changelog\"\",\"description\":\"Ensure changelog dir\"},\"tool_response\":{\"stdout\":\"\",\"stderr\":\"\",\"interrupted\":false,\"isImage\":fal","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-29T23:51:09Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\shopPRO\\\\.paul\\\\changelog\\\\2026-04-30.md","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-29T23:51:14Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\shopPRO\\\\.paul\\\\PROJECT.md","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-29T23:51:21Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\shopPRO\\\\.paul\\\\PROJECT.md","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-29T23:51:27Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\shopPRO\\\\.paul\\\\PROJECT.md","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-29T23:51:32Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\shopPRO\\\\.paul\\\\PROJECT.md","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-29T23:51:36Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\shopPRO\\\\.paul\\\\PROJECT.md","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-29T23:51:40Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\shopPRO\\\\.paul\\\\ROADMAP.md","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-29T23:51:43Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\shopPRO\\\\.paul\\\\ROADMAP.md","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-29T23:51:49Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\shopPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-29T23:51:59Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\shopPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-29T23:52:03Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\shopPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-29T23:52:10Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\shopPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-29T23:52:17Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\shopPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-29T23:52:23Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\shopPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-29T23:52:50Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/shopPRO\" && /c/xampp/php/php.exe phpunit.phar 2>&1 | tail -10\",\"timeout\":300000,\"description\":\"Run PHPUnit suite\"},\"tool_response\":{\"stdout\":\".........","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-29T23:52:55Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/shopPRO\" && (which sonar-scanner; ls sonar-project.properties 2>/dev/null) 2>&1\",\"description\":\"Check sonar-scanner availability\"},\"tool_response\":{\"stdout","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-29T23:54:27Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/shopPRO\" && sonar-scanner 2>&1 | tail -20\",\"timeout\":600000,\"description\":\"Run sonar-scanner\"},\"tool_response\":{\"stdout\":\"[WARN] ScannerEngine: * te","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-29T23:55:04Z","tool":"Bash","cmd":"jq '.paging' \"C:/Users/jacek/.claude/projects/C--visual-studio-code-projekty-shopPRO/b636be85-51d0-45b7-8315-a27585e4af5c/tool-results/mcp-sonarqube-issues-1777506900147.txt\"\",\"description\":\"Che","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-29T23:55:09Z","tool":"Bash","cmd":"jq '[.issues[] | {sev: .severity, file: (.component | sub(\"^shopPRO:\"; \"\")), line: .line, msg: .message, rule: .rule, type: .type}] | group_by(.sev) | map({severity: .[0].sev, count: length})' \"C","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-29T23:55:39Z","tool":"Bash","cmd":"jq -r '.issues[] | \"[\\\\(.severity)] \\\\((.component | sub(\"^shopPRO:\"; \"\")))\\\\t:\\\\(.line // 0)\\\\t\\\\(.message)\\\\t(\\\\(.rule))\"' \"C:/Users/jacek/.claude/projects/C--visual-studio-co","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-29T23:55:52Z","tool":"Bash","cmd":"while IFS=$'\\\\t' read -r sev_file line msg rule; do\\n # Build canonical TODO line: \"[SEV] file:LINE - msg (rule)\"\\n file=\"${sev_file#*] }\"\\n sev_prefix=\"${sev_file%% *}\"\\n ln=\"${line#","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-29T23:56:11Z","tool":"Bash","cmd":"file \"C:/visual studio code/projekty/shopPRO/.paul/docs/TODO.md\" && grep -a \"LayoutEngine.php:198\\\\|LayoutEngine.php:200\\\\|LayoutEngine.php:208\" \"C:/visual studio code/projekty/shopPRO/.paul/","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-29T23:56:17Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/shopPRO\" && git tag --sort=-v:refname | head -3\",\"description\":\"Get latest tag\"},\"tool_response\":{\"stdout\":\"\",\"stderr\":\"\",\"interrupted\":false,\"is","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-29T23:56:21Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/shopPRO\" && git tag -l | sort -V | tail -5\",\"description\":\"List tags sorted\"},\"tool_response\":{\"stdout\":\"\",\"stderr\":\"\",\"interrupted\":false,\"isIma","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-29T23:56:25Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/shopPRO\" && git tag | head -5; echo \"---\"; git log --oneline | head -5\",\"description\":\"Check tags and log\"},\"tool_response\":{\"stdout\":\"---\\na57d76a up","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-29T23:56:30Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/shopPRO\" && git remote -v && echo \"---\" && git ls-remote --tags origin 2>&1 | tail -10\",\"description\":\"Check remote and tags\"},\"tool_response\":{\"stdout\"","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-29T23:57:09Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\shopPRO\\\\.paul\\\\docs\\\\TECH_CHANGELOG.md","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-29T23:57:14Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\shopPRO\\\\.paul\\\\codebase\\\\testing.md","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-29T23:57:20Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/shopPRO\" && printf '\\\\n\\\\n## SonarQube - v0.350 - brak nowych issues\\\\n' >> .paul/docs/TODO.md\",\"description\":\"Append Sonar entry to TODO\"},\"tool_respo","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-29T23:57:24Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/shopPRO\" && git status --short 2>&1 | head -40\",\"description\":\"Git status\"},\"tool_response\":{\"stdout\":\" M .htaccess\\n M .paul/PROJECT.md\\n M .paul/ROAD","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-29T23:57:42Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/shopPRO\" && git diff --stat 2>&1 | tail -5; echo \"---\"; git diff \"autoload/Domain/Attribute/AttributeRepository.php\" | head -20\",\"description\":\"Check diff ","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-29T23:57:46Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/shopPRO\" && git diff \"autoload/Domain/Attribute/AttributeRepository.php\" | head -10; echo \"===\"; git diff \"templates/articles/news.php\" | head -10\",\"descri","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-29T23:57:53Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/shopPRO\" && git diff --numstat 2>&1 | grep -v \"^0\\\\s\\\\+0\\\\s\" | head -30\",\"description\":\"Files with real content changes\"},\"tool_response\":{\"stdout\","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-29T23:57:57Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/shopPRO\" && git diff --numstat 2>/dev/null | awk '$1+$2>0 || $1==\"-\"' | head -30\",\"description\":\"Files with real changes\"},\"tool_response\":{\"stdout\":\"5","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-29T23:58:08Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/shopPRO\" && git status --short 2>/dev/null | grep \"^??\" | head -30\",\"description\":\"Untracked files\"},\"tool_response\":{\"stdout\":\"?? .paul/changelog/2026","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-29T23:58:16Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/shopPRO\" && git add \\\\\\n autoload/Domain/Product/ProductRepository.php \\\\\\n autoload/Shared/Helpers/Helpers.php \\\\\\n autoload/front/LayoutEngine.php \\","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-29T23:58:29Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/shopPRO\" && git commit -m \"$(cat <<'EOF'\\nfix: linki produktow z permutacja atrybutow w feedzie Google (v0.350)\\n\\nSeparator URL miedzy parami attr-val zmienio","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-29T23:58:36Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/shopPRO\" && git push 2>&1 | tail -10\",\"timeout\":120000,\"description\":\"Push to remote\"},\"tool_response\":{\"stdout\":\"remote: . Processing 1 references ","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-29T23:58:42Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/shopPRO\" && ls build-update.ps1 2>&1; git tag v0.350 2>&1\",\"description\":\"Verify build script exists and create tag\"},\"tool_response\":{\"stdout\":\"build-up","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-29T23:58:48Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/shopPRO\" && git fetch --tags 2>&1 | tail -5; git tag | sort -V | tail -3\",\"timeout\":60000,\"description\":\"Fetch tags from remote\"},\"tool_response\":{\"stdou","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-29T23:58:55Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/shopPRO\" && git fetch origin tag v0.349 2>&1 | tail -5\",\"timeout\":60000,\"description\":\"Fetch v0.349 tag specifically\"},\"tool_response\":{\"stdout\":\"remot","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-29T23:59:03Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/shopPRO\" && git cat-file -t 9afcc70f385892b2d27a7603dbc10935460bca9b 2>&1; echo \"---\"; git log --oneline a57d76a -5 2>&1 | head\",\"description\":\"Check if v0.3","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-29T23:59:10Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/shopPRO\" && grep -n \"FromTag\\\\|ToTag\\\\|param\\\\|git diff\" build-update.ps1 | head -30\",\"description\":\"Inspect build-update.ps1 params\"},\"tool_response","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-29T23:59:21Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/shopPRO\" && ls -la updates/0.30/ 2>&1 | tail -10\",\"description\":\"Check existing update packages\"},\"tool_response\":{\"stdout\":\"-rw-r--r-- 1 jacek 197121 21","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-29T23:59:30Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/shopPRO\" && git fetch origin refs/tags/v0.349:refs/tags/v0.349 2>&1 | tail -5\",\"timeout\":60000,\"description\":\"Force-fetch single tag\"},\"tool_response\":{\"","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-30T00:36:15Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/shopPRO\" && git fetch --tags 2>&1 | tail -5; git tag | sort -V | tail -5\",\"timeout\":60000,\"description\":\"Verify remote fixed, fetch tags\"},\"tool_response\"","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-30T00:36:23Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/shopPRO\" && powershell.exe -ExecutionPolicy Bypass -File build-update.ps1 -FromTag v0.349 -ToTag v0.350 -ChangelogEntry \"FIX - linki produktow z permutacja w feed","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-30T00:36:28Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/shopPRO\" && git add \"updates/0.30/ver_0.350.zip\" \"updates/0.30/ver_0.350_manifest.json\" \"updates/versions.php\" \"updates/changelog-data.html\" && git status ","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-30T00:36:33Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/shopPRO\" && git commit -m \"$(cat <<'EOF'\\nbuild: ver_0.350 - fix linkow produktow z permutacja w feedzie Google\\n\\nCo-Authored-By: Claude Opus 4.6 <noreply@ant","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-30T00:36:41Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/shopPRO\" && git push 2>&1 | tail -5 && git push origin v0.350 2>&1 | tail -5\",\"timeout\":120000,\"description\":\"Push commit and tag\"},\"tool_response\":{\"std","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-30T00:37:03Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/shopPRO\" && sleep 15 && git push 2>&1 | tail -5\",\"timeout\":120000,\"description\":\"Retry push after Gitea restart\"},\"tool_response\":{\"stdout\":\"remote: Fa","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-30T00:37:09Z","tool":"Bash","cmd":"curl -s -o /dev/null -w \"%{http_code}\\\\n\" https://git.project-pro.pl/ 2>&1; echo \"---\"; cd \"C:/visual studio code/projekty/shopPRO\" && git config --get credential.helper 2>&1\",\"description\"","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-30T00:37:27Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/shopPRO\" && sleep 10 && git push 2>&1 | tail -3\",\"timeout\":120000,\"description\":\"Retry push once more\"},\"tool_response\":{\"stdout\":\"hint: its remote cou","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-30T00:37:32Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/shopPRO\" && git push 2>&1 | tail -10\",\"timeout\":120000,\"description\":\"Full push output\"},\"tool_response\":{\"stdout\":\"To https://git.project-pro.pl/Proje","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-30T00:37:41Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/shopPRO\" && git fetch origin main 2>&1 | tail -3 && echo \"---\" && git log --oneline main..origin/main 2>&1 | head -10 && echo \"=== ours not on remote ===\" && g","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-30T00:37:57Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/shopPRO\" && git diff be6d8a2f fba215b3 --stat 2>&1 | tail -5\",\"description\":\"Compare our fix commit vs remote's fix commit\"},\"tool_response\":{\"stdout\":\"\","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-30T00:38:15Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/shopPRO\" && git tag -d v0.350 && git tag v0.350 fba215b3\",\"description\":\"Re-point v0.350 to remote fix sha\"},\"tool_response\":{\"stdout\":\"Deleted tag 'v0.3","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-30T00:38:20Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/shopPRO\" && git rebase --onto origin/main be6d8a2f main 2>&1 | tail -10\",\"description\":\"Rebase build commit onto origin/main\"},\"tool_response\":{\"stdout\":\","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-30T00:38:24Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/shopPRO\" && git status --short 2>/dev/null | grep -v \"^??\" | head -10\",\"description\":\"See unstaged changes\"},\"tool_response\":{\"stdout\":\" M .htaccess\\n","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-30T00:38:31Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/shopPRO\" && git stash push -u -m \"crlf-noise\" -- .htaccess .phpunit.result.cache .scannerwork .serena .vscode autoload templates \"updates/0.10\" \"updates/0.20\","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-30T00:38:35Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/shopPRO\" && git status --short 2>/dev/null | grep -v \"^??\" | head -5\",\"description\":\"Confirm tree clean\"},\"tool_response\":{\"stdout\":\" M updates/0.30/ve","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-30T00:38:40Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/shopPRO\" && git stash push -u -m \"crlf-noise-2\" -- \"updates/0.30/ver_0.305_manifest.json\" \"updates/0.30/ver_0.324_files.txt\" \"updates/0.30/ver_0.324_sql.txt","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-30T00:38:45Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/shopPRO\" && git stash push -u -m \"crlf-3\" -- \"updates/changelog.php\" \"updates/shopPRO_sql.txt\" 2>&1 | tail -2 && git status --short 2>/dev/null | grep -v \"^","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-30T00:38:49Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/shopPRO\" && git rebase --onto origin/main be6d8a2f main 2>&1 | tail -10\",\"description\":\"Rebase build commit onto origin/main\"},\"tool_response\":{\"stdout\":\","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-30T00:38:58Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/shopPRO\" && git log --oneline -3 && echo \"---\" && git push 2>&1 | tail -5 && git push origin v0.350 2>&1 | tail -5\",\"timeout\":120000,\"description\":\"Push re","cwd":"/c/visual studio code/projekty/shopPRO"}

View File

@@ -0,0 +1,191 @@
---
phase: 17-cart-summary-transport-cost-fix
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- autoload/front/Controllers/ShopBasketController.php
- templates/shop-basket/summary-view.php
- tests/Unit/front/Controllers/ShopBasketControllerSummaryViewTest.php
autonomous: false
delegation: off
---
<objective>
## Goal
Naprawic blad na stronie /koszyk-podsumowanie, gdzie wybrana forma wysylki oraz laczna kwota zamowienia pokazywane sa za 0,00 zl, mimo ze koszyk nie osiagnal progu darmowej dostawy.
## Purpose
Klient widzi nieprawidlowe podsumowanie zamowienia. Koszt transportu w szablonie summary-view.php jest redukowany do zera zawsze, gdy transport ma flage `delivery_free = 1`, bez sprawdzenia czy wartosc koszyka przekroczyla prog `$settings['free_delivery']`. W efekcie klient widzi "0,00 zl" i zaniżona sume zamowienia. Po zlozeniu zamowienia dane w bazie i ostatecznej cenie moga sie roznic, co psuje zaufanie i ksiegowosc.
## Output
- ShopBasketController::summaryView() przekazuje do szablonu koszt transportu po uwzglednieniu progu darmowej dostawy (nowy klucz `transport_cost_effective` oraz `free_delivery_applies`).
- Szablon summary-view.php pokazuje koszt transportu i sume koncowa na podstawie tych kluczy zamiast surowej flagi `delivery_free`.
- Nowy test jednostkowy potwierdza logike wyliczania kosztu w kontrolerze dla 3 scenariuszy (basket ponizej progu, basket rowny progowi, transport bez `delivery_free`).
</objective>
<context>
## Project Context
@.paul/PROJECT.md
@.paul/ROADMAP.md
@.paul/STATE.md
## Source Files
@autoload/front/Controllers/ShopBasketController.php
@autoload/Domain/Transport/TransportRepository.php
@autoload/Domain/Basket/BasketCalculator.php
@templates/shop-basket/summary-view.php
@templates/shop-basket/basket-transport-methods.php
## Powiazane pliki (do odwolania)
- settings.free_delivery w `pp_settings` (globalny prog darmowej dostawy)
- Helpers::normalize_decimal / Helpers::decimal (format kwot)
</context>
<acceptance_criteria>
## AC-1: Transport z flaga delivery_free ponizej progu pokazuje rzeczywisty koszt
```gherkin
Given transport ma `delivery_free = 1`, cost = 15.00 zl, a `$settings['free_delivery']` = 300 zl
And wartosc koszyka (po kuponie) wynosi 150 zl
When klient wchodzi na /koszyk-podsumowanie
Then linia transportu pokazuje "15,00 zl"
And laczna kwota zamowienia zawiera te 15,00 zl
```
## AC-2: Transport z flaga delivery_free powyzej progu pokazuje 0,00 zl
```gherkin
Given transport ma `delivery_free = 1`, cost = 15.00 zl, a `$settings['free_delivery']` = 300 zl
And wartosc koszyka (po kuponie) wynosi 350 zl
When klient wchodzi na /koszyk-podsumowanie
Then linia transportu pokazuje "0,00 zl"
And laczna kwota nie zawiera kosztu transportu
```
## AC-3: Transport bez flagi delivery_free zawsze pokazuje swoj koszt
```gherkin
Given transport ma `delivery_free = 0`, cost = 25.00 zl
And wartosc koszyka wynosi 500 zl (powyzej dowolnego progu)
When klient wchodzi na /koszyk-podsumowanie
Then linia transportu pokazuje "25,00 zl"
And laczna kwota zawiera te 25,00 zl
```
## AC-4: Suma testow PHPUnit nie maleje, nowy test zielony
```gherkin
Given istniejacy zestaw testow `./test.ps1`
When uruchamiam pelna suite
Then nowy test `ShopBasketControllerSummaryViewTest` przechodzi
And zadne istniejace testy nie zaczynaja failowac
```
</acceptance_criteria>
<tasks>
<task type="auto">
<name>Task 1: Przekaz wyliczony koszt transportu do summary-view z kontrolera</name>
<files>autoload/front/Controllers/ShopBasketController.php</files>
<action>
W metodzie `summaryView()` (ok. linia 270):
- Po pobraniu `$transport` (findActiveByIdCached) wylicz kwote koszyka po kuponie uzywajac `\Domain\Basket\BasketCalculator::summaryPrice( $basket, $coupon )` - tak jak robi to `transportMethodsFront`.
- Wczytaj `$settings['free_delivery']` z globala `$settings`.
- Ustaw `free_delivery_applies = false` gdy transport nie istnieje; w przeciwnym razie `true` wtedy i tylko wtedy gdy `$transport['delivery_free'] == 1` ORAZ `normalize_decimal($products_summary) >= normalize_decimal($settings['free_delivery'])`.
- Wylicz `transport_cost_effective` = `free_delivery_applies ? 0.0 : (float)$transport['cost']`.
- Do `Tpl::view` przekaz dodatkowe klucze `transport_cost_effective` i `free_delivery_applies`.
Nie zmieniaj istniejacych kluczy (transport, payment_method itd.) zeby nie zepsuc innych uzyc szablonu.
Nie modyfikuj logiki tokenu zamowienia ani guardow.
Unikaj: dodawania nowych metod do TransportRepository (kalkulacja nalezy do warstwy koszyka, nie transportu).
</action>
<verify>Recznie odczytaj plik, upewnij sie ze dane sa w tablicy Tpl::view i sa uzywane deterministycznie dla transport === null.</verify>
<done>AC-1, AC-2 i AC-3 zaspokojone po stronie danych; AC-4 kontrolera.</done>
</task>
<task type="auto">
<name>Task 2: Zaktualizuj summary-view.php aby uzywal wyliczonych kluczy</name>
<files>templates/shop-basket/summary-view.php</files>
<action>
W bloku "basket-summary" (ok. linii 97-115):
- Zamien warunek `$this->transport['delivery_free'] == 1` na `$this->free_delivery_applies`.
- Zamiast `$this->transport['cost']` wyswietlaj `$this->transport_cost_effective` w galezi "else" oraz w kwocie koncowej.
- Linia koncowej kwoty (order-summary): `$this->free_delivery_applies ? decimal($summary) : decimal($summary + $this->transport_cost_effective)`.
- Zadbaj o poprawne wyswietlenie gdy `transport` jest `null` (skeleton: zachowaj stary fallback - brak kosztu dodawanego).
Nie modyfikuj pozostalych fragmentow (produkty, adres, GTM itd.).
Unikaj: duplikowania logiki progu w szablonie - szablon ma wyswietlac, nie liczyc.
</action>
<verify>W szablonie nie wystepuje juz `$this->transport['delivery_free']` w tym bloku; nowe klucze sa uzyte dwukrotnie (linia transportu + suma).</verify>
<done>AC-1, AC-2, AC-3 zaspokojone po stronie prezentacji.</done>
</task>
<task type="auto">
<name>Task 3: Test jednostkowy dla logiki kontrolera</name>
<files>tests/Unit/front/Controllers/ShopBasketControllerSummaryViewTest.php</files>
<action>
Utworz nowy plik testow PHPUnit extending `PHPUnit\Framework\TestCase`.
Testuj publiczna metode pomocnicza lub kalkulacje w `summaryView()` przez refleksje/helper - preferowane: wyodrebnic wyliczenie do prywatnej metody i wyeksponowac prywatna metode przez ReflectionMethod (bez zmiany publicznego API).
Alternatywa: utworz w kontrolerze protected method `calculateTransportCostForSummary(array $transport = null, array $basket, $coupon, float $freeDeliveryThreshold): array` zwracajaca `['transport_cost_effective' => float, 'free_delivery_applies' => bool]` i pokryj ja testami bezposrednio.
Trzy scenariusze (AC-1, AC-2, AC-3) + czwarty: transport === null -> cost 0.0, applies false.
Mock `\Domain\Basket\BasketCalculator::summaryPrice` nie jest wymagany - podaj gotowa liczbe w tescie.
Test musi sie uruchamiac pod `./test.ps1`.
Unikaj: testowania na realnej bazie - stub Medoo zaslugujac na AAA.
</action>
<verify>Uruchom `./test.ps1 tests/Unit/front/Controllers/ShopBasketControllerSummaryViewTest.php` - 4 testy zielone. Potem `./test.ps1` pelna suite - liczba testow >= 825 (pelna, bez regresji).</verify>
<done>AC-4 zaspokojone.</done>
</task>
<task type="checkpoint:human-verify" gate="blocking">
<what-built>
Poprawka koszt transportu na /koszyk-podsumowanie.
</what-built>
<how-to-verify>
1. W panelu admina upewnij sie, ze co najmniej jedna metoda transportu ma `delivery_free = 1` i niezerowy `cost` (np. 15 zl).
2. Ustaw `settings.free_delivery` na np. 300 zl.
3. Dodaj do koszyka produkty o wartosci PONIZEJ progu (np. 150 zl).
4. Wejdz na /koszyk, wybierz transport z `delivery_free = 1`, przejdz do /koszyk-podsumowanie.
5. Potwierdz, ze linia transportu pokazuje "15,00 zl" (nie "0,00 zl") i suma zawiera ten koszt.
6. Dolow koszyk do wartosci POWYZEJ progu (>300 zl), odswiez /koszyk-podsumowanie.
7. Potwierdz, ze linia transportu pokazuje "0,00 zl" i suma NIE zawiera kosztu transportu.
8. Wybierz transport bez `delivery_free = 1` (np. kurier 25 zl), potwierdz ze zawsze pokazuje 25,00 zl.
</how-to-verify>
<resume-signal>Wpisz "approved" aby zakonczyc, lub opisz niezgodnosc do poprawy.</resume-signal>
</task>
</tasks>
<boundaries>
## DO NOT CHANGE
- autoload/Domain/Transport/TransportRepository.php (kalkulacja kosztu transportu juz jest w `transportMethodsFront`; nie duplikujemy logiki tam).
- autoload/Domain/Basket/BasketCalculator.php (wyliczenie wartosci koszyka pozostaje bez zmian).
- templates/shop-basket/basket-transport-methods.php (lista metod na /koszyk dziala poprawnie).
- Logika tokenu zamowienia w ShopBasketController (createOrderSubmitToken, consumeOrderSubmitToken).
- Struktura bazy danych (brak migracji).
## SCOPE LIMITS
- Plan naprawia WYLACZNIE wyswietlanie kosztu na /koszyk-podsumowanie.
- Nie refaktoryzujemy summary-view.php poza blokiem transportu.
- Nie zmieniamy mechanizmu cache transportu.
- Nie dodajemy nowych ustawien/kolumn w bazie.
</boundaries>
<verification>
Przed zamknieciem planu:
- [ ] `./test.ps1` pelna suite zielona (wszystkie >=824 + 4 nowe testy).
- [ ] Recznie zweryfikowano 3 scenariusze na /koszyk-podsumowanie (checkpoint human-verify).
- [ ] W summary-view.php nie wystepuje juz `$this->transport['delivery_free']` w sekcji podsumowania.
- [ ] Nowy plik testu istnieje i jest w strukturze `tests/Unit/front/Controllers/`.
- [ ] Kod zgodny z PHP 7.4 (brak `match`, named arguments itd.).
</verification>
<success_criteria>
- Wszystkie 4 AC zaspokojone.
- Suita PHPUnit zielona bez regresji.
- Checkpoint human-verify zaakceptowany.
- Brak nowych ostrzezen/bledow w logach.
</success_criteria>
<output>
Po zakonczeniu utworz `.paul/phases/17-cart-summary-transport-cost-fix/17-01-SUMMARY.md`
</output>

View File

@@ -0,0 +1,150 @@
---
phase: 17-cart-summary-transport-cost-fix
plan: 01
subsystem: checkout
tags: [basket, transport, free-delivery, summary-view, php74]
requires:
- phase: 13-basket-logging-ttl-token
provides: createOrderSubmitToken + TTL i logging w basketSave
provides:
- Poprawna kalkulacja kosztu transportu na /koszyk-podsumowanie
- Testowalna chroniona metoda ShopBasketController::calculateTransportCostForSummary
affects: [przyszle zmiany checkoutu, kupony, promocje darmowej dostawy]
tech-stack:
added: []
patterns:
- "Logika prezentacyjna kosztu transportu trzymana w kontrolerze, nie w szablonie"
- "Chronione metody pomocnicze testowane przez ReflectionMethod"
key-files:
created:
- tests/Unit/front/Controllers/ShopBasketControllerSummaryViewTest.php
modified:
- autoload/front/Controllers/ShopBasketController.php
- templates/shop-basket/summary-view.php
key-decisions:
- "Kalkulacja kosztu transportu zostaje w warstwie kontrolera (summaryView), nie w TransportRepository — Repository dostarcza dane, kontroler interpretuje je dla konkretnego widoku"
- "Metoda calculateTransportCostForSummary pozostaje protected i jest testowana przez Reflection (public API kontrolera bez zmian)"
patterns-established:
- "Szablon summary-view otrzymuje gotowe klucze prezentacyjne (transport_cost_effective, free_delivery_applies) zamiast liczyc progi w locie"
duration: ~25min
started: 2026-04-20T00:00:00Z
completed: 2026-04-20T00:25:00Z
---
# Phase 17 Plan 01: Cart summary transport cost fix — Summary
**Na /koszyk-podsumowanie wybrany transport z flaga delivery_free=1 pokazuje teraz rzeczywisty koszt ponizej progu darmowej dostawy i 0,00 zl powyzej progu — zgodnie z logika listy transportow na /koszyk.**
## Performance
| Metric | Value |
|--------|-------|
| Duration | ~25 min |
| Started | 2026-04-20 |
| Completed | 2026-04-20 |
| Tasks | 4 completed (3 auto + 1 checkpoint) |
| Files modified | 2 modified + 1 created |
## Acceptance Criteria Results
| Criterion | Status | Notes |
|-----------|--------|-------|
| AC-1: Transport z delivery_free ponizej progu pokazuje rzeczywisty koszt | Pass | Test `testTransportWithDeliveryFreeBelowThresholdShowsRealCost` + manualna weryfikacja |
| AC-2: Transport z delivery_free powyzej progu pokazuje 0,00 zl | Pass | Test `testTransportWithDeliveryFreeAboveThresholdShowsZero` + manualna weryfikacja |
| AC-3: Transport bez flagi delivery_free zawsze pokazuje koszt | Pass | Test `testTransportWithoutDeliveryFreeAlwaysShowsCost` + manualna weryfikacja |
| AC-4: Suita PHPUnit zielona, nowy test przechodzi | Pass | 834/834 OK, 2318 assertions (6 nowych testow) |
## Accomplishments
- Chroniona metoda `ShopBasketController::calculateTransportCostForSummary()` enkapsuluje regule progowa darmowej dostawy i jest czysto testowalna.
- Szablon `summary-view.php` pozbyty dwoch duplikatow logiki `delivery_free == 1` — uzywa teraz gotowych kluczy widokowych.
- 6 testow jednostkowych pokrywa 3 AC i 3 edge case'y (transport null, prog 0, wartosc koszyka rowna progowi).
- Pelna suita zgadza sie z docs/MEMORY.md (>800 testow, 821 -> 834 po fazie).
## Task Commits
Commit transition-phase jeszcze nie wykonany w tym UNIFY (patrz Deviations).
| Task | Commit | Type | Description |
|------|--------|------|-------------|
| Task 1: Calc effective cost w kontrolerze | (pending) | fix | ShopBasketController::summaryView + calculateTransportCostForSummary |
| Task 2: summary-view.php uzywa nowych kluczy | (pending) | fix | Usuniety odwolanie do transport.delivery_free w bloku podsumowania |
| Task 3: Nowy test jednostkowy | (pending) | test | ShopBasketControllerSummaryViewTest (6 testow) |
## Files Created/Modified
| File | Change | Purpose |
|------|--------|---------|
| `autoload/front/Controllers/ShopBasketController.php` | Modified | Dodana protected method calculateTransportCostForSummary; summaryView przekazuje transport_cost_effective + free_delivery_applies |
| `templates/shop-basket/summary-view.php` | Modified | Wiersz kosztu transportu i suma koncowa uzywaja nowych kluczy zamiast transport.delivery_free |
| `tests/Unit/front/Controllers/ShopBasketControllerSummaryViewTest.php` | Created | 6 testow jednostkowych dla logiki kalkulacji kosztu |
## Decisions Made
| Decision | Rationale | Impact |
|----------|-----------|--------|
| Kalkulacja w kontrolerze, nie w TransportRepository | Repository juz ma `transportMethodsFront` robiace to samo, ale dla listy metod — dla pojedynczego wybranego transportu to decyzja widokowa nalezaca do kontrolera | Brak zmiany publicznego API Repository |
| protected + Reflection do testow | Zgodne z istniejacym wzorcem `ShopBasketControllerTest` (Reflection), nie rozszerza publicznego API | Test izolowany od sesji i globali |
| Boundary na prog > 0 | Jesli `settings.free_delivery = 0`, darmowa dostawa jest wylaczona (brak progu = brak regul) | Ochrona przed niezamierzonym zerowaniem kosztu w sklepach bez tej funkcji |
## Deviations from Plan
### Summary
| Type | Count | Impact |
|------|-------|--------|
| Auto-fixed | 0 | — |
| Scope additions | 2 | 2 dodatkowe edge-case testy (boundary rowny prog + prog 0) |
| Deferred | 1 | Git commit transition-phase do wykonania w transition-phase lub rece |
**Total impact:** Bez scope creepu; dodatki to defensywne testy edge-case'ow.
### Auto-fixed Issues
None.
### Scope Additions
**1. Test dla wartosci koszyka rownej progowi**
- **Found during:** Task 3 (test jednostkowy)
- **Issue:** Plan AC-2 mowi "powyzej progu", granica rowna progowi nie byla pokryta
- **Fix:** Dodany `testTransportWithDeliveryFreeAtExactThresholdShowsZero`
- **Rationale:** Stare `transportMethodsFront` uzywa `>=` — utrzymana spojnosc
**2. Test dla settings.free_delivery = 0**
- **Found during:** Task 1 (implementacja)
- **Issue:** Sklepy bez ustawionego progu darmowej dostawy nie mogly miec zerowanych transportow; guard na > 0 wart pokrycia testem
- **Fix:** Dodany `testZeroFreeDeliveryThresholdDisablesFreeDelivery`
### Deferred Items
- Transition-phase git commit do uruchomienia w ramach `/paul:transition` lub recznego commita (spojne z historycznym wzorcem faz 15 i 16).
## Issues Encountered
| Issue | Resolution |
|-------|------------|
| `test.ps1` nie istnieje w repo (pomimo wzmianki w CLAUDE.md) | Uruchomiono phpunit.phar bezposrednio przez `C:/xampp/php/php.exe phpunit.phar -c phpunit.xml` |
## Next Phase Readiness
**Ready:**
- Logika kosztu transportu w checkoutu spojna miedzy /koszyk i /koszyk-podsumowanie.
- Pelna suita zielona.
**Concerns:**
- Git commit nie wykonany automatycznie — nalezy domknac w transition-phase.
- CLAUDE.md odwoluje sie do `./test.ps1` ktorego nie ma w repo — do rozwazenia porzadkowo.
**Blockers:**
- None.
---
*Phase: 17-cart-summary-transport-cost-fix, Plan: 01*
*Completed: 2026-04-20*

View File

@@ -0,0 +1,258 @@
---
phase: 18-google-feed-permutation-url-fix
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- autoload/Domain/Product/ProductRepository.php
- autoload/Shared/Helpers/Helpers.php
- autoload/front/LayoutEngine.php
- templates/shop-product/_partial/product-attribute.php
- tests/Unit/Domain/Product/ProductRepositoryTest.php
- tests/Unit/Shared/Helpers/HelpersTest.php
autonomous: true
delegation: off
---
<objective>
## Goal
Naprawić linki produktów z permutacją atrybutów w feedzie Google: zamienić separator `/` na `_` między parami `attr-val`, dopasować routing `pp_routes`, konwersję `_``|` w warstwie front oraz preselekcję wartości atrybutów na podstawie `permutation_hash` z URL.
## Purpose
URL z formatu `/slug/20-170/21-175` nie matchował się w `pp_routes` (wzorzec `[0-9-]+` nie obejmuje `/`), więc Google Merchant Center prowadził klientów na stronę główną zamiast na produkt z wybraną kombinacją atrybutów. Strata ruchu komercyjnego z feedu.
## Output
- 4 pliki silnika z nowym separatorem `_`
- Unit testy: regex routingu (Helpers) + generator linku (ProductRepository::appendCombinationToXml)
- SUMMARY z listą akcji post-deploy do wykonania ręcznie na produkcji
</objective>
<context>
## Project Context
@.paul/PROJECT.md
@.paul/ROADMAP.md
@.paul/STATE.md
@.paul/codebase/architecture.md
## Source Files
@autoload/Domain/Product/ProductRepository.php
@autoload/Shared/Helpers/Helpers.php
@autoload/front/LayoutEngine.php
@templates/shop-product/_partial/product-attribute.php
<clarifications>
- **Testy** — Czy dodać unit testy dla zmian?
→ Odpowiedź: Tak — pełne pokrycie (Helpers regex + ProductRepository::appendCombinationToXml)
- **Post-deploy** — Czy wykonać regenerację routes/cache/feedu w ramach fazy?
→ Odpowiedź: Nic — tylko kod; akcje produkcyjne udokumentowane w SUMMARY
- **Redirect 301** — Czy dodać redirecty ze starych URL-i?
→ Odpowiedź: Nie — Google sam zaktualizuje linki z feedu
- **Skills** — /feature-dev required w SPECIAL-FLOWS?
→ Odpowiedź: Override — pomiń (hotfix z konkretną instrukcją, jak w fazach 15/16/17)
</clarifications>
</context>
<acceptance_criteria>
## AC-1: Generator linku w feedzie używa `_`
```gherkin
Given produkt z permutacją atrybutów (permutation_hash = "20-170|21-175")
When wywołany jest ProductRepository::appendCombinationToXml dla feedu Google
Then wygenerowany URL zawiera segment `20-170_21-175` (jeden segment, separator `_`)
And nie zawiera `/` między parami atrybutów
And dotyczy obu gałęzi (z seo_link i fallback p-id-name)
```
## AC-2: Routing `pp_routes` matchuje URL z `_`
```gherkin
Given wzorzec routingu wygenerowany przez Helpers dla produktu z permutacją
When URI to `slug-produktu/20-170_21-175`
Then regex `[0-9_-]+` dopasowuje cały segment permutacji
And `permutation_hash` w wynikowych GET to `20-170_21-175`
And dotyczy obu wariantów (z seo_link i fallback p-id-name)
```
## AC-3: Front konwertuje `_` z URL na `|` przed zapytaniem do bazy
```gherkin
Given GET['permutation_hash'] = "20-170_21-175"
When LayoutEngine renderuje blok PRODUKT
Then ProductRepository::findCached otrzymuje argument "20-170|21-175"
And gdy GET['permutation_hash'] nie istnieje, findCached otrzymuje null
```
## AC-4: Partial atrybutu preselectuje wartość z URL
```gherkin
Given URL produktu z permutation_hash zawierającym parę dla bieżącego atrybutu
When renderuje się templates/shop-product/_partial/product-attribute.php
Then aktywna (checked) jest wartość z URL, nie z is_default
And gdy atrybut nie występuje w hashu, zachowane jest stare zachowanie (is_default)
And blok <script> z fradio_label_click() emitowany jest dla wartości z URL
```
## AC-5: Pełna suita testów zielona
```gherkin
Given wprowadzone zmiany w 4 plikach + 2 nowe/zaktualizowane testy
When uruchomiony jest ./test.ps1
Then wszystkie testy przechodzą (>=836 834 obecnych + 2 nowe)
And brak warningów PHP
```
</acceptance_criteria>
<tasks>
<task type="auto">
<name>Task 1: Zmiana separatora w generatorze feedu i routingu</name>
<files>autoload/Domain/Product/ProductRepository.php, autoload/Shared/Helpers/Helpers.php, autoload/front/LayoutEngine.php</files>
<action>
1. **autoload/Domain/Product/ProductRepository.php** — w `appendCombinationToXml` (~linie 2372 i 2374):
- Zamienić `str_replace('|', '/', $combination['permutation_hash'])` na `str_replace('|', '_', $combination['permutation_hash'])`
- Dotyczy OBU gałęzi (seo_link i fallback `p-id-name`)
- Najpierw przeczytać metodę i potwierdzić obie wystąpienia przed edycją
2. **autoload/Shared/Helpers/Helpers.php** — w generatorze tras (~linie 694 i 699):
- Rozszerzyć regex z `'^' . ... . '/([0-9-]+)$'` na `'^' . ... . '/([0-9_-]+)$'`
- Dotyczy OBU wariantów (seo_link i fallback `p-id-name`)
3. **autoload/front/LayoutEngine.php** — w bloku `// PRODUKT` (~linia 196):
- Wyciągnąć `permutation_hash` do zmiennej z konwersją `_``|`:
```php
$permutation_hash = isset($_GET['permutation_hash']) ? str_replace('_', '|', $_GET['permutation_hash']) : null;
```
- Przekazać `$permutation_hash` do `findCached()` zamiast inline `$_GET['permutation_hash'] ?? null`
Avoid:
- Zmian w `findCached()` lub `permutation_hash` w bazie — separator w DB pozostaje `|`
- Modyfikacji innych metod ProductRepository
- PHP 8.0+ syntaxu (`match`, named args)
</action>
<verify>
- `grep -n "str_replace.*'|'.*'/'" autoload/Domain/Product/ProductRepository.php` — brak wyników (0 wystąpień)
- `grep -n "str_replace.*'|'.*'_'" autoload/Domain/Product/ProductRepository.php` — 2 wystąpienia
- `grep -n "\[0-9_-\]+" autoload/Shared/Helpers/Helpers.php` — 2 wystąpienia
- `grep -n "\[0-9-\]+\\\$" autoload/Shared/Helpers/Helpers.php` — brak (stary wzorzec usunięty z generatora produktów z permutacją)
- `grep -n "permutation_hash" autoload/front/LayoutEngine.php` — zmienna wyciągnięta przed `findCached`
</verify>
<done>AC-1, AC-2, AC-3 spełnione</done>
</task>
<task type="auto">
<name>Task 2: Preselekcja atrybutu z permutation_hash w partialu</name>
<files>templates/shop-product/_partial/product-attribute.php</files>
<action>
Najpierw przeczytać cały plik partiala (mały, ~kilkadziesiąt linii) i zlokalizować pętlę `foreach` po `values` oraz miejsca używające `$value['is_default']`.
Na początku partiala (przed pętlą po values) dodać:
```php
$forced_value_id = null;
if ( isset( $_GET['permutation_hash'] ) && $_GET['permutation_hash'] !== '' )
{
$pairs = explode( '|', str_replace( '_', '|', $_GET['permutation_hash'] ) );
foreach ( $pairs as $pair )
{
$parts = explode( '-', $pair );
if ( count( $parts ) == 2 && (int)$parts[0] === (int)$this -> attribute['id'] )
{
$forced_value_id = (int)$parts[1];
break;
}
}
}
```
W pętli foreach po values, przed użyciem flagi `is_default`, policzyć:
```php
$is_active = $forced_value_id !== null
? ( (int)$value['id'] === $forced_value_id )
: (bool)$value['is_default'];
```
Zastąpić WSZYSTKIE użycia `$value['is_default']` w kontekście aktywności (checked, fradio_label_click) zmienną `$is_active`. Nie ruszać `is_default` jeśli używane gdzie indziej semantycznie (np. atrybut metadata).
Avoid:
- Modyfikacji `templates_user/` (potwierdzono: nie istnieje w tym repo)
- Zmian struktury HTML / klas CSS
- PHP 8.0+ syntaxu
</action>
<verify>
- `grep -n "forced_value_id" templates/shop-product/_partial/product-attribute.php` — co najmniej 4 wystąpienia (deklaracja, set, użycie w `$is_active`, użycie w `$is_active`)
- `grep -n "is_active" templates/shop-product/_partial/product-attribute.php` — co najmniej 2 wystąpienia (deklaracja + użycie w checked/script)
- Manualnie potwierdzić: `checked="checked"` używa `$is_active`, `fradio_label_click(...)` script gate'owany przez `$is_active`
</verify>
<done>AC-4 spełnione</done>
</task>
<task type="auto">
<name>Task 3: Unit testy dla regex routingu i generatora linku</name>
<files>tests/Unit/Shared/Helpers/HelpersTest.php, tests/Unit/Domain/Product/ProductRepositoryTest.php</files>
<action>
Najpierw sprawdzić strukturę istniejących testów (szczególnie czy `HelpersTest.php` istnieje — jeśli nie, utworzyć z bootstrapem zgodnym z innymi testami w `tests/Unit/Shared/`).
1. **Helpers — test regex routingu z `_`:**
- Wywołać generator tras dla produktu z permutacją (jeśli metoda jest publiczna; w przeciwnym razie test integracyjny z mockiem `$mdb` zwracającym permutacje produktu)
- Zweryfikować że wygenerowany pattern zawiera `[0-9_-]+` zamiast `[0-9-]+`
- Test: `preg_match` na patternie z URI `slug/20-170_21-175` zwraca true i wyciąga `20-170_21-175` jako capture group
- Test negatywny: pattern NIE matchuje `slug/20-170/21-175` (stary format ze slashem — chcemy 404, nie przypadkowy match)
2. **ProductRepository::appendCombinationToXml — test separatora `_`:**
- Może być nieosiągalna metoda private/protected. Strategia A (preferowana): jeśli private, użyć ReflectionMethod do wywołania na instancji z mockiem `$mdb`. Strategia B: jeśli zbyt skomplikowane, dodać minimalny test który wywołuje publiczną metodę feedu z mockiem i sprawdza wygenerowany XML.
- Mock combination z `permutation_hash = '20-170|21-175'`, `seo_link = 'jakas-fraza'`
- Asercja: w wygenerowanym XML link zawiera `jakas-fraza/20-170_21-175`, NIE zawiera `20-170/21-175`
- Drugi test: gałąź fallback (brak `seo_link`) — link `p-{id}-{name}/20-170_21-175`
Trzymać się konwencji: AAA, mock Medoo (`$this->createMock(\medoo::class)`), namespace tests jak w istniejących plikach. Brak PHP 8.0+ syntaxu. Nazwy metod z `test` prefiksem.
</action>
<verify>
- `./test.ps1 tests/Unit/Shared/Helpers/HelpersTest.php` — przechodzi
- `./test.ps1 tests/Unit/Domain/Product/ProductRepositoryTest.php` — przechodzi (wszystkie testy, łącznie z nowymi)
- `./test.ps1` — pełna suita zielona, count >= 836
</verify>
<done>AC-5 spełnione (testy zielone, ≥2 nowe testy pokrywające AC-1 i AC-2)</done>
</task>
</tasks>
<boundaries>
## DO NOT CHANGE
- Format `permutation_hash` w bazie (kolumna `pp_shop_product_combinations.permutation_hash` pozostaje z separatorem `|`)
- Sygnatura `ProductRepository::findCached()` — przyjmuje hash z `|`
- Inne metody ProductRepository / inne kontrolery / inne partiale
- Plików `templates_user/` (nie istnieje w tym repo, akcja po stronie klientów)
- Schemat bazy danych
- `.htaccess` w roocie (redirecty 301 wykluczone z scope)
## SCOPE LIMITS
- Tylko 4 pliki silnika + 2 pliki testów (lista w `files_modified`)
- Brak automatycznej regeneracji `pp_routes` — udokumentowane w SUMMARY jako akcja deploy
- Brak czyszczenia cache `pp_routes:all` w fazie — akcja deploy
- Brak regeneracji `google-feed.xml` w fazie — akcja deploy
- Brak redirectów 301 ze starych URL-i
</boundaries>
<verification>
- [ ] `./test.ps1` — pełna suita zielona (≥836 testów)
- [ ] Brak `str_replace('|', '/', ...)` w ProductRepository (grep)
- [ ] `[0-9_-]+` w obu wzorcach Helpers (grep)
- [ ] `permutation_hash` wyciągnięte do zmiennej w LayoutEngine z konwersją `_`→`|`
- [ ] Partial używa `$is_active` (forced_value_id || is_default) zamiast surowego `is_default`
- [ ] Wszystkie 5 AC spełnione
</verification>
<success_criteria>
- 4 pliki silnika zmienione zgodnie z instrukcją
- 2 nowe / zaktualizowane testy: routing regex + generator XML linku
- Pełna suita testów zielona
- Brak regresji w istniejących testach (834 → ≥836)
- SUMMARY zawiera dokładną listę akcji post-deploy (regen pp_routes, clear cache, regen feedu, resubmit GMC)
</success_criteria>
<output>
After completion, create `.paul/phases/18-google-feed-permutation-url-fix/18-01-SUMMARY.md` containing:
- Co zmienione (lista plików + diff highlights)
- Akcje post-deploy do wykonania ręcznie na produkcji (kolejność: regen pp_routes → clear cache pp_routes:all → regen google-feed.xml → resubmit GMC)
- Test count delta
- Decyzje (override /feature-dev, brak redirectów 301)
</output>

View File

@@ -0,0 +1,168 @@
---
phase: 18-google-feed-permutation-url-fix
plan: 01
subsystem: feed/routing
tags: [google-merchant, pp_routes, permutation, regex, php74]
requires:
- phase: prior-architecture
provides: ProductRepository, Helpers::htacces, LayoutEngine, frontAttributePartial
provides:
- Separator URL permutacji `_` zamiast `/` w feedzie Google
- Wzorzec routingu pp_routes obejmujący `[0-9_-]+`
- Konwersja `_``|` po stronie front przed `findCached`
- Preselekcja wartości atrybutu z `permutation_hash` w partialu
affects: [google-feed, pp_routes, frontend-product-attributes]
tech-stack:
added: []
patterns:
- "URL feedu: jeden segment z `_` zamiast wielu segmentów ze `/`"
- "DB format `|`, URL format `_`, konwersja w warstwie front"
key-files:
created:
- tests/Unit/Shared/Helpers/HelpersRoutingTest.php
- tests/Unit/Domain/Product/ProductFeedLinkTest.php
modified:
- autoload/Domain/Product/ProductRepository.php
- autoload/Shared/Helpers/Helpers.php
- autoload/front/LayoutEngine.php
- templates/shop-product/_partial/product-attribute.php
key-decisions:
- "Separator URL `_` zamiast `/` (one segment dopasowywalny przez pp_routes)"
- "Konwersja `_``|` w LayoutEngine, format DB pozostaje `|`"
- "Brak redirectów 301 — Google sam zaktualizuje feed"
- "Brak automatycznych akcji post-deploy — udokumentowane jako manual steps"
- "Override /feature-dev (hotfix z konkretną instrukcją)"
patterns-established:
- "Forced value via URL parameters w partialach (preselekcja zamiast is_default)"
- "Reflection-based test prywatnych metod XML feedu"
duration: ~25min
completed: 2026-04-30
---
# Phase 18 Plan 01: Google feed permutation URL fix — Summary
**Linki produktów z permutacją w feedzie Google używają teraz `_` jako separatora par `attr-val`, routing `pp_routes` matchuje takie URL-e, a partial atrybutu preselectuje wartości na podstawie `permutation_hash` z URL.**
## Performance
| Metric | Value |
|--------|-------|
| Duration | ~25 min |
| Started | 2026-04-30 |
| Completed | 2026-04-30 |
| Tasks | 3 / 3 |
| Files modified | 6 (4 silnik + 2 testy) |
| Tests | 834 → 841 (+7) |
## Acceptance Criteria Results
| Criterion | Status | Notes |
|-----------|--------|-------|
| AC-1: Generator linku w feedzie używa `_` | Pass | ProductFeedLinkTest.testCombinationLinkUsesUnderscoreInSeoLinkBranch + fallback + single-pair |
| AC-2: Routing `pp_routes` matchuje URL z `_` | Pass | HelpersRoutingTest weryfikuje obecność `[0-9_-]+` w generatorze + preg_match na nowym wzorcu |
| AC-3: Front konwertuje `_` z URL na `|` przed zapytaniem | Pass | LayoutEngine.php:196 — zmienna `$permutation_hash` z `str_replace('_','|',...)` |
| AC-4: Partial preselectuje wartość z URL | Pass | `$forced_value_id` + `$is_active` używane w `checked` i `<script>` |
| AC-5: Pełna suita testów zielona | Pass | PHPUnit: 841 tests, 2330 assertions, 0.764s |
## Accomplishments
- Naprawa krytycznego problemu komercyjnego: feed Google prowadził klientów na home zamiast na produkt
- Spójność stosu: separator URL (`_`) ↔ format DB (`|`) z jasnym punktem konwersji w warstwie front
- 7 nowych testów (4 routing + 3 generator linku) — pełne pokrycie zmiany
- Reflection-based test metody prywatnej `appendCombinationToXml` z mockiem Medoo i mockiem TransportRepository
- UI strony produktu wchodząc z linka feedu pokazuje wybraną kombinację atrybutów (zamiast `is_default`)
## Files Created/Modified
| File | Change | Purpose |
|------|--------|---------|
| `autoload/Domain/Product/ProductRepository.php` | Modified (×2) | `appendCombinationToXml`: separator `/``_` w obu gałęziach (seo_link i fallback) |
| `autoload/Shared/Helpers/Helpers.php` | Modified (×2) | Generator pp_routes: regex `[0-9-]+``[0-9_-]+` w obu wariantach |
| `autoload/front/LayoutEngine.php` | Modified | Wyciągnięcie `$permutation_hash` z konwersją `_``|` przed `findCached` |
| `templates/shop-product/_partial/product-attribute.php` | Modified | `$forced_value_id` z URL + `$is_active` w `checked`/`<script>` |
| `tests/Unit/Shared/Helpers/HelpersRoutingTest.php` | Created | 4 testy regex routingu (file content + preg_match dla `_` i odrzucenia `/`) |
| `tests/Unit/Domain/Product/ProductFeedLinkTest.php` | Created | 3 testy `appendCombinationToXml` via Reflection (seo_link, fallback, single-pair) |
## Decisions Made
| Decision | Rationale | Impact |
|----------|-----------|--------|
| Separator URL `_` zamiast `/` | `_` mieści się w jednym segmencie regex `[0-9_-]+`; `/` wymagałby zmiany struktury routingu | Czysty fix, minimalna zmiana w pp_routes |
| Format DB pozostaje `|` | Nie tykać zapisanych danych w `pp_shop_product_combinations.permutation_hash` | Zero migracji DB; konwersja tylko w warstwie I/O |
| Brak redirectów 301 | Stare URL-e z feedu wymarły gdy GMC zaciągnie nowy feed | Mniej kodu w `.htaccess`, brak długoterminowego balastu |
| Brak automatycznych akcji post-deploy | Hotfix dotyczy tylko silnika; regen routes/cache/feed są zależne od środowiska | Wymaga manualnego runbook'a (poniżej) |
| Override /feature-dev | Hotfix z konkretną instrukcją od użytkownika, jak fazy 15/16/17 | Skill audit logged |
## Deviations from Plan
### Summary
| Type | Count | Impact |
|------|-------|--------|
| Auto-fixed | 0 | — |
| Scope additions | 0 | — |
| Deferred | 0 | — |
**Total impact:** Zero. Plan wykonany dokładnie według instrukcji.
### Auto-fixed Issues
None.
### Deferred Items
None.
## Issues Encountered
| Issue | Resolution |
|-------|------------|
| `ProductRepository::appendCombinationToXml` jest `private` z zależnościami od `transportRepoForXml` i `AttributeRepository` (DB) | Test via `ReflectionMethod`; mock Medoo (`select``[]`, `get``null`); wstrzyknięty mock `TransportRepository::lowestTransportPrice` zwracający `0.0` na dynamicznej property `transportRepoForXml` |
| Brak istniejącego folderu `tests/Unit/Shared/Helpers/` | Utworzony nowy katalog + `HelpersRoutingTest.php` |
| `Helpers::htacces()` zbyt rozległe do testu E2E (DB writes, file I/O) | Test pośredni: assercje na zawartości pliku Helpers.php (file_get_contents) + standalone `preg_match` na sample patternie |
## Post-deploy runbook (manual, kolejność krytyczna)
Wymagane akcje na środowisku produkcyjnym po deployu kodu:
1. **Regeneracja `pp_routes`** — wywołać `Helpers::htacces()` (np. zapis ustawień w adminie lub regeneracja sitemap), żeby nowe wzorce z `_` trafiły do bazy. Bez tego stare wzorce `[0-9-]+` w `pp_routes` nadal nie zmatchują URL z `_`.
2. **Wyczyścić cache routingu** — skasować klucz `pp_routes:all` w Redis (`DEL pp_routes:all`) albo poczekać 24h na expiry. `index.php:63` cachuje routing.
3. **Regeneracja feedu Google** — uruchomić cron `cron/cron-xml.php` (`\admin\factory\ShopProduct::generate_google_feed_xml()`), żeby `google-feed.xml` zawierał nowe linki z `_`.
4. **Resubmit feedu w GMC** — automatycznie wg harmonogramu lub ręcznie "Fetch now".
5. **Stare URL-e w GMC** — same wypadną z indeksu po podmianie feedu (Google).
Walidacja po deployu:
- `https://domena/google-feed.xml` — tagi `<link>` zawierają `_` zamiast `/` między parami
- `https://domena/slug-produktu/20-170_21-175` — ładuje produkt z preselectowaną kombinacją (nie home)
- GMC: feed bez błędów "Landing page error"
## Skill Audit (Phase 18)
| Expected | Invoked | Notes |
|----------|---------|-------|
| /feature-dev | ○ | User-approved override (hotfix z konkretną instrukcją) |
| /koniec-pracy | ○ | Pending — uruchomić przy zakończeniu sesji jeśli release wchodzi do update package |
## Next Phase Readiness
**Ready:**
- Hotfix completed; pełna suita zielona
- Brak zmian w schemacie DB
- Wzorzec preselekcji partial z URL parameter dostępny dla innych partiali (jeśli pojawi się potrzeba)
**Concerns:**
- Akcje post-deploy (regen routes / clear cache / regen feed) wymagają manualnego wykonania — brak automatu
- Klienci sklepu mający własne nadpisane `templates_user/shop-product/_partial/product-attribute.php` muszą zaaplikować zmianę u siebie (Tpl::view priorytetuje `templates_user/`)
**Blockers:**
- None.
---
*Phase: 18-google-feed-permutation-url-fix, Plan: 01*
*Completed: 2026-04-30*

View File

@@ -0,0 +1,264 @@
---
phase: 19-frontend-meta-tags-fix
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- autoload/front/LayoutEngine.php
- tests/Unit/Front/LayoutEngineMetaTagsTest.php
autonomous: false
delegation: off
---
<objective>
## Goal
Strona kategorii (np. `/sen-i-otulenie`) i strona produktu (np. `/kocyk-niemowlaka-szczeniak-z-balonikiem-fuksja`) renderują poprawne `<title>`, `<meta name="description">` i `<meta name="keywords">` zgodne z danymi SEO kategorii/produktu z bazy. Obecnie wszystkie podstrony pokazują tytuł strony głównej (`"Sklep z akcesoriami dla dzieci i niemowląt... | shopPRO 1"`) oraz literalne wartości `keywords`/`description` z layoutu/danych homepage.
## Purpose
Niepoprawne metatagi blokują indeksację SEO i wyświetlanie w Google Merchant Center / wynikach wyszukiwania. Klient sklepu (właściciel) traci ruch organiczny — każda podstrona ma identyczny title i puste meta.
## Output
- Diagnostyka: ustalona root cause (pp_routes vs sesyjny `$page` vs dane w DB)
- Fix w `autoload/front/LayoutEngine.php` (lub w sąsiednim kodzie inicjującym `$page`)
- Test jednostkowy dla logiki podmiany metatagów
- Suita testów PHPUnit zielona
- Weryfikacja human-verify: 3 URL-e (homepage, kategoria, produkt) zwracają różne `<title>` / `<meta description>`
</objective>
<context>
## Project Context
@.paul/PROJECT.md
@.paul/ROADMAP.md
@.paul/STATE.md
## Source Files
@autoload/front/LayoutEngine.php
@index.php
@autoload/Domain/Category/CategoryRepository.php
@autoload/Domain/Product/ProductRepository.php
@autoload/Domain/Pages/PagesRepository.php
## Clarifications
- **Root cause** — Hipoteza wymaga weryfikacji w bazie (pp_routes + pp_shop_categories_languages + pp_shop_products_languages). Brak dostępu do DB z lokalnego środowiska planowania.
→ Odpowiedź: Najpierw diagnostyka, potem fix — nie zakładamy bugu w kodzie ani w danych a priori.
- **pp_routes content** — Nie znamy treści destination dla URL-i kategorii/produktu w bazie produkcyjnej.
→ Odpowiedź: Sprawdź w bazie (Task 1).
- **DB meta values** — Nie wiemy czy meta_keywords/meta_description dla `sen-i-otulenie` w bazie są wypełnione poprawnie, czy zawierają literalne `"keywords"`/`"description"`.
→ Odpowiedź: Sprawdź w bazie (Task 1).
## Background — co już wiemy
- `LayoutEngine::show()` (linie 152, 174, 194) podmienia `$page['language']['title']`, `meta_keywords`, `meta_description` tylko jeżeli `$_GET['category']`, `$_GET['article']` lub `$_GET['product']` są ustawione (przez `Helpers::get(...)`).
- `$_GET` jest zasilane przez `pp_routes``index.php:76-94` matchuje regex, parsuje destination jako query string i merge'uje z `$_GET`.
- `$page` jest cache'owane w sesji (`index.php:147``Helpers::get_session('page')`) i fallbackuje do `frontPageDetails('')` (homepage) jeśli puste.
- Na produkcji: layout HTML zawiera `<title>[TITLE]</title>`, `<meta name="keywords" content="[META_KEYWORDS]">`, `<meta name="description" content="[META_DESCRIPTION]">` — placeholder mechanizm działa, ale podmieniane wartości są nieprawidłowe.
- og:title/og:description dla produktu działają poprawnie (są dorzucane bezpośrednio przez DOM w `index.php:242-292`).
</context>
<acceptance_criteria>
## AC-1: Diagnostyka — ustalona root cause
```gherkin
Given dostęp do bazy produkcyjnej shoppro.project-dc.pl (FTP/SSH/phpMyAdmin)
When wykonamy SQL diagnostyczne dla pp_routes + meta kategorii sen-i-otulenie + meta produktu 522
Then znamy konkretną przyczynę: czy pp_routes nie ustawia category=/product=, czy meta w DB są zaślepkami, czy bug jest w session caching $page
And wynik diagnostyki jest zapisany w plan-fix sekcji `<context>` przed Task 2
```
## AC-2: Strona kategorii pokazuje własny `<title>` i `<meta description>`
```gherkin
Given kategoria w bazie ma wypełnione meta_title="Pościel dla dzieci" i meta_description="Kocyki, pościele..."
When klient otwiera /sen-i-otulenie
Then `<title>` zawiera meta_title kategorii (plus ' | ' + firm_name)
And `<meta name="description" content="...">` zawiera meta_description kategorii
And NIE pokazuje tytułu strony głównej
```
## AC-3: Strona produktu pokazuje własny `<title>` i `<meta description>`
```gherkin
Given produkt w bazie ma wypełnione meta_title i meta_description
When klient otwiera /kocyk-niemowlaka-szczeniak-z-balonikiem-fuksja
Then `<title>` zawiera meta_title produktu (plus ' | ' + firm_name)
And `<meta name="description" content="...">` zawiera meta_description produktu
And NIE pokazuje tytułu strony głównej
```
## AC-4: Fallback dla pustego meta_title/meta_description
```gherkin
Given kategoria/produkt ma puste meta_title w bazie
When klient otwiera tę stronę
Then `<title>` używa nazwy kategorii/produktu + ' | ' + firm_name
And `<meta description>` jest puste (zachowanie obecne brak fallbacku do opisu, by nie zmieniać semantyki)
```
## AC-5: Test jednostkowy + cała suita zielona
```gherkin
Given nowy test tests/Unit/Front/LayoutEngineMetaTagsTest.php
When ./test.ps1 zostanie uruchomione
Then test pokrywa scenariusz: poprawnie podmieniony [TITLE]/[META_KEYWORDS]/[META_DESCRIPTION] dla kategorii i produktu
And cała suita 841+N testów przechodzi na zielono
```
</acceptance_criteria>
<tasks>
<task type="auto">
<name>Task 1: Diagnostyka produkcji — pp_routes + meta w DB</name>
<files>(brak modyfikacji kodu — tylko zapytania SQL)</files>
<action>
Uruchom diagnostyczne SQL na bazie shoppro.project-dc.pl (przez FTP→phpMyAdmin lub SSH):
1. **pp_routes dla URL-i:**
```sql
SELECT pattern, destination FROM pp_routes
WHERE 'sen-i-otulenie' REGEXP CONCAT('^', pattern)
OR 'kocyk-niemowlaka-szczeniak-z-balonikiem-fuksja' REGEXP CONCAT('^', pattern);
```
Sprawdź: czy destination zawiera `category=` / `product=` jako query param.
2. **Meta kategorii sen-i-otulenie:**
```sql
SELECT c.id, c.url, cl.title, cl.meta_title, cl.meta_keywords, cl.meta_description
FROM pp_shop_categories c
JOIN pp_shop_categories_languages cl ON cl.shop_category_id = c.id
WHERE c.url = 'sen-i-otulenie';
```
3. **Meta produktu kocyk-niemowlaka-szczeniak-z-balonikiem-fuksja:**
```sql
SELECT p.id, p.url, pl.name, pl.meta_title, pl.meta_keywords, pl.meta_description
FROM pp_shop_products p
JOIN pp_shop_products_languages pl ON pl.shop_product_id = p.id
WHERE p.url = 'kocyk-niemowlaka-szczeniak-z-balonikiem-fuksja';
```
4. **Default page meta (czy tytuł "Sklep z akcesoriami..." stamtąd pochodzi):**
```sql
SELECT pp.id, ppl.title, ppl.meta_title, ppl.meta_keywords, ppl.meta_description
FROM pp_pages pp
JOIN pp_pages_languages ppl ON ppl.shop_page_id = pp.id
WHERE ppl.title LIKE 'Sklep z akcesoriami%' OR ppl.meta_title LIKE 'Sklep z akcesoriami%';
```
Zapisz wyniki w pliku `.paul/phases/19-frontend-meta-tags-fix/DIAGNOSTICS.md` — surowe wyniki SQL + interpretacja (która z hipotez się potwierdza).
Unikaj: zakładania root cause bez danych. Nie modyfikuj kodu w tym tasku.
</action>
<verify>Plik .paul/phases/19-frontend-meta-tags-fix/DIAGNOSTICS.md istnieje, zawiera wyniki 4 zapytań SQL i konkluzję wskazującą jedną z hipotez (pp_routes / DB meta / session $page)</verify>
<done>AC-1 satisfied: root cause ustalona i udokumentowana</done>
</task>
<task type="checkpoint:decision" gate="blocking">
<decision>Który fix wdrażamy w Task 3 na podstawie wyników diagnostyki?</decision>
<context>Zależnie od wyniku Task 1 — fix dotyka różnych miejsc kodu. Decyzja blokuje Task 3.</context>
<options>
<option id="fix-routes">
<name>Fix pp_routes (jeśli destination nie zawiera category=/product=)</name>
<pros>Naprawia źródło — wszystkie podstrony zaczynają działać; nie dotyka silnika</pros>
<cons>Wymaga update SQL w paczce instalacyjnej; ryzyko nadpisania custom routes klienta</cons>
</option>
<option id="fix-engine-detection">
<name>Fix LayoutEngine — niezależna detekcja kategorii/produktu (jeśli pp_routes OK, ale sesyjny $page wycieka)</name>
<pros>Defensywny — engine sam wykrywa kontekst i nadpisuje meta, niezależnie od $page session</pros>
<cons>Dodaje logikę; może zmienić zachowanie dla custom stron</cons>
</option>
<option id="fix-db-data">
<name>Fix danych w DB (jeśli meta_keywords/meta_description = literalne "keywords"/"description")</name>
<pros>Najprostsze — admin uzupełnia poprawne SEO przez panel</pros>
<cons>To nie jest bug w shopPRO tylko w danych klienta; nie wchodzi do update package</cons>
</option>
<option id="fix-session-bleed">
<name>Fix session $page bleed — index.php nie używa session $page gdy URL wskazuje na category/product/article</name>
<pros>Naprawia konkretną klasę bugów; zmiana ograniczona do index.php</pros>
<cons>Wymaga uważnej regresji — $page jest używane też do menu/breadcrumbs</cons>
</option>
</options>
<resume-signal>Wybierz: fix-routes | fix-engine-detection | fix-db-data | fix-session-bleed (lub kombinacja)</resume-signal>
</task>
<task type="auto">
<name>Task 3: Implementacja fixu + test jednostkowy</name>
<files>autoload/front/LayoutEngine.php (lub index.php), tests/Unit/Front/LayoutEngineMetaTagsTest.php</files>
<action>
Zaimplementuj fix wybrany w Task 2.
Kluczowe zasady (niezależnie od wybranej opcji):
- PHP < 8.0 — bez `match`, bez named args, bez union types, bez str_contains/str_starts_with
- Medoo `$db->get()` zwraca null gdy brak rekordu (NIE false)
- Cache Redis: po fixie wyczyść `pp_routes:all` i `shop\\product:*` jeśli dotykamy danych (Helpers::clear_product_cache lub CacheHandler::deletePattern)
- Nie dotykaj logiki og:title/og:description w index.php — to działa poprawnie
- Zachowaj zachowanie dla strony głównej i CMS pages (regresja!)
Test jednostkowy (`tests/Unit/Front/LayoutEngineMetaTagsTest.php`):
- Mock Medoo via createMock(\medoo::class)
- Scenariusze:
1. category=ID + meta_title wypełniony → `<title>` = meta_title + ' | ' + firm_name
2. category=ID + meta_title pusty → `<title>` = category.title + ' | ' + firm_name
3. product=ID + meta_description wypełniony → `<meta description>` zawiera tę wartość
4. brak category/product/article (homepage) → tytuł strony page'a (regresja)
Komentarze tylko gdzie wyjaśniają "dlaczego" (np. dlaczego ignorujemy sesyjny $page dla kategorii).
</action>
<verify>./test.ps1 tests/Unit/Front/LayoutEngineMetaTagsTest.php zwraca OK; ./test.ps1 (cała suita) — 841+N tests zielono</verify>
<done>AC-2, AC-3, AC-4, AC-5 satisfied</done>
</task>
<task type="checkpoint:human-verify" gate="blocking">
<what-built>Fix metatagów dla kategorii i produktu na froncie</what-built>
<how-to-verify>
1. Wyczyść cache Redis (`pp_routes:all` + product cache) lub poczekaj na TTL
2. Otwórz w przeglądarce 3 URL-e:
- https://shoppro.project-dc.pl/ (homepage — baseline)
- https://shoppro.project-dc.pl/sen-i-otulenie (kategoria)
- https://shoppro.project-dc.pl/kocyk-niemowlaka-szczeniak-z-balonikiem-fuksja (produkt)
3. View Source (Ctrl+U) na każdym z nich. Sprawdź:
- `<title>` jest RÓŻNY dla 3 stron
- `<meta name="description" content="...">` zawiera opis kategorii/produktu (nie "description")
- `<meta name="keywords" content="...">` zawiera słowa kluczowe z DB (lub puste, ale NIE "keywords")
4. Powtórz na innej kategorii i innym produkcie (regresja)
5. Potwierdź że strona główna nadal pokazuje swój oryginalny `<title>` (regresja)
</how-to-verify>
<resume-signal>Wpisz "approved" by kontynuować, lub opisz issues</resume-signal>
</task>
</tasks>
<boundaries>
## DO NOT CHANGE
- `index.php:242-292` — logika og:title/og:description/og:image dla produktu (działa poprawnie, nie ruszać)
- `pp_routes` regex dla permutacji (`[0-9_-]+`) — Phase 18 fix, nie regresować
- `Helpers::clear_product_cache()` — sygnatura stała
- Mechanizm `[META_INDEX]` / `[CANONICAL]` / `[CSS]` / `[JAVA_SCRIPT]` — niezwiązane
## SCOPE LIMITS
- Plan dotyczy TYLKO `<title>`, `<meta description>`, `<meta keywords>` dla stron kategorii i produktu
- NIE dodajemy og:* dla kategorii (deferred — osobny plan jeśli wyjdzie potrzeba)
- NIE rozszerzamy fallbacku meta_description o auto-generowany opis (deferred)
- NIE dotykamy CMS pages, articles, producers — chyba że wynik diagnostyki pokaże wspólny mechanizm
- Bez build/update package — to robi się w `/koniec-pracy` po UNIFY
</boundaries>
<verification>
Przed zamknięciem planu:
- [ ] DIAGNOSTICS.md istnieje i wskazuje konkretną przyczynę
- [ ] Fix zaimplementowany w wybranej lokalizacji (Task 2 decision)
- [ ] Nowy test jednostkowy przechodzi
- [ ] Cała suita PHPUnit zielona (841+ testów)
- [ ] Human-verify na 3 URL-ach z różnymi `<title>` zatwierdzony
- [ ] Brak regresji dla strony głównej i CMS pages
- [ ] Wszystkie acceptance criteria spełnione
</verification>
<success_criteria>
- Strona kategorii i strona produktu zwracają poprawne metatagi SEO
- Diagnostyka udokumentowana (DIAGNOSTICS.md) dla przyszłej referencji
- Test regresyjny pokrywa scenariusz
- Bez regresji w istniejących funkcjach (suita zielona)
</success_criteria>
<output>
After completion: `.paul/phases/19-frontend-meta-tags-fix/19-01-SUMMARY.md`
</output>

View File

@@ -0,0 +1,147 @@
---
phase: 19-frontend-meta-tags-fix
plan: 01
subsystem: frontend
tags: [seo, metatags, layout-engine, frontend, cache]
requires:
- phase: none
provides: n/a
provides:
- poprawne <title>/<meta description>/<meta keywords> dla stron kategorii/artykułu/produktu
- applyEntityMetaToPage() — testowalna metoda helper w \front\LayoutEngine
- regression test suite dla bug "homepage meta_title leak"
affects: [future-seo-fixes, layout-engine-refactor, og-tags-for-category]
tech-stack:
added: []
patterns:
- "Helper static method pattern w LayoutEngine — wyodrębnianie pure-function logic dla testowalności"
key-files:
created:
- tests/Unit/front/LayoutEngineMetaTagsTest.php
- .paul/phases/19-frontend-meta-tags-fix/DIAGNOSTICS.md
modified:
- autoload/front/LayoutEngine.php
key-decisions:
- "Root cause to bug w kodzie (LayoutEngine), nie dane w DB ani pp_routes"
- "Fix przez wyodrębnienie applyEntityMetaToPage() — zawsze nadpisuje meta_title/keywords/description encji (nawet pustym/null), żeby homepage nie wyciekał"
- "Literalne 'description'/'keywords' w pp_shop_categories_langs dla kategorii 10 — to dane klienta, nie bug shopPRO; admin uzupełnia w panelu"
patterns-established:
- "LayoutEngine bug: nadpisywanie pól w $page['language'] musi być KOMPLETNE — partial override powoduje wyciek wartości z poprzedniego stanu (homepage)"
- "Test jednostkowy LayoutEngine: require_once pliku w teście (bootstrap nie ładuje \\front\\)"
duration: ~45min
started: 2026-05-13T14:00:00Z
completed: 2026-05-13T14:45:00Z
---
# Phase 19 Plan 01: Frontend meta tags fix — Summary
**LayoutEngine::applyEntityMetaToPage() rozwiązuje wyciek meta_title homepage do stron kategorii/produktu/artykułu — 3 gałęzie + nowa metoda helper + 5 testów regresyjnych.**
## Performance
| Metric | Value |
|--------|-------|
| Duration | ~45min |
| Tasks | 4 z 4 wykonane |
| Files modified | 2 (1 zmiana, 1 nowy test) |
| Tests added | 5 (18 asercji) |
| Total suite | 846 zielone (z 841) |
## Acceptance Criteria Results
| Criterion | Status | Notes |
|-----------|--------|-------|
| AC-1: Diagnostyka — ustalona root cause | Pass | DIAGNOSTICS.md zawiera 4 zapytania SQL + analizę kodu + jednoznaczny wniosek |
| AC-2: Strona kategorii pokazuje własny title | Pass | Curl: `/sen-i-otulenie``<title>Sen i otulenie | shopPRO 1</title>` |
| AC-3: Strona produktu pokazuje własny title | Pass | Curl: `/kocyk-niemowlaka-...``<title>Kocyk niemowlaka - Szczeniak z balonikiem - Fuksja | shopPRO 1</title>` |
| AC-4: Fallback dla pustego meta_title | Pass | Dla kategorii meta_title=NULL → title=`category.title`; pokryte testem `testHomepageMetaTitleIsClearedWhenEntityHasNoMetaTitle` |
| AC-5: Test + cała suita zielona | Pass | LayoutEngineMetaTagsTest (5/18) + 846/846 ogółem |
## Accomplishments
- Zidentyfikowano root cause przez diagnostyczne SQL na produkcyjnej DB (3 hipotezy zweryfikowane, 1 potwierdzona) — bug w `LayoutEngine` linie 156-213, nie pp_routes, nie session, nie tylko dane
- Wyodrębniono `applyEntityMetaToPage()` — testowalna pure-function w `\front\LayoutEngine`, używana w 3 gałęziach (category/article/product)
- Fix zweryfikowany curl-em na produkcji (auto-upload FTP po edycji): 3 różne `<title>` na 3 stronach, homepage nie wycieka
- Boczny issue zidentyfikowany i zostawiony klientowi: literalne 'description'/'keywords' w `pp_shop_categories_langs.id=331` — admin uzupełnia w panelu
## Files Created/Modified
| File | Change | Purpose |
|------|--------|---------|
| `autoload/front/LayoutEngine.php` | Modified | 3 gałęzie (category line 152, article 174, product 194) zastąpione wywołaniem `applyEntityMetaToPage()`; nowa metoda po `title()` (linie ~430-455) |
| `tests/Unit/front/LayoutEngineMetaTagsTest.php` | Created | 5 testów: meta_title encji wygrywa, NULL czyści homepage, all-null product, null entity safe, empty page struct |
| `.paul/phases/19-frontend-meta-tags-fix/DIAGNOSTICS.md` | Created | Wyniki 4 zapytań SQL + analiza kodu + wniosek root cause |
| `.gitignore` | Modified | Dodano `temp/` (skrypty diagnostyczne z DB password) |
## Decisions Made
| Decision | Rationale | Impact |
|----------|-----------|--------|
| Fix przez extract helper (`applyEntityMetaToPage`) zamiast inline edycji 3 gałęzi | Testowalność (statyczna pure-function), DRY (jedno miejsce z logiką override meta) | Future SEO fixes działają w jednym miejscu |
| Zawsze nadpisuj meta_title (nawet NULL), nie tylko gdy wypełnione | Eliminuje wyciek z homepage; semantyka „encja w pełni opisuje swoje meta" | Linia 332 LayoutEngine działa zgodnie z intencją oryginalnego kodu |
| Dane klienta (literalne 'description'/'keywords') NIE są częścią fixu | To dane, nie kod; admin uzupełnia przez panel; klient inaczej skomplikowane | Phase 19 zamyka się czysto, nie wlecze tematu DB |
## Deviations from Plan
### Summary
| Type | Count | Impact |
|------|-------|--------|
| Auto-fixed | 1 | `.gitignore` rozszerzony o `temp/` (DB password w skryptach diag) |
| Scope additions | 0 | — |
| Deferred | 1 | Git commit transition-phase (pattern z faz 15-18) |
**Total impact:** Plan wykonany bez odchyleń scope. Jeden security micro-fix (.gitignore).
### Auto-fixed Issues
**1. [Security] DB credentials w temp/diag*.php**
- **Found during:** Task 1 (diagnostyka)
- **Issue:** Skrypty diagnostyczne `temp/diag_meta*.php` zawierają hardcoded credentials produkcyjnej DB
- **Fix:** Dodano `temp/` do `.gitignore` (CLAUDE.md i tak nakazuje skrypty pomocnicze w temp/)
- **Files:** `.gitignore`
- **Verification:** `git status` nie pokazuje temp/* jako trackowane
### Deferred Items
- Transition-phase git commit dla Phase 19 — kontynuacja patternu z faz 15/16/17/18 (commit robi `/koniec-pracy` lub user manualnie). Brak negatywnego impactu — kod działa na produkcji już teraz (auto-upload FTP).
## Issues Encountered
| Issue | Resolution |
|-------|------------|
| Pierwsza wersja diag SQL używała `pp_shop_categories_languages` (nie istnieje) | Sprawdzono `SHOW TABLES` — tabele to `*_langs` (skrót). Zaktualizowano zapytania |
| Pierwsza wersja używała `shop_category_id` (nie istnieje) | Kolumna to `category_id`. Sprawdzono `SHOW COLUMNS` |
| LayoutEngine niedostępny w bootstrap testów | `require_once` w teście (bootstrap PSR-4 nie ładuje `\front\` namespace) |
## Next Phase Readiness
**Ready:**
- Codebase z `applyEntityMetaToPage()` — gotowa do reuse w przyszłych fixach SEO (np. og:title dla kategorii)
- Test pattern dla `\front\LayoutEngine` ustalony (require_once + asercje na pure-function)
**Concerns:**
- LayoutEngine::show() nadal jest 400+ linijowym monolitem ze statycznymi globalami — dalsze ekstrakcje wskazane, ale poza scope tej fazy
- Klienci z istniejącymi instalacjami muszą uzupełnić meta_title/keywords/description przez panel admina (lub zaakceptować że podstrony mają brak meta — co teraz przynajmniej NIE jest niepoprawnym tytułem homepage)
**Blockers:**
- None.
## Skill Audit (Phase 19)
| Expected | Invoked | Notes |
|----------|---------|-------|
| /feature-dev | ○ | Hotfix z konkretną instrukcją — override per pattern Phase 15-18 |
| /koniec-pracy | ○ | Pending — przy zamknięciu sesji jeśli release wchodzi do update package |
---
*Phase: 19-frontend-meta-tags-fix, Plan: 01*
*Completed: 2026-05-13*

View File

@@ -0,0 +1,129 @@
# Phase 19 — DIAGNOSTICS
**Data:** 2026-05-13
**Środowisko:** shoppro.project-dc.pl (produkcja) — DB `host117523_shoppro` na `host117523.hostido.net.pl`
## Wynik diagnostyczny: ROOT CAUSE w kodzie (LayoutEngine.php)
`$page['language']['meta_title']` z domyślnej strony (homepage id=6) **nigdy nie jest nadpisywany** w gałęziach kategorii/artykułu/produktu w `LayoutEngine::show()`. Linia 332 priorytetuje `meta_title` nad `title`, więc tytuł homepage wycieka na wszystkie podstrony.
Dodatkowo: meta_description/meta_keywords dla podstron też nie zachowują się dobrze, bo w DB klient ma literalne zaślepki ("description", "keywords") albo NULL — ale to drugorzędny problem względem bugu w kodzie.
---
## 1) pp_routes — działa poprawnie
```
URL: sen-i-otulenie
MATCH pattern='^sen-i-otulenie$' dest='index.php?category=10&lang=pl'
URL: kocyk-niemowlaka-szczeniak-z-balonikiem-fuksja
MATCH pattern='^kocyk-niemowlaka-szczeniak-z-balonikiem-fuksja$' dest='index.php?product=522'
```
Hipoteza "pp_routes nie ustawia category=/product=" — **OBALONA**.
## 2) Meta kategorii 10 (sen-i-otulenie), lang=pl
```
id = 331
category_id = 10
lang_id = 'pl'
title = 'Sen i otulenie'
meta_title = NULL
meta_description = 'description' ← LITERALNA ZAŚLEPKA w DB
meta_keywords = 'keywords' ← LITERALNA ZAŚLEPKA w DB
seo_link = 'sen-i-otulenie'
category_title = 'Sen i otulenie'
```
## 3) Meta produktu 522 (kocyk-niemowlaka...), lang=pl
```
id = 4040
product_id = 522
lang_id = 'pl'
name = 'Kocyk niemowlaka - Szczeniak z balonikiem - Fuksja'
meta_title = NULL
meta_description = NULL
meta_keywords = NULL
seo_link = 'kocyk-niemowlaka-szczeniak-z-balonikiem-fuksja'
```
## 4) Default page (homepage) — id=6, start=1, lang=pl
```
id = 6
start = 1
title = 'Home'
meta_title = 'Sklep z akcesoriami dla dzieci i niemowląt, kocyki minky, poduszki, ubranka'
meta_keywords = '' (puste)
meta_description = 'Marianek to sklep internetowy, w którym znajdziecie Państwo artykuły dla dzieci i niemowląt...'
```
To jest źródło "wyciekającego" tytułu na podstronach.
---
## Analiza kodu (autoload/front/LayoutEngine.php)
### Gałąź kategorii (linie 152-168)
```php
if ( \Shared\Helpers\Helpers::get( 'category' ) ) {
$category = $categoryRepo->frontCategoryDetails(...);
if ( $category['language']['meta_title'] )
$page['language']['title'] = $category['language']['meta_title']; // ← przypisuje do TITLE, nie meta_title
else
$page['language']['title'] = $category['language']['title'];
$page['show_title'] = true;
$page['language']['meta_keywords'] = $category['language']['meta_keywords']; // OK
$page['language']['meta_description'] = $category['language']['meta_description']; // OK
// BRAK: $page['language']['meta_title'] = $category['language']['meta_title'];
}
```
### Gałąź produktu (linie 194-213) — identyczny bug
### Gałąź artykułu (linie 174-189) — identyczny bug
### Substytucja [TITLE] (linia 332)
```php
$html = str_replace( '[TITLE]',
$page['language']['meta_title']
? $page['language']['meta_title'] . ' | ' . $settings['firm_name']
: $page['language']['title'] . ' | ' . $settings['firm_name'],
$html );
```
`meta_title` z homepage żyje dalej w `$page['language']` (bo nie został zresetowany w gałęzi kategorii/produktu) → wygrywa nad title kategorii/produktu.
---
## Wpływ na obserwowane zachowanie
| URL | Obserwowane `<title>` | Powód |
|-----|----------------------|-------|
| /sen-i-otulenie | "Sklep z akcesoriami... \| shopPRO 1" | meta_title homepage wycieka |
| /kocyk-niemowlaka-... | "Sklep z akcesoriami... \| shopPRO 1" | meta_title homepage wycieka |
| URL | Obserwowane `<meta description>` | Powód |
|-----|---------------------------------|-------|
| /sen-i-otulenie | "description" | meta_description kategorii (literalna zaślepka) — POPRAWNE nadpisanie, ale dane w DB są wadliwe |
| /kocyk-niemowlaka-... | "" (puste) | meta_description produktu = NULL — POPRAWNE nadpisanie |
| URL | Obserwowane `<meta keywords>` | Powód |
|-----|------------------------------|-------|
| /sen-i-otulenie | "keywords" | meta_keywords kategorii (literalna zaślepka) — POPRAWNE nadpisanie, ale dane w DB są wadliwe |
| /kocyk-niemowlaka-... | "" (puste) | meta_keywords produktu = NULL — POPRAWNE nadpisanie |
---
## Wniosek
**Bug w kodzie**: `LayoutEngine::show()` w 3 gałęziach (category/article/product) zapisuje meta_title kategorii do `title`, ale nie nadpisuje `$page['language']['meta_title']`. Wartość z homepage zostaje i wygrywa.
**Dane klienta**: oddzielny issue — meta_keywords/meta_description dla kategorii to literalne zaślepki "keywords"/"description", produkt ma NULL. To NIE jest bug shopPRO — admin musi wypełnić panel.
**Rekomendacja fixu**: opcja `fix-engine-detection` z planu — naprawić gałęzie kategorii/artykułu/produktu, by zawsze nadpisywały `meta_title` (nawet pustym/NULL), oraz uprościć logikę title żeby była symetryczna.

View File

@@ -0,0 +1,197 @@
---
phase: 20-category-edit-mojibake-fix
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- admin/templates/shop-category/category-edit.php
autonomous: false
delegation: off
---
<objective>
## Goal
Etykiety formularza edycji kategorii (`/admin/shop_category/category_edit/id=X`) pokazują poprawne polskie znaki: "Treść", "Język domyślny", "Tytuł kategorii (h1)", "Wyświetlić podkategorie", "Sortowanie produktów", "Blokuj indeksację", "Opis kategorii (rozwinięcie)" — zamiast mojibake `TreĹć`, `JÄ™zyk domyĹlny`, itp.
## Purpose
Mojibake w etykietach UI panelu admina utrudnia korzystanie z formularza. Administrator musi się domyślać znaczenia pól. Naprawia profesjonalny wygląd panelu i poprawia UX uzupełniania kategorii (m.in. SEO).
## Output
- `admin/templates/shop-category/category-edit.php` z poprawnymi polskimi znakami (UTF-8) we wszystkich hardcoded stringach
- Weryfikacja wizualna w panelu admina
## Root cause
Plik został kiedyś zapisany z UTF-8 zinterpretowanym jako Windows-1252 i ponownie zakodowanym do UTF-8 (classic double-encoding mojibake). `file` raportuje "UTF-8 text" bo bajty są poprawnym UTF-8 — tyle że ich znaczenie po dekodowaniu UTF-8 to bezsensowne kombinacje znaków łacińskich (Ä, Ĺ, Ă itp.).
Grep `Ä[\x{84}-\x{99}]|Ĺ|Ăł` wskazał, że **tylko ten jeden plik** w całym repo (`admin/templates/`, `templates/`, `autoload/`) jest dotknięty — pozostałe templates mają poprawne UTF-8.
</objective>
<context>
## Project Context
@.paul/PROJECT.md
@.paul/STATE.md
## Source Files
@admin/templates/shop-category/category-edit.php
## Clarifications
- **Objaw** — Mojibake (`Ä€„` zamiast `ą`) w labelkach formularza edycji kategorii
→ Odpowiedź: potwierdzone — mojibake (double-encoded UTF-8)
- **Pola** — Nie wiem, sprawdź
→ Odpowiedź (po inspekcji): WSZYSTKIE hardcoded labelki w pliku `category-edit.php` (9 wystąpień), żadne inne pliki templates nie dotknięte
- **Migracja danych** — chodzi o labelki, nie zapisywane dane
→ Odpowiedź: plan dotyczy tylko hardcoded stringów w pliku PHP. Dane w bazie (mieszane HTML entities w additional_text) — oddzielny issue, NIE w tym planie
## Wykryte miejsca mojibake (grep)
```
Line 10: TreĹć → Treść
Line 20: JÄ™zyk domyĹlny → Język domyślny
Line 48: Opis kategorii (rozwiniÄ™cie) → Opis kategorii (rozwinięcie)
Line 87: Sortowanie produktĂłw → Sortowanie produktów
Line 96: WyĹwietlić podkategorie → Wyświetlić podkategorie
Line 107: JÄ™zyk domyĹlny → Język domyślny (duplikat)
Line 127: TytuĹ kategorii (h1) → Tytuł kategorii (h1)
Line 159: Blokuj indeksacjÄ™ → Blokuj indeksację
```
Mapowanie sekwencji (do podmiany):
- `ć``ć`, `Ä…``ą`, `Ä™``ę`, `Ĺ‚``ł`, `Ĺ›``ś`, `Ĺ„``ń`, `Ĺş``ź`, `Ĺľ``ż`
- `Ć``Ć`, `Ä„``Ą`, `Ę``Ę`, `Ĺ``Ł`, `Ĺš``Ś`, `Ĺƒ``Ń`, `Ĺ¹``Ź`, `Ĺ˝``Ż`
- `Ăł``ó`, `Ă“``Ó`
</context>
<acceptance_criteria>
## AC-1: Brak mojibake w pliku
```gherkin
Given plik admin/templates/shop-category/category-edit.php po fixie
When uruchomię grep regex `Ä[\x{84}-\x{99}]|Ĺ|Ăł|Ĺ|Ĺ|Ä|Ä|Ä`
Then nie ma żadnego dopasowania (0 linii)
And plik dalej jest UTF-8 (bez BOM) sprawdzalne przez `file`
```
## AC-2: Labelki formularza renderują się z polskimi znakami
```gherkin
Given administrator otwiera /admin/shop_category/category_edit/id=10 po deployu
When sprawdza nazwy tabów i etykiet pól
Then widzi "Treść", "Ustawienia", "SEO" jako nazwy tabów
And widzi "Język domyślny", "Tytuł kategorii (h1)", "Sortowanie produktów",
"Wyświetlić podkategorie", "Blokuj indeksację", "Opis kategorii (rozwinięcie)"
jako etykiety pól
And NIE widzi `Ä`, `Ĺ`, `Ă` w żadnym widocznym miejscu na tej stronie
```
## AC-3: Brak regresji w innych szablonach
```gherkin
Given fix dotyczy tylko jednego pliku
When uruchomię grep mojibake w admin/templates/ + templates/ + autoload/
Then żaden inny plik nie zawiera mojibake (tak jak przed fixem)
And cała suita PHPUnit (846 testów) pozostaje zielona
```
</acceptance_criteria>
<tasks>
<task type="auto">
<name>Task 1: Podmień double-encoded UTF-8 na właściwe polskie znaki</name>
<files>admin/templates/shop-category/category-edit.php</files>
<action>
Wykonaj globalne podstawienia w pliku z mojibake → UTF-8 polskie znaki:
Podstawienia całych słów (najbezpieczniejsze — unikamy fałszywych alarmów z `Ä` w innym kontekście):
"TreĹć" → "Treść"
"JÄ™zyk domyĹlny" → "Język domyślny"
"rozwiniÄ™cie" → "rozwinięcie"
"Sortowanie produktĂłw" → "Sortowanie produktów"
"WyĹwietlić podkategorie" → "Wyświetlić podkategorie"
"TytuĹ kategorii (h1)" → "Tytuł kategorii (h1)"
"Blokuj indeksacjÄ™" → "Blokuj indeksację"
Po wszystkich podstawieniach zweryfikuj greppem że żadne mojibake już nie zostało.
Avoid: globalne sekwencyjne `str_replace` typu `ć``ć` bez kontekstu — mogłoby przypadkiem złamać poprawne fragmenty w przyszłości (na razie nie ma ryzyka, ale lepiej trzymać się konkretnych słów).
Avoid: zmiana encodingu pliku (musi zostać UTF-8 bez BOM, CRLF — jak teraz).
Avoid: jakichkolwiek zmian poza podmianami stringów (struktura HTML, logika, imports — bez zmian).
</action>
<verify>
grep -P "Ä[\x{84}-\x{99}]|Ĺ|Ăł|Ĺ›|Ĺ‚|Ä™|Ä…|ć|Ĺ„" admin/templates/shop-category/category-edit.php
→ 0 dopasowań
file admin/templates/shop-category/category-edit.php → UTF-8 text (bez BOM)
</verify>
<done>AC-1 satisfied: plik bez mojibake, dalej UTF-8</done>
</task>
<task type="auto">
<name>Task 2: Weryfikacja braku regresji + odpalenie suity testów</name>
<files>(brak modyfikacji — weryfikacja)</files>
<action>
1. Powtórz globalny grep mojibake w admin/templates/, templates/, autoload/ — powinno być 0 plików (poza tym fixowanym, który po Task 1 też ma 0)
2. Uruchom suitę PHPUnit: `php phpunit.phar`
3. Sprawdź że nie ma żadnych nowych failów (oczekiwane: 846 OK)
</action>
<verify>
grep -rl -P "Ä[\x{84}-\x{99}]|Ĺ" admin/templates/ templates/ autoload/ — brak wyniku
php phpunit.phar — exit code 0, 846 tests OK
</verify>
<done>AC-3 satisfied: brak regresji, suita zielona</done>
</task>
<task type="checkpoint:human-verify" gate="blocking">
<what-built>
Plik admin/templates/shop-category/category-edit.php z polskimi znakami w UTF-8
(auto-upload FTP wysyła zmianę na shoppro.project-dc.pl po zapisie pliku)
</what-built>
<how-to-verify>
1. Otwórz https://shoppro.project-dc.pl/admin/shop_category/category_edit/id=10 (zaloguj się jeśli trzeba)
2. Sprawdź nazwy 3 tabów: "Treść", "Ustawienia", "SEO" — powinny być po polsku, bez mojibake
3. W tabie "Treść" sprawdź:
- tooltip ikony gwiazdki: "Język domyślny"
- etykiety pól: "Nazwa kategorii", "Opis kategorii", "Opis kategorii (rozwinięcie)", "Dodatkowy tekst (nad produktami)"
4. W tabie "Ustawienia" sprawdź: "Aktywna", "Sortowanie produktów", "Wyświetlić podkategorie"
5. W tabie "SEO" sprawdź: "Tytuł kategorii (h1)", "Blokuj indeksację"
6. Otwórz inną kategorię (np. id=11) — to samo dla regresji
</how-to-verify>
<resume-signal>Wpisz "approved" by kontynuować, lub opisz issues</resume-signal>
</task>
</tasks>
<boundaries>
## DO NOT CHANGE
- Struktura HTML pliku (tagi, klasy CSS, JS scripts)
- Logika PHP (if/foreach, wywołania Tpl)
- Inne pliki w admin/templates/shop-category/ — nie są dotknięte mojibake
- Encoding pliku (musi zostać UTF-8 bez BOM, jak obecnie)
- Dane w bazie (HTML entities w `additional_text` to oddzielny issue)
## SCOPE LIMITS
- Plan dotyczy TYLKO pliku `admin/templates/shop-category/category-edit.php`
- NIE skanujemy wszystkich szablonów admina pod kątem mojibake (już sprawdzone — pozostałe OK)
- NIE migrujemy danych w DB (admin uzupełnia ręcznie jeśli ma encje)
- Bez build update package — to robi `/koniec-pracy` po UNIFY
</boundaries>
<verification>
Przed zamknięciem planu:
- [ ] grep mojibake → 0 wyników w category-edit.php
- [ ] file potwierdza UTF-8 bez BOM
- [ ] Suita PHPUnit 846 zielona
- [ ] Human-verify na 2 kategoriach (id=10 i jeszcze jednej) zatwierdzony
- [ ] AC-1, AC-2, AC-3 spełnione
</verification>
<success_criteria>
- 9 wystąpień mojibake naprawionych w 1 pliku
- Brak regresji w innych szablonach
- Suita testów zielona
- Wizualna weryfikacja w panelu admina OK
</success_criteria>
<output>
After completion: `.paul/phases/20-category-edit-mojibake-fix/20-01-SUMMARY.md`
</output>

View File

@@ -0,0 +1,121 @@
---
phase: 20-category-edit-mojibake-fix
plan: 01
subsystem: admin-ui
tags: [encoding, utf8, mojibake, admin-templates, polish-chars]
requires:
- phase: none
provides: n/a
provides:
- poprawne polskie znaki w labelkach formularza edycji kategorii
- pewność że pozostałe pliki admin/templates/, templates/, autoload/ są wolne od double-encoded UTF-8 mojibake
affects: []
tech-stack:
added: []
patterns:
- "Detekcja double-encoded UTF-8: grep `Ä[\\x{84}-\\x{99}]|Ĺ|Ăł|Ĺ›|Ĺ‚|Ä™|Ä…|ć|Ĺ„` znajduje pliki z mojibake"
key-files:
modified:
- admin/templates/shop-category/category-edit.php
key-decisions:
- "Fix tylko 1 plik (9 wystąpień) — żaden inny szablon w repo nie ma mojibake"
- "Podmiana całych słów zamiast generyczna `ć``ć` — unika fałszywych alarmów na przyszłość"
- "Brak migracji danych w DB (te są oddzielnym tematem — HTML entities w additional_text)"
patterns-established:
- "Workflow detekcji mojibake: jeden grep regex pokrywa najczęstsze sekwencje Win-1252→UTF-8 double-encoding"
duration: ~10min
started: 2026-05-13T16:30:00Z
completed: 2026-05-13T16:40:00Z
---
# Phase 20 Plan 01: Category edit mojibake fix — Summary
**9 wystąpień double-encoded UTF-8 w `admin/templates/shop-category/category-edit.php` naprawione na poprawne polskie znaki; pozostałe szablony zweryfikowane jako wolne od mojibake.**
## Performance
| Metric | Value |
|--------|-------|
| Duration | ~10min |
| Tasks | 3 z 3 wykonane |
| Files modified | 1 |
| Mojibake fixed | 9 wystąpień (7 unikalnych fraz) |
| Total suite | 846 zielone (bez zmian) |
## Acceptance Criteria Results
| Criterion | Status | Notes |
|-----------|--------|-------|
| AC-1: Brak mojibake w pliku | Pass | grep mojibake → 0 wyników; file → UTF-8 bez BOM |
| AC-2: Labelki renderują się z polskimi znakami | Pass | Human-verify approved przez użytkownika |
| AC-3: Brak regresji w innych szablonach | Pass | grep w admin/templates/+templates/+autoload/ → 0 plików; suita 846 OK |
## Accomplishments
- Wykryto i naprawiono mojibake w jedynym dotkniętym pliku repo
- Etykiety formularza edycji kategorii w panelu admina są czytelne po polsku ("Treść", "Język domyślny", "Sortowanie produktów", "Tytuł kategorii (h1)", "Blokuj indeksację", "Wyświetlić podkategorie", "Opis kategorii (rozwinięcie)")
- Auto-upload FTP wysłał plik na produkcję; weryfikacja w przeglądarce potwierdzona
## Files Created/Modified
| File | Change | Purpose |
|------|--------|---------|
| `admin/templates/shop-category/category-edit.php` | Modified | Podmiana 9 wystąpień double-encoded UTF-8 na poprawne polskie znaki (7 unikalnych fraz: TreĹć, JÄ™zyk domyĹlny x2, rozwiniÄ™cie, Sortowanie produktĂłw, WyĹwietlić podkategorie, TytuĹ kategorii, Blokuj indeksacjÄ™) |
## Decisions Made
| Decision | Rationale | Impact |
|----------|-----------|--------|
| Podmiana całych słów zamiast `ć``ć` itd. | Bezpieczniej — nie zostawia ryzyka uszkodzenia poprawnych fragmentów przy hipotetycznym przyszłym pojawieniu się tych bajtów w innym kontekście | Edits są precyzyjne, łatwe do code-review |
| Fix tylko ten jeden plik | grep pokazał że to jedyny plik z mojibake w całym repo | Scope minimalny, brak ryzyka regresji |
## Deviations from Plan
### Summary
| Type | Count | Impact |
|------|-------|--------|
| Auto-fixed | 0 | — |
| Scope additions | 0 | — |
| Deferred | 1 | Git commit transition-phase (pattern z faz 15-19) |
**Total impact:** Plan wykonany dokładnie według planu.
### Deferred Items
- Transition-phase git commit dla Phase 20 — pattern z faz 15-19 (commit zrobi `/koniec-pracy` lub user manualnie). Brak negatywnego impactu — auto-upload FTP już ma plik na produkcji.
## Issues Encountered
None.
## Next Phase Readiness
**Ready:**
- Pattern detekcji mojibake (regex) udokumentowany — przydatny przy przyszłych audytach encoding
- Plik category-edit.php czysty UTF-8
**Concerns:**
- Inne pola w bazie (`additional_text` kategorii) mają mieszane HTML entities z polskimi znakami — oddzielny issue, nie blokuje
- Brak automatycznego sprawdzania encoding w CI — przyszły fix mógłby wprowadzić nowy mojibake bez wykrycia
**Blockers:** None.
## Skill Audit (Phase 20)
| Expected | Invoked | Notes |
|----------|---------|-------|
| /feature-dev | ○ | Triviany fix labelek — override per pattern poprzednich faz |
| /koniec-pracy | ○ | Pending — uruchomić przy release update package |
---
*Phase: 20-category-edit-mojibake-fix, Plan: 01*
*Completed: 2026-05-13*

File diff suppressed because one or more lines are too long

View File

@@ -2,5 +2,5 @@ projectKey=shopPRO
serverUrl=https://sonar.project-pro.pl
serverVersion=26.3.0.120487
dashboardUrl=https://sonar.project-pro.pl/dashboard?id=shopPRO
ceTaskId=3051d3c7-50d0-4263-bdc3-12917447a707
ceTaskUrl=https://sonar.project-pro.pl/api/ce/task?id=3051d3c7-50d0-4263-bdc3-12917447a707
ceTaskId=33e10a49-4790-400e-bf25-9b40eaed053e
ceTaskUrl=https://sonar.project-pro.pl/api/ce/task?id=33e10a49-4790-400e-bf25-9b40eaed053e

View File

@@ -7,7 +7,7 @@ ob_start();
?>
<div id="settings-tabs">
<ul class="resp-tabs-list settings-tabs">
<li><i class="fa fa-file"></i>Treść</li>
<li><i class="fa fa-file"></i>Treść</li>
<li><i class="fa fa-wrench"></i>Ustawienia</li>
<li><i class="fa fa-globe"></i>SEO</li>
</ul>
@@ -17,7 +17,7 @@ ob_start();
<ul class="resp-tabs-list languages-main htabs">
<? if ( is_array( $this -> languages ) ): foreach ( $this -> languages as $lg ):?>
<? if ( $lg['status'] ):?>
<li><? if ( $lg['id'] == $this->dlang ) echo '<i class="fa fa-star fa-lg text-system" title="Język domyślny"></i> ';?><?= $lg['name'];?></a></li>
<li><? if ( $lg['id'] == $this->dlang ) echo '<i class="fa fa-star fa-lg text-system" title="Język domyślny"></i> ';?><?= $lg['name'];?></a></li>
<? endif;?>
<? endforeach; endif;?>
</ul>
@@ -45,7 +45,7 @@ ob_start();
);?>
<?= \Shared\Html\Html::textarea(
array(
'label' => 'Opis kategorii (rozwinięcie)',
'label' => 'Opis kategorii (rozwinięcie)',
'name' => 'text_hidden[' . $lg['id'] . ']',
'id' => 'text_hidden_' . $lg['id'],
'value' => $this -> category['languages'][ $lg['id'] ]['text_hidden'],
@@ -84,7 +84,7 @@ ob_start();
);?>
<?= \Shared\Html\Html::select(
[
'label' => 'Sortowanie produktĂłw',
'label' => 'Sortowanie produktów',
'name' => 'sort_type',
'id' => 'sort_type',
'values' => is_array( $this -> sort_types ) ? $this -> sort_types : [],
@@ -93,7 +93,7 @@ ob_start();
);?>
<?= \Shared\Html\Html::input_switch(
array(
'label' => 'Wyświetlić podkategorie',
'label' => 'Wyświetlić podkategorie',
'name' => 'view_subcategories',
'checked' => $this -> category['view_subcategories'] == 1 ? true : false
)
@@ -104,7 +104,7 @@ ob_start();
<ul class="resp-tabs-list languages-seo htabs">
<? if ( is_array( $this -> languages ) ): foreach ( $this -> languages as $lg ):?>
<? if ( $lg['status'] ):?>
<li><? if ( $lg['id'] == $this->dlang ) echo '<i class="fa fa-star fa-lg text-system" title="Język domyślny"></i> ';?><?= $lg['name'];?></a></li>
<li><? if ( $lg['id'] == $this->dlang ) echo '<i class="fa fa-star fa-lg text-system" title="Język domyślny"></i> ';?><?= $lg['name'];?></a></li>
<? endif;?>
<? endforeach; endif;?>
</ul>
@@ -124,7 +124,7 @@ ob_start();
);?>
<?= \Shared\Html\Html::input(
array(
'label' => 'Tytuł kategorii (h1)',
'label' => 'Tytuł kategorii (h1)',
'name' => 'category_title[' . $lg['id'] . ']',
'id' => 'category_title_' . $lg['id'],
'value' => $this -> category['languages' ][ $lg['id'] ]['category_title']
@@ -156,7 +156,7 @@ ob_start();
);?>
<?= \Shared\Html\Html::select(
array(
'label' => 'Blokuj indeksacjÄ™',
'label' => 'Blokuj indeksację',
'name' => 'noindex[' . $lg['id'] . ']',
'id' => 'noindex_' . $lg['id'],
'values' => array(

View File

@@ -2369,9 +2369,9 @@ class ProductRepository
$itemNode->appendChild( $doc->createElement( 'g:description', html_entity_decode( strip_tags( $desc ) ) ) );
if ( $product['language']['seo_link'] ) {
$link = $domainPrefix . '://' . $url . '/' . \Shared\Helpers\Helpers::seo( $product['language']['seo_link'] ) . '/' . str_replace( '|', '/', $combination['permutation_hash'] );
$link = $domainPrefix . '://' . $url . '/' . \Shared\Helpers\Helpers::seo( $product['language']['seo_link'] ) . '/' . str_replace( '|', '_', $combination['permutation_hash'] );
} else {
$link = $domainPrefix . '://' . $url . '/p-' . $product['id'] . '-' . \Shared\Helpers\Helpers::seo( $product['language']['name'] ) . '/' . str_replace( '|', '/', $combination['permutation_hash'] );
$link = $domainPrefix . '://' . $url . '/p-' . $product['id'] . '-' . \Shared\Helpers\Helpers::seo( $product['language']['name'] ) . '/' . str_replace( '|', '_', $combination['permutation_hash'] );
}
$itemNode->appendChild( $doc->createElement( 'link', $link ) );

View File

@@ -691,12 +691,12 @@ class Helpers
if ( $row2['seo_link'] )
{
$mdb->insert( 'pp_routes', [ 'product_id' => $row2['product_id'], 'lang_id' => $row['id'], 'pattern' => '^' . $language_link . self::seo( $row2['seo_link'] ) . '$', 'destination' => 'index.php?product=' . $row2['product_id'] ] );
$mdb->insert( 'pp_routes', [ 'product_id' => $row2['product_id'], 'lang_id' => $row['id'], 'pattern' => '^' . $language_link . self::seo( $row2['seo_link'] ) . '/([0-9-]+)$', 'destination' => 'index.php?product=' . $row2['product_id'] . '&permutation_hash=$1' ] );
$mdb->insert( 'pp_routes', [ 'product_id' => $row2['product_id'], 'lang_id' => $row['id'], 'pattern' => '^' . $language_link . self::seo( $row2['seo_link'] ) . '/([0-9_-]+)$', 'destination' => 'index.php?product=' . $row2['product_id'] . '&permutation_hash=$1' ] );
}
else
{
$mdb->insert( 'pp_routes', [ 'product_id' => $row2['product_id'], 'lang_id' => $row['id'], 'pattern' => '^' . $language_link . 'p-' . $row2['product_id'] . '-' . self::seo( $row2['name'] ) . '$', 'destination' => 'index.php?product=' . $row2['product_id'] ] );
$mdb->insert( 'pp_routes', [ 'product_id' => $row2['product_id'], 'lang_id' => $row['id'], 'pattern' => '^' . $language_link . 'p-' . $row2['product_id'] . '-' . self::seo( $row2['name'] ) . '/([0-9-]+)$', 'destination' => 'index.php?product=' . $row2['product_id'] . '&permutation_hash=$1' ] );
$mdb->insert( 'pp_routes', [ 'product_id' => $row2['product_id'], 'lang_id' => $row['id'], 'pattern' => '^' . $language_link . 'p-' . $row2['product_id'] . '-' . self::seo( $row2['name'] ) . '/([0-9_-]+)$', 'destination' => 'index.php?product=' . $row2['product_id'] . '&permutation_hash=$1' ] );
}
}
}

View File

@@ -280,20 +280,71 @@ class ShopBasketController
$client = \Shared\Helpers\Helpers::get_session( 'client' );
$orderSubmitToken = $this->createOrderSubmitToken();
$basket = \Shared\Helpers\Helpers::get_session( 'basket' );
$coupon = \Shared\Helpers\Helpers::get_session( 'coupon' );
$transport = ( new \Domain\Transport\TransportRepository( $GLOBALS['mdb'] ) )->findActiveByIdCached( \Shared\Helpers\Helpers::get_session( 'basket-transport-method-id' ) );
$productsSummary = (float)\Domain\Basket\BasketCalculator::summaryPrice( $basket, $coupon );
$freeDeliveryThreshold = isset( $settings['free_delivery'] ) ? (float)$settings['free_delivery'] : 0.0;
$transportCalc = $this->calculateTransportCostForSummary( $transport, $productsSummary, $freeDeliveryThreshold );
return \Shared\Tpl\Tpl::view( 'shop-basket/summary-view', [
'lang_id' => $lang_id,
'client' => \Shared\Helpers\Helpers::get_session( 'client' ),
'basket' => \Shared\Helpers\Helpers::get_session( 'basket' ),
'transport' => ( new \Domain\Transport\TransportRepository( $GLOBALS['mdb'] ) )->findActiveByIdCached( \Shared\Helpers\Helpers::get_session( 'basket-transport-method-id' ) ),
'basket' => $basket,
'transport' => $transport,
'transport_cost_effective' => $transportCalc['transport_cost_effective'],
'free_delivery_applies' => $transportCalc['free_delivery_applies'],
'payment_method' => $this->paymentMethodRepository->paymentMethodCached( (int)\Shared\Helpers\Helpers::get_session( 'basket-payment-method-id' ) ),
'addresses' => ( new \Domain\Client\ClientRepository( $GLOBALS['mdb'] ) )->clientAddresses( (int)$client['id'] ),
'settings' => $settings,
'coupon' => \Shared\Helpers\Helpers::get_session( 'coupon' ),
'coupon' => $coupon,
'basket_message' => \Shared\Helpers\Helpers::get_session( 'basket_message' ),
'order_submit_token' => $orderSubmitToken
] );
}
/**
* Wylicza efektywny koszt transportu dla widoku /koszyk-podsumowanie.
* Koszt spada do 0, gdy transport ma flage delivery_free=1 ORAZ wartosc koszyka
* (po kuponie) osiaga prog darmowej dostawy $freeDeliveryThreshold.
*
* @param array|null $transport Aktywny transport (lub null gdy nie wybrany)
* @param float $productsSummary Wartosc koszyka po kuponie
* @param float $freeDeliveryThreshold Prog darmowej dostawy z settings.free_delivery
* @return array{transport_cost_effective: float, free_delivery_applies: bool}
*/
protected function calculateTransportCostForSummary( $transport, $productsSummary, $freeDeliveryThreshold )
{
if ( !is_array( $transport ) )
{
return [
'transport_cost_effective' => 0.0,
'free_delivery_applies' => false,
];
}
$deliveryFree = isset( $transport['delivery_free'] ) ? (int)$transport['delivery_free'] : 0;
$cost = isset( $transport['cost'] ) ? (float)$transport['cost'] : 0.0;
$applies = false;
if ( $deliveryFree === 1 && $freeDeliveryThreshold > 0 )
{
$summaryNormalized = \Shared\Helpers\Helpers::normalize_decimal( $productsSummary );
$thresholdNormalized = \Shared\Helpers\Helpers::normalize_decimal( $freeDeliveryThreshold );
if ( $summaryNormalized >= $thresholdNormalized )
{
$applies = true;
}
}
return [
'transport_cost_effective' => $applies ? 0.0 : $cost,
'free_delivery_applies' => $applies,
];
}
public function basketSave()
{
$orderSubmitToken = (string)\Shared\Helpers\Helpers::get( 'order_submit_token', true );

View File

@@ -153,15 +153,9 @@ class LayoutEngine
{
$category = $categoryRepo->frontCategoryDetails( (int)\Shared\Helpers\Helpers::get( 'category' ), $lang_id );
if ( $category['language']['meta_title'] )
$page['language']['title'] = $category['language']['meta_title'];
else
$page['language']['title'] = $category['language']['title'];
$page = self::applyEntityMetaToPage( $page, isset( $category['language'] ) ? $category['language'] : null, isset( $category['language']['title'] ) ? $category['language']['title'] : '' );
$page['show_title'] = true;
$page['language']['meta_keywords'] = $category['language']['meta_keywords'];
$page['language']['meta_description'] = $category['language']['meta_description'];
$page['language']['page_title'] = $category['language']['category_title'] ? $category['language']['category_title'] : $category['language']['title'];
$page['language']['page_title'] = !empty( $category['language']['category_title'] ) ? $category['language']['category_title'] : ( isset( $category['language']['title'] ) ? $category['language']['title'] : '' );
// CANONICAL
$html = str_replace( '[CANONICAL]', '', $html );
@@ -175,14 +169,8 @@ class LayoutEngine
{
$article = $articleRepo->articleDetailsFrontend( (int)\Shared\Helpers\Helpers::get( 'article' ), $lang_id );
if ( $article['language']['meta_title'] )
$page['language']['title'] = $article['language']['meta_title'];
else
$page['language']['title'] = $article['language']['title'];
$page = self::applyEntityMetaToPage( $page, isset( $article['language'] ) ? $article['language'] : null, isset( $article['language']['title'] ) ? $article['language']['title'] : '' );
$page['show_title'] = false;
$page['language']['meta_keywords'] = $article['language']['meta_keywords'];
$page['language']['meta_description'] = $article['language']['meta_description'];
// CANONICAL
$html = str_replace( '[CANONICAL]', '', $html );
@@ -193,16 +181,11 @@ class LayoutEngine
//
if ( \Shared\Helpers\Helpers::get( 'product' ) )
{
$product = ( new \Domain\Product\ProductRepository( $GLOBALS['mdb'] ) )->findCached( \Shared\Helpers\Helpers::get( 'product' ), $lang_id, $_GET['permutation_hash'] ?? null );
if ( $product['language']['meta_title'] )
$page['language']['title'] = $product['language']['meta_title'];
else
$page['language']['title'] = $product['language']['name'];
$permutation_hash = isset( $_GET['permutation_hash'] ) ? str_replace( '_', '|', $_GET['permutation_hash'] ) : null;
$product = ( new \Domain\Product\ProductRepository( $GLOBALS['mdb'] ) )->findCached( \Shared\Helpers\Helpers::get( 'product' ), $lang_id, $permutation_hash );
$page = self::applyEntityMetaToPage( $page, isset( $product['language'] ) ? $product['language'] : null, isset( $product['language']['name'] ) ? $product['language']['name'] : '' );
$page['show_title'] = false;
$page['language']['meta_keywords'] = $product['language']['meta_keywords'];
$page['language']['meta_description'] = $product['language']['meta_description'];
// CANONICAL
if ( $product['language']['canonical'] )
@@ -439,6 +422,35 @@ class LayoutEngine
] );
}
/**
* Przepisuje meta encji (kategoria/artykuł/produkt) do $page['language'].
*
* Dlaczego: domyślne $page jest stroną główną CMS. Jeśli nie nadpiszemy
* meta_title encji (nawet pustym), meta_title homepage wycieka do <title>
* na podstronie kategorii/produktu (linia podstawienia [TITLE]).
*
* @param array $page obecne $page (z homepage lub session)
* @param array|null $entityLanguage wiersz *_langs encji (może być null)
* @param string $fallbackTitle nazwa encji używana jako $page.language.title
* @return array zmodyfikowany $page
*/
public static function applyEntityMetaToPage( $page, $entityLanguage, $fallbackTitle )
{
if ( !is_array( $page ) ) {
$page = [];
}
if ( !isset( $page['language'] ) or !is_array( $page['language'] ) ) {
$page['language'] = [];
}
$page['language']['title'] = $fallbackTitle;
$page['language']['meta_title'] = is_array( $entityLanguage ) && isset( $entityLanguage['meta_title'] ) ? $entityLanguage['meta_title'] : null;
$page['language']['meta_keywords'] = is_array( $entityLanguage ) && isset( $entityLanguage['meta_keywords'] ) ? $entityLanguage['meta_keywords'] : null;
$page['language']['meta_description'] = is_array( $entityLanguage ) && isset( $entityLanguage['meta_description'] ) ? $entityLanguage['meta_description'] : null;
return $page;
}
public static function alert()
{
if ( $alert = \Shared\Helpers\Helpers::get_session( 'alert' ) )

View File

@@ -97,11 +97,11 @@
<div class="basket-summary">
<?= $this -> transport[ 'name_visible' ];?>:
<? if ( $this -> transport[ 'delivery_free' ] == 1 ):?>
<? if ( $this -> free_delivery_applies ):?>
<span>0,00 zł</span>
<? else:?>
<span>
<?= \Shared\Helpers\Helpers::decimal( $this -> transport[ 'cost' ] );?> zł
<?= \Shared\Helpers\Helpers::decimal( $this -> transport_cost_effective );?> zł
</span>
<? endif;?>
</div>
@@ -111,7 +111,7 @@
$summary -= $discount;
?>
<span id="order-summary">
<?= $this -> transport[ 'delivery_free' ] == 1 ? \Shared\Helpers\Helpers::decimal( $summary ) : \Shared\Helpers\Helpers::decimal( $summary + $this -> transport[ 'cost' ] );?> zł
<?= $this -> free_delivery_applies ? \Shared\Helpers\Helpers::decimal( $summary ) : \Shared\Helpers\Helpers::decimal( $summary + $this -> transport_cost_effective );?> zł
</span>
</div>
<div class="basket-summary">

View File

@@ -2,17 +2,37 @@
global $lang_id;
$attributeRepo = new \Domain\Attribute\AttributeRepository( $GLOBALS['mdb'] );
$attribute_details = $attributeRepo->frontAttributeDetails( (int)$this -> attribute['id'], $lang_id );
$forced_value_id = null;
if ( isset( $_GET['permutation_hash'] ) && $_GET['permutation_hash'] !== '' )
{
$pairs = explode( '|', str_replace( '_', '|', $_GET['permutation_hash'] ) );
foreach ( $pairs as $pair )
{
$parts = explode( '-', $pair );
if ( count( $parts ) == 2 && (int)$parts[0] === (int)$this -> attribute['id'] )
{
$forced_value_id = (int)$parts[1];
break;
}
}
}
if ( $attribute_details['type'] == 0 )
{
?>
<div class="attribute-container fradio-group attribute-<?= \Shared\Helpers\Helpers::seo( $attribute_details['language']['name'] );?>" level="<?= $this -> level;?>" order="<?= $this -> order;?>" attribute="<?= \Shared\Helpers\Helpers::seo( $attribute_details['language']['name'] );?>" attribute-name="<?= $attribute_details['language']['name'];?>">
<p class="attribute-label"><?= $attribute_details['language']['name'];?>:</p>
<? foreach ( $this -> attribute['values'] as $value ):?>
<? foreach ( $this -> attribute['values'] as $value ):
$is_active = $forced_value_id !== null
? ( (int)$value['id'] === $forced_value_id )
: (bool)$value['is_default'];
?>
<div class="fradio">
<input type="radio" id="<?= $this -> attribute['id'];?>-<?= $value['id'];?>" <? if ( $value['is_default'] ):?>checked="checked"<? endif;?> require="true" value="<?= $this -> attribute['id'];?>-<?= $value['id'];?>" name="<?= \Shared\Helpers\Helpers::seo( $attribute_details['language']['name'] );?>">
<input type="radio" id="<?= $this -> attribute['id'];?>-<?= $value['id'];?>" <? if ( $is_active ):?>checked="checked"<? endif;?> require="true" value="<?= $this -> attribute['id'];?>-<?= $value['id'];?>" name="<?= \Shared\Helpers\Helpers::seo( $attribute_details['language']['name'] );?>">
<label for="<?= $this -> attribute['id'];?>-<?= $value['id'];?>" order="<?= $this -> order;?>"><?= ( new \Domain\Attribute\AttributeRepository( $GLOBALS['mdb'] ) )->getAttributeValueById( $value['id'], $lang_id );?></label>
</div>
<? if ( $value['is_default'] ):?>
<? if ( $is_active ):?>
<script class="footer" type="text/javascript">
$( function()
{

View File

@@ -0,0 +1,151 @@
<?php
namespace Tests\Unit\Domain\Product;
use PHPUnit\Framework\TestCase;
use Domain\Product\ProductRepository;
/**
* Phase 18 — testy generatora linku do feedu Google.
*
* ProductRepository::appendCombinationToXml buduje <link> dla pozycji
* feedu Google. permutation_hash w bazie ma format "attr-val|attr-val".
* W URL feedu separator między parami to "_" (nie "/"), żeby URL był
* jednym segmentem dopasowywalnym przez routing pp_routes.
*
* Test wywołuje prywatną metodę przez ReflectionMethod z minimalnymi
* danymi produktu i sprawdza zawartość wynikowego DOMDocument.
*/
class ProductFeedLinkTest extends TestCase
{
private function buildRepoWithMocks(): ProductRepository
{
$mockDb = $this->createMock(\medoo::class);
$mockDb->method('select')->willReturn([]);
$mockDb->method('get')->willReturn(null);
$repo = new ProductRepository($mockDb);
// appendShippingToXml wywołuje $this->transportRepoForXml->lowestTransportPrice().
// Inicjalizacja w generateGoogleXmlFeed(); dla unit testu wstrzykujemy mock dynamicznie.
$transportMock = $this->getMockBuilder(\Domain\Transport\TransportRepository::class)
->disableOriginalConstructor()
->getMock();
$transportMock->method('lowestTransportPrice')->willReturn(0.0);
$repo->transportRepoForXml = $transportMock;
return $repo;
}
private function invokeAppendCombination(ProductRepository $repo, array $product, array $combination): string
{
$doc = new \DOMDocument('1.0', 'UTF-8');
$channelNode = $doc->appendChild($doc->createElement('channel'));
$method = new \ReflectionMethod(ProductRepository::class, 'appendCombinationToXml');
$method->setAccessible(true);
$method->invoke($repo, $doc, $channelNode, $product, $combination, 'https', 'shop.example.com');
return $doc->saveXML();
}
private function baseProduct(array $overrides = []): array
{
return array_merge([
'id' => 123,
'ean' => '5901234567890',
'language' => [
'name' => 'Produkt testowy',
'xml_name' => '',
'short_description' => 'Opis',
'meta_title' => '',
'seo_link' => 'sukienka-czerwona',
],
'price_brutto' => 100,
'price_brutto_promo' => 0,
'quantity' => 10,
'stock_0_buy' => 0,
'wp' => 1,
'images' => [],
], $overrides);
}
public function testCombinationLinkUsesUnderscoreInSeoLinkBranch()
{
$repo = $this->buildRepoWithMocks();
$product = $this->baseProduct();
$combination = [
'id' => 555,
'permutation_hash' => '20-170|21-175',
'price_brutto' => 120,
'price_brutto_promo' => 0,
'quantity' => 5,
'stock_0_buy' => 0,
];
$xml = $this->invokeAppendCombination($repo, $product, $combination);
$this->assertStringContainsString(
'<link>https://shop.example.com/sukienka-czerwona/20-170_21-175</link>',
$xml,
'Link feedu z seo_link musi używać "_" jako separatora par attr-val'
);
$this->assertStringNotContainsString(
'20-170/21-175',
$xml,
'Link feedu nie może zawierać starego separatora "/" między parami atrybutów'
);
}
public function testCombinationLinkUsesUnderscoreInFallbackBranch()
{
$repo = $this->buildRepoWithMocks();
$product = $this->baseProduct([
'language' => [
'name' => 'Sukienka czerwona',
'xml_name' => '',
'short_description' => 'Opis',
'meta_title' => '',
'seo_link' => '',
],
]);
$combination = [
'id' => 555,
'permutation_hash' => '20-170|21-175',
'price_brutto' => 120,
'price_brutto_promo' => 0,
'quantity' => 5,
'stock_0_buy' => 0,
];
$xml = $this->invokeAppendCombination($repo, $product, $combination);
// Fallback uses "p-{id}-{seo(name)}/...". Helpers::seo stub returns input unchanged.
$this->assertStringContainsString(
'<link>https://shop.example.com/p-123-Sukienka czerwona/20-170_21-175</link>',
$xml,
'Link fallback (bez seo_link) musi używać "_" jako separatora par attr-val'
);
}
public function testCombinationLinkWithSinglePair()
{
$repo = $this->buildRepoWithMocks();
$product = $this->baseProduct();
$combination = [
'id' => 555,
'permutation_hash' => '20-170',
'price_brutto' => 120,
'price_brutto_promo' => 0,
'quantity' => 5,
'stock_0_buy' => 0,
];
$xml = $this->invokeAppendCombination($repo, $product, $combination);
$this->assertStringContainsString(
'<link>https://shop.example.com/sukienka-czerwona/20-170</link>',
$xml,
'Pojedyncza para attr-val pozostaje bez zmian (str_replace nie ma co podmieniać)'
);
}
}

View File

@@ -0,0 +1,83 @@
<?php
namespace Tests\Unit\Shared\Helpers;
use PHPUnit\Framework\TestCase;
/**
* Phase 18 — testy regex routingu pp_routes dla URL produktów z permutacją.
*
* Helpers::htacces() generuje pattern dla każdego produktu z permutacją.
* Pattern używa klasy znakowej [0-9_-]+, żeby dopasować segment "20-170_21-175"
* w jednym kawałku (separator pomiędzy parami atrybutów to "_", nie "/").
*
* Testy nie wywołują htacces() (zbyt duże zależności), tylko weryfikują:
* 1. Wzorzec literałem [0-9_-]+ występuje w generatorze pp_routes (file content)
* 2. Wzorzec przyjmuje URL z "_" i odrzuca wariant ze "/"
*/
class HelpersRoutingTest extends TestCase
{
private $helpersSource;
protected function setUp(): void
{
parent::setUp();
$this->helpersSource = file_get_contents(
__DIR__ . '/../../../../autoload/Shared/Helpers/Helpers.php'
);
}
public function testHelpersGeneratorUsesPermutationCharClassWithUnderscore()
{
// Liczba miejsc, gdzie pattern produktu z permutacją używa nowej klasy znaków.
$newPattern = substr_count($this->helpersSource, '/([0-9_-]+)$');
$this->assertGreaterThanOrEqual(
2,
$newPattern,
'Helpers.php musi zawierać dwa wystąpienia /([0-9_-]+)$ (gałąź seo_link i fallback p-id-name)'
);
// Stary wzorzec [0-9-]+ nie powinien już występować jako finalny segment URL.
$this->assertStringNotContainsString(
'/([0-9-]+)$',
$this->helpersSource,
'Stary wzorzec /([0-9-]+)$ został zastąpiony przez /([0-9_-]+)$ — nie powinno go już być w generatorze pp_routes'
);
}
public function testRegexMatchesUrlWithUnderscoreSeparator()
{
$pattern = '#^slug-produktu/([0-9_-]+)$#';
$matches = [];
$this->assertSame(
1,
preg_match($pattern, 'slug-produktu/20-170_21-175', $matches),
'Nowy wzorzec musi dopasować URL z "_" jako separatorem par atrybutów'
);
$this->assertSame('20-170_21-175', $matches[1]);
}
public function testRegexRejectsLegacyUrlWithSlashSeparator()
{
$pattern = '#^slug-produktu/([0-9_-]+)$#';
$this->assertSame(
0,
preg_match($pattern, 'slug-produktu/20-170/21-175'),
'Wzorzec NIE powinien dopasować starego URL ze "/" — taki URL ma trafiać do innego routingu lub 404'
);
}
public function testRegexMatchesSinglePairUrl()
{
$pattern = '#^slug-produktu/([0-9_-]+)$#';
$matches = [];
$this->assertSame(
1,
preg_match($pattern, 'slug-produktu/20-170', $matches),
'Wzorzec dopasowuje też URL z jedną parą attr-val'
);
$this->assertSame('20-170', $matches[1]);
}
}

View File

@@ -0,0 +1,120 @@
<?php
namespace Tests\Unit\front\Controllers;
use PHPUnit\Framework\TestCase;
use front\Controllers\ShopBasketController;
use Domain\Order\OrderRepository;
use Domain\PaymentMethod\PaymentMethodRepository;
class ShopBasketControllerSummaryViewTest extends TestCase
{
private $controller;
protected function setUp(): void
{
$orderRepository = $this->createMock(OrderRepository::class);
$paymentMethodRepository = $this->createMock(PaymentMethodRepository::class);
$this->controller = new ShopBasketController($orderRepository, $paymentMethodRepository);
}
/**
* Wywoluje chroniona metode calculateTransportCostForSummary przez Reflection.
*
* @param array|null $transport
* @param float $productsSummary
* @param float $freeDeliveryThreshold
* @return array
*/
private function invokeCalc($transport, $productsSummary, $freeDeliveryThreshold): array
{
$reflection = new \ReflectionClass(ShopBasketController::class);
$method = $reflection->getMethod('calculateTransportCostForSummary');
$method->setAccessible(true);
return $method->invoke($this->controller, $transport, $productsSummary, $freeDeliveryThreshold);
}
public function testTransportWithDeliveryFreeBelowThresholdShowsRealCost(): void
{
// AC-1: delivery_free=1, basket 150, threshold 300 -> cost 15.00
$transport = [
'id' => 4,
'cost' => 15.00,
'delivery_free' => 1,
];
$result = $this->invokeCalc($transport, 150.00, 300.00);
$this->assertFalse($result['free_delivery_applies']);
$this->assertSame(15.00, $result['transport_cost_effective']);
}
public function testTransportWithDeliveryFreeAboveThresholdShowsZero(): void
{
// AC-2: delivery_free=1, basket 350, threshold 300 -> cost 0.0, applies true
$transport = [
'id' => 4,
'cost' => 15.00,
'delivery_free' => 1,
];
$result = $this->invokeCalc($transport, 350.00, 300.00);
$this->assertTrue($result['free_delivery_applies']);
$this->assertSame(0.0, $result['transport_cost_effective']);
}
public function testTransportWithDeliveryFreeAtExactThresholdShowsZero(): void
{
// Boundary: basket == threshold should trigger free delivery
$transport = [
'id' => 4,
'cost' => 20.00,
'delivery_free' => 1,
];
$result = $this->invokeCalc($transport, 300.00, 300.00);
$this->assertTrue($result['free_delivery_applies']);
$this->assertSame(0.0, $result['transport_cost_effective']);
}
public function testTransportWithoutDeliveryFreeAlwaysShowsCost(): void
{
// AC-3: delivery_free=0, basket 500, threshold 300 -> cost 25.00, applies false
$transport = [
'id' => 5,
'cost' => 25.00,
'delivery_free' => 0,
];
$result = $this->invokeCalc($transport, 500.00, 300.00);
$this->assertFalse($result['free_delivery_applies']);
$this->assertSame(25.00, $result['transport_cost_effective']);
}
public function testNullTransportReturnsZeroAndDoesNotApply(): void
{
// Scenario: no transport selected yet (findActiveByIdCached zwrocil null)
$result = $this->invokeCalc(null, 500.00, 300.00);
$this->assertFalse($result['free_delivery_applies']);
$this->assertSame(0.0, $result['transport_cost_effective']);
}
public function testZeroFreeDeliveryThresholdDisablesFreeDelivery(): void
{
// Ochrona: jesli settings.free_delivery = 0, darmowa dostawa nie dziala nigdy
$transport = [
'id' => 4,
'cost' => 15.00,
'delivery_free' => 1,
];
$result = $this->invokeCalc($transport, 9999.00, 0.00);
$this->assertFalse($result['free_delivery_applies']);
$this->assertSame(15.00, $result['transport_cost_effective']);
}
}

View File

@@ -0,0 +1,110 @@
<?php
namespace Tests\Unit\Front;
use PHPUnit\Framework\TestCase;
require_once __DIR__ . '/../../../autoload/front/LayoutEngine.php';
/**
* Phase 19 — fix metatagów na stronach kategorii/produktu/artykułu.
*
* Bug: domyślne $page jest stroną główną. W gałęziach category/article/product
* w LayoutEngine::show() nadpisywany był tylko title, a meta_title homepage
* żył dalej w $page['language']['meta_title'] i wygrywał w substytucji [TITLE].
*
* Fix: applyEntityMetaToPage() zawsze nadpisuje meta_title/meta_keywords/
* meta_description encji (nawet pustymi/null), żeby nic z homepage nie wyciekało.
*/
class LayoutEngineMetaTagsTest extends TestCase
{
public function testHomepageMetaTitleDoesNotLeakWhenEntityHasOwnMetaTitle()
{
$page = $this->homepagePage();
$category = [
'meta_title' => 'Sen i otulenie — kocyki minky',
'meta_keywords' => 'kocyki, otulacze',
'meta_description' => 'Najwyższej jakości kocyki minky',
'title' => 'Sen i otulenie',
];
$result = \front\LayoutEngine::applyEntityMetaToPage($page, $category, $category['title']);
$this->assertSame('Sen i otulenie — kocyki minky', $result['language']['meta_title']);
$this->assertSame('Sen i otulenie', $result['language']['title']);
$this->assertSame('kocyki, otulacze', $result['language']['meta_keywords']);
$this->assertSame('Najwyższej jakości kocyki minky', $result['language']['meta_description']);
}
public function testHomepageMetaTitleIsClearedWhenEntityHasNoMetaTitle()
{
$page = $this->homepagePage();
$category = [
'meta_title' => null,
'meta_keywords' => 'description',
'meta_description' => 'keywords',
'title' => 'Sen i otulenie',
];
$result = \front\LayoutEngine::applyEntityMetaToPage($page, $category, $category['title']);
$this->assertNull(
$result['language']['meta_title'],
'meta_title homepage nie może wyciekać gdy kategoria nie ma własnego'
);
$this->assertSame('Sen i otulenie', $result['language']['title']);
}
public function testProductWithAllMetaNullClearsHomepageValues()
{
$page = $this->homepagePage();
$productLang = [
'meta_title' => null,
'meta_keywords' => null,
'meta_description' => null,
'name' => 'Kocyk niemowlaka - Szczeniak z balonikiem - Fuksja',
];
$result = \front\LayoutEngine::applyEntityMetaToPage($page, $productLang, $productLang['name']);
$this->assertNull($result['language']['meta_title']);
$this->assertNull($result['language']['meta_keywords']);
$this->assertNull($result['language']['meta_description']);
$this->assertSame($productLang['name'], $result['language']['title']);
}
public function testNullEntityLanguageDoesNotCrashAndClearsMeta()
{
$page = $this->homepagePage();
$result = \front\LayoutEngine::applyEntityMetaToPage($page, null, 'Fallback');
$this->assertSame('Fallback', $result['language']['title']);
$this->assertNull($result['language']['meta_title']);
$this->assertNull($result['language']['meta_keywords']);
$this->assertNull($result['language']['meta_description']);
}
public function testEmptyPageInputCreatesLanguageStructure()
{
$result = \front\LayoutEngine::applyEntityMetaToPage([], ['meta_title' => 'X'], 'T');
$this->assertIsArray($result);
$this->assertIsArray($result['language']);
$this->assertSame('X', $result['language']['meta_title']);
$this->assertSame('T', $result['language']['title']);
}
private function homepagePage()
{
return [
'id' => 6,
'language' => [
'title' => 'Home',
'meta_title' => 'Sklep z akcesoriami dla dzieci i niemowląt, kocyki minky, poduszki, ubranka',
'meta_keywords' => '',
'meta_description' => 'Marianek to sklep internetowy z artykułami dla dzieci...',
'page_title' => null,
],
];
}
}

BIN
updates/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -0,0 +1,2 @@
ALTER TABLE pp_shop_payment_methods ADD COLUMN min_order_amount DECIMAL(10,2) DEFAULT NULL;
ALTER TABLE pp_shop_payment_methods ADD COLUMN max_order_amount DECIMAL(10,2) DEFAULT NULL;

View File

@@ -0,0 +1,5 @@
ALTER TABLE pp_log ADD COLUMN `action` VARCHAR(100) NULL DEFAULT NULL AFTER `id`;
ALTER TABLE pp_log ADD COLUMN `order_id` INT NULL DEFAULT NULL AFTER `action`;
ALTER TABLE pp_log ADD COLUMN `context` TEXT NULL DEFAULT NULL AFTER `message`;
ALTER TABLE pp_log ADD INDEX `idx_action` (`action`);
ALTER TABLE pp_log ADD INDEX `idx_order_id` (`order_id`);

View File

@@ -0,0 +1 @@
ALTER TABLE `pp_shop_products_custom_fields` ADD COLUMN `type` VARCHAR(30) NOT NULL DEFAULT '' AFTER `name`;

BIN
updates/0.30/ver_0.348.zip Normal file

Binary file not shown.

View File

@@ -0,0 +1,26 @@
{
"changelog": "Dodano przełącznik i edycję etykiet niestandardowych na liście produktów.",
"version": "0.348",
"files": {
"added": [
],
"deleted": [
],
"modified": [
"admin/templates/shop-product/products-list-custom-script.php",
"admin/templates/shop-product/products-list.php",
"autoload/Domain/Product/ProductRepository.php",
"autoload/admin/Controllers/ShopProductController.php"
]
},
"checksum_zip": "sha256:3dc1557d1cd17d07ce9e406cb919571d2c3f4e829d0496343cd1210e0b45da87",
"sql": [
],
"date": "2026-04-19",
"directories_deleted": [
]
}

BIN
updates/0.30/ver_0.349.zip Normal file

Binary file not shown.

View File

@@ -0,0 +1,24 @@
{
"changelog": "Naprawa kosztu transportu na podsumowaniu koszyka (delivery_free z uwzglednieniem progu)",
"version": "0.349",
"files": {
"added": [
],
"deleted": [
],
"modified": [
"autoload/front/Controllers/ShopBasketController.php",
"templates/shop-basket/summary-view.php"
]
},
"checksum_zip": "sha256:acb6a6f4d1c80c5bb4d39c84b5d6d9335069de42febb177d9f581b1b8006aefd",
"sql": [
],
"date": "2026-04-20",
"directories_deleted": [
]
}

BIN
updates/0.30/ver_0.350.zip Normal file

Binary file not shown.

View File

@@ -0,0 +1,26 @@
{
"changelog": "FIX - linki produktow z permutacja w feedzie Google (separator _)",
"version": "0.350",
"files": {
"added": [
],
"deleted": [
],
"modified": [
"autoload/Domain/Product/ProductRepository.php",
"autoload/Shared/Helpers/Helpers.php",
"autoload/front/LayoutEngine.php",
"templates/shop-product/_partial/product-attribute.php"
]
},
"checksum_zip": "sha256:0cf918bb070260a8e055a1c8f0f3407532008a5a920da588a14c1f3b11e02c59",
"sql": [
],
"date": "2026-04-30",
"directories_deleted": [
]
}

BIN
updates/0.30/ver_0.351.zip Normal file

Binary file not shown.

View File

@@ -0,0 +1,23 @@
{
"changelog": "FIX - poprawne metatagi \u003ctitle\u003e/\u003cdescription\u003e/\u003ckeywords\u003e dla stron kategorii i produktow (eliminacja wycieku meta_title homepage)",
"version": "0.351",
"files": {
"added": [
],
"deleted": [
],
"modified": [
"autoload/front/LayoutEngine.php"
]
},
"checksum_zip": "sha256:046d0504accf29a57fea155ecaa2acfe09078379e43118af7bf6c0f4831a3e5f",
"sql": [
],
"date": "2026-05-13",
"directories_deleted": [
]
}

View File

@@ -1,3 +1,15 @@
<b>ver. 0.351 - 13.05.2026</b><br />
FIX - poprawne metatagi <title>/<description>/<keywords> dla stron kategorii i produktow (eliminacja wycieku meta_title homepage)
<hr>
<b>ver. 0.350 - 30.04.2026</b><br />
FIX - linki produktow z permutacja w feedzie Google (separator _)
<hr>
<b>ver. 0.349 - 20.04.2026</b><br />
Naprawa kosztu transportu na podsumowaniu koszyka (delivery_free z uwzglednieniem progu)
<hr>
<b>ver. 0.348 - 19.04.2026</b><br />
Dodano przełącznik i edycję etykiet niestandardowych na liście produktów.
<hr>
<b>ver. 0.347 - 18.04.2026</b><br />
Naprawa zapisu edycji kontenerow statycznych + testy regresyjne
<hr>

View File

@@ -1,5 +1,5 @@
<?
$current_ver = 347;
$current_ver = 351;
for ($i = 1; $i <= $current_ver; $i++)
{