Compare commits

..

14 Commits
v0.346 ... main

Author SHA1 Message Date
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
Jacek
9577d4944a feat: custom labels toggle and inline editing in product list
Adds session-based show/hide toggle for custom labels in admin product list, inline editable fields for custom_label_0..4, and label suggestions with custom entry support. Includes repository/controller updates, UI fixes, tests, and PAUL docs release updates.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-19 11:09:19 +02:00
Jacek
41e491c6b7 update 2026-04-18 23:52:11 +02:00
Jacek
e195ffc841 build: ver_0.347 - naprawa zapisu scontainers 2026-04-18 23:25:45 +02:00
Jacek
5b66720f7c fix: scontainers edit saves existing record instead of creating new
Fixes static container admin edit flow by preserving id in hiddenFields and adding route-id fallback during save.
Adds regression tests for edit/create id behavior, updates release docs (changelog/testing/CLAUDE), and appends SonarQube open issues to docs/TODO.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-18 22:56:14 +02:00
Jacek
c611b012c6 update 2026-04-16 23:12:41 +02:00
Jacek
3fa3d72758 build: ver_0.346 - Fix usuwania wszystkich dodatkowych pól produktu
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 22:52:32 +02:00
76 changed files with 7357 additions and 5878 deletions

View File

@@ -1,6 +1,6 @@
# shopPRO Koniec Pracy (release workflow)
# shopPRO - Koniec Pracy (release workflow)
Execute the full release workflow for shopPRO. This is a sequential pipeline each step depends on the previous one succeeding. Stop and report if any step fails.
Execute the full release workflow for shopPRO. This is a sequential pipeline - each step depends on the previous one succeeding. Stop and report if any step fails.
## Step 1: Run tests
@@ -8,7 +8,7 @@ Run the full PHPUnit test suite:
```bash
php phpunit.phar
```
All tests must pass. If any test fails, stop here do not proceed to commit. Report the failures and wait for instructions.
All tests must pass. If any test fails, stop here - do not proceed to commit. Report the failures and wait for instructions.
## Step 1b: SonarQube scan
@@ -19,20 +19,20 @@ sonar-scanner
After the scan completes, query the SonarQube issues via MCP tool `mcp__sonarqube__issues` with `project_key: "shopPRO"` and `resolved: false`. Fetch all open issues (bugs, vulnerabilities, code smells).
Then open `docs/TODO.md` and append the found issues at the bottom under a new section:
Then open `.paul/docs/TODO.md` and append the found issues at the bottom under a new section:
```markdown
## SonarQube {VERSION} ({DATE})
## SonarQube - {VERSION} ({DATE})
- [ ] [SEVERITY] FILENAME:LINE description (rule)
- [ ] [SEVERITY] FILENAME:LINE - description (rule)
- [ ] ...
```
Rules:
- Only add issues that are NOT already present in `docs/TODO.md`
- Only add issues that are NOT already present in `.paul/docs/TODO.md`
- Group by type: first Bugs/Vulnerabilities, then Code Smells
- Skip INFO severity Code Smells only include MINOR and above
- If there are no new issues, write: `## SonarQube {VERSION} brak nowych issues`
- Skip INFO severity Code Smells - only include MINOR and above
- If there are no new issues, write: `## SonarQube - {VERSION} - brak nowych issues`
## Step 2: Determine version
@@ -40,24 +40,24 @@ Read the latest git tag to determine the current version number:
```bash
git tag --sort=-v:refname | head -1
```
The new version is the previous version incremented by 1 (e.g., v0.333 v0.334). Use this version number throughout the remaining steps.
The new version is the previous version incremented by 1 (e.g., v0.333 -> v0.334). Use this version number throughout the remaining steps.
## Step 3: Update documentation
Update these docs files **only if** changes in this session affect them:
Update these docs files **only if** changes in this session affect them.
Do not update files in root `docs/` directory.
| File | When to update |
|------|---------------|
| `docs/CHANGELOG.md` | Always add a new version entry at the top describing what changed |
| `docs/TESTING.md` | If tests were added/removed update test count and structure |
| `CLAUDE.md` | If test count changed — update the "Current suite" line |
| `docs/DATABASE_STRUCTURE.md` | If database schema changed |
| `docs/PROJECT_STRUCTURE.md` | If architecture/files changed significantly |
| `docs/FORM_EDIT_SYSTEM.md` | If form system was modified |
| `.paul/docs/CHANGELOG.md` | Always - add a new version entry at the top describing what changed |
| `.paul/docs/TESTING.md` | If tests were added/removed - update test count and structure |
| `.paul/docs/DB_SCHEMA.md` | If database schema changed |
| `.paul/docs/ARCHITECTURE.md` | If architecture/files changed significantly |
| `.paul/docs/FORMS.md` | If form system was modified |
## Step 4: SQL migrations
If database schema changes were made, create a migration file at `migrations/{version}.sql` (e.g., `migrations/0.334.sql`). Do NOT put SQL files in `updates/` the build script reads from `migrations/` automatically.
If database schema changes were made, create a migration file at `migrations/{version}.sql` (e.g., `migrations/0.334.sql`). Do NOT put SQL files in `updates/` - the build script reads from `migrations/` automatically.
If no DB changes were made, skip this step.

View File

@@ -1,12 +1,12 @@
# shopPRO
# shopPRO
## What This Is
Autorski silnik sklepu internetowego pisany od podstaw odpowiednik WooCommerce lub PrestaShop, ale bez zależności od zewnętrznych platform. Składa się z panelu administratora (zarządzanie zamówieniami, produktami, klientami) oraz części frontowej dla klienta końcowego.
Autorski silnik sklepu internetowego pisany od podstaw - odpowiednik WooCommerce lub PrestaShop, ale bez zależności od zewnętrznych platform. Składa się z panelu administratora (zarządzanie zamówieniami, produktami, klientami) oraz części frontowej dla klienta końcowego.
## Core Value
Właściciel sklepu internetowego ma pełną kontrolę nad sprzedażą online produktami, zamówieniami i klientami w jednym spójnym systemie pisanym od podstaw, bez narzutów zewnętrznych platform.
Właściciel sklepu internetowego ma pełną kontrolę nad sprzedażą online - produktami, zamówieniami i klientami - w jednym spójnym systemie pisanym od podstaw, bez narzutów zewnętrznych platform.
## Current State
@@ -14,22 +14,25 @@ Właściciel sklepu internetowego ma pełną kontrolę nad sprzedażą online
|-----------|-------|
| Version | 0.333 |
| Status | Production |
| Last Updated | 2026-03-12 |
| Last Updated | 2026-04-30 |
## Requirements
### Validated (Shipped)
- [x] Panel administratora zarządzanie produktami, kategoriami, atrybutami
- [x] Panel administratora zarządzanie zamówieniami
- [x] Panel administratora zarządzanie klientami
- [x] Część frontowa przeglądanie i kupowanie produktów
- [x] Panel administratora - zarządzanie produktami, kategoriami, atrybutami
- [x] Panel administratora - zarządzanie zamówieniami
- [x] Panel administratora - zarządzanie klientami
- [x] Część frontowa - przeglądanie i kupowanie produktów
- [x] Koszyk i składanie zamówień
- [x] Integracje płatności i dostaw
- [x] REST API (ordersPRO + Ekomi)
- [x] Redis caching
- [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)
@@ -41,7 +44,7 @@ Właściciel sklepu internetowego ma pełną kontrolę nad sprzedażą online
### Out of Scope
- Multitenancy (wiele sklepów w jednej instancji) nie planowane
- Multitenancy (wiele sklepów w jednej instancji) - nie planowane
## Target Users
@@ -65,7 +68,7 @@ Właściciel sklepu internetowego ma pełną kontrolę nad sprzedażą online
### Technical Constraints
- PHP < 8.0 na produkcji (brak `match`, named arguments, union types)
- Medoo ORM prepared statements bez wyjątków
- Medoo ORM - prepared statements bez wyjątków
- Redis wymagany dla cache
### Business Constraints
@@ -78,12 +81,16 @@ Właściciel sklepu internetowego ma pełną kontrolę nad sprzedażą online
| DDD + DI zamiast legacy architektury | Testowalność, separacja odpowiedzialności | 2025 | Active |
| PHP < 8.0 kompatybilność | Klienci na starszych serwerach | 2025 | Active |
| 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
@@ -102,14 +109,14 @@ Właściciel sklepu internetowego ma pełną kontrolę nad sprzedażą online
See: .paul/SPECIAL-FLOWS.md
Quick Reference:
- /feature-dev Nowe funkcje, większe zmiany (required)
- /koniec-pracy Release, update package (required)
- /frontend-design Komponenty UI, szablony widoków
- /code-review Przegląd kodu przed release
- /simplify Upraszczanie po implementacji
- /claude-md-improver Utrzymanie CLAUDE.md
- /zapisz + /wznow Zapis i wznowienie sesji
- /feature-dev -> Nowe funkcje, większe zmiany (required)
- /koniec-pracy -> Release, update package (required)
- /frontend-design -> Komponenty UI, szablony widoków
- /code-review -> Przegląd kodu przed release
- /simplify -> Upraszczanie po implementacji
- /claude-md-improver -> Utrzymanie CLAUDE.md
- /zapisz + /wznow -> Zapis i wznowienie sesji
---
*PROJECT.md Updated when requirements or context change*
*Last updated: 2026-03-12*
*PROJECT.md - Updated when requirements or context change*
*Last updated: 2026-04-30 after Phase 18*

View File

@@ -1,4 +1,4 @@
# Roadmap: shopPRO
# Roadmap: shopPRO
## Overview
@@ -6,9 +6,9 @@ shopPRO to autorski silnik sklepu internetowego rozwijany iteracyjnie. Projekt j
## Current Milestone
**Security hardening** (v0.33x)
Status: In progress
Phases: 3 of 4 complete
**Feature — Product list custom labels quick edit**
Status: Complete
Phases: 1 of 1 complete
## Phases
@@ -36,6 +36,9 @@ Status: Planning
| 7 | Coupon Fatal Error — order placement crash | 1 | Done | 2026-03-15 |
| 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 |
## Feature
@@ -46,6 +49,7 @@ Status: Planning
| 12 | summaryView redirect fix — double order block | 1 | Done | 2026-03-25 |
| 13 | Basket logging + TTL token fix | 1 | Done | 2026-03-25 |
| 14 | Custom fields delete bug — usunięcie wszystkich pól | 1 | Done | 2026-04-16 |
| 16 | Product list custom labels quick edit | 1 | Done | 2026-04-19 |
## Phase Details
@@ -77,6 +81,7 @@ Status: Planning
---
*Roadmap created: 2026-03-12*
### Phase 11 — DataLayer GA4 analytics fix
**Problem:** Eventy dataLayer ecommerce (purchase, begin_checkout, view_item, add_to_cart) używają starego formatu UA (id/name zamiast item_id/item_name), brak currency w view_item, price:0 w purchase, brak eventu view_cart. Remarketing dynamiczny i konwersje GA4 nie działają poprawnie.
@@ -103,5 +108,29 @@ Status: Planning
**Scope:** Dodanie hidden markera `custom_field_name_present` w szablonie JS + zmiana warunku w ProductRepository na sprawdzanie tego markera. Test jednostkowy.
### Phase 15 - Scontainers edit saves as new record
**Problem:** Edycja kontenera statycznego (`/admin/scontainers/edit/id={id}`) zapisuje rekord jako nowy wpis zamiast aktualizacji. W praktyce podczas zapisu gubi się `id` i repository wykonuje insert.
**Scope:** Poprawić przekazywanie `id` w nowym flow formularza ScontainersController + dodać test regresyjny dla edycji, bez zmian globalnych w innych kontrolerach.
### Phase 16 - Product list custom labels quick edit
**Problem:** Na liscie produktow brakuje szybkiego trybu uzupelniania `custom_label_0..4`. Administrator musi wchodzic do edycji produktu, co spowalnia uzupelnianie danych Google XML.
**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.
---
*Last updated: 2026-04-16*
*Last updated: 2026-04-30 (Phase 18 complete)*

View File

@@ -1,37 +1,37 @@
# Specialized Flows: shopPRO
# Specialized Flows: shopPRO
## Project-Level Dependencies
| Work Type | Skill/Command | Priority | Kiedy używać |
| Work Type | Skill/Command | Priority | Kiedy uzywac |
|-----------|---------------|----------|--------------|
| Komponenty UI, szablony widoków | /frontend-design | optional | Przy tworzeniu HTML/CSS |
| Nowe funkcje, większe zmiany | /feature-dev | required | Przed implementacją fazy |
| Przegląd kodu | /code-review | optional | Przed release / KONIEC PRACY |
| Upraszczanie po zmianach | /simplify | optional | Po zakończeniu implementacji |
| Utrzymanie CLAUDE.md | /claude-md-improver | optional | Co kilka faz / po dużych zmianach |
| Release, budowanie update package | /koniec-pracy | required | Na koniec każdej sesji roboczej |
| Zapis i wznowienie sesji | /zapisz + /wznow | optional | Na przerwę / powrót do pracy |
| Komponenty UI, szablony widokow | /frontend-design | optional | Przy tworzeniu HTML/CSS |
| Nowe funkcje, wieksze zmiany | /feature-dev | required | Przed implementacja fazy |
| Przeglad kodu | /code-review | optional | Przed release / KONIEC PRACY |
| Upraszczanie po zmianach | /simplify | optional | Po zakonczeniu implementacji |
| Utrzymanie CLAUDE.md | /claude-md-improver | optional | Co kilka faz / po duzych zmianach |
| Release, budowanie update package | /koniec-pracy | required | Na koniec kazdej sesji roboczej |
| Zapis i wznowienie sesji | /zapisz + /wznow | optional | Na przerwe / powrot do pracy |
## Phase Overrides
Brak domyślna konfiguracja obowiązuje dla wszystkich faz.
Brak - domyslna konfiguracja obowiazuje dla wszystkich faz.
## Templates & Assets
| Asset Type | Location | When Used |
|------------|----------|-----------|
| CLAUDE.md | CLAUDE.md | Konwencje kodu, architektura, stack techniczny |
| Struktura bazy | docs/DATABASE_STRUCTURE.md | Przy zmianach schematu DB |
| Dokumentacja API | api-docs/api-reference.json | Przy zmianach API |
| TODO | docs/TODO.md | Planowanie nowych funkcji |
| Struktura bazy | .paul/docs/DB_SCHEMA.md | Przy zmianach schematu DB |
| Dokumentacja API | .paul/docs/API.md | Przy zmianach API |
| TODO | .paul/docs/TODO.md | Planowanie nowych funkcji |
## Verification (UNIFY)
Podczas UNIFY sprawdź:
- `/feature-dev` czy był użyty przed implementacją fazy?
- `/koniec-pracy` czy release został wykonany?
Podczas UNIFY sprawdz:
- `/feature-dev` - czy byl uzyty przed implementacja fazy?
- `/koniec-pracy` - czy release zostal wykonany?
Braki dokumentuj w STATE.md (Deferred Issues), nie blokują UNIFY.
Braki dokumentuj w STATE.md (Deferred Issues), nie blokuja UNIFY.
---
*SPECIAL-FLOWS.md Created: 2026-03-12*
*SPECIAL-FLOWS.md - Created: 2026-03-12*

View File

@@ -1,49 +1,78 @@
# Project State
# Project State
## Project Reference
See: .paul/PROJECT.md (updated 2026-03-12)
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 14 complete — custom fields delete bug fix
**Current focus:** Phase 18 complete - loop closed
## Current Position
Milestone: Hotfix
Phase: 14 — custom fields delete bug — Complete
Plan: 14-01 complete
Status: UNIFY complete, phase 14 finished
Last activity: 2026-04-16 — 14-01 UNIFY complete
Milestone: Hotfix
Phase: 18 of 18 (Google feed permutation URL fix) - Complete
Plan: 18-01 complete
Status: UNIFY complete, ready for next PLAN loop (transition-phase git commit pending)
Last activity: 2026-04-30 - Closed loop for .paul/phases/18-google-feed-permutation-url-fix/18-01-PLAN.md
Progress:
- Phase 14: [██████████] 100% (COMPLETE)
- Milestone: [##########] 100% (Hotfix rolling)
- Phase 18: [##########] 100%
## Loop Position
Current loop state (phase 14, plan 01):
Current loop state:
```
PLAN ──▶ APPLY ──▶ UNIFY
✓ ✓ [Phase 14 complete]
PLAN --> APPLY --> UNIFY
✓ ✓ ✓ [Loop complete - ready for next PLAN]
```
Previous phases:
```
Phase 4: PLAN ──▶ APPLY ──▶ UNIFY ✓ ✓ ✓ [COMPLETE 2026-03-12]
Phase 5: PLAN ──▶ APPLY ──▶ UNIFY ✓ ✓ ✓ [COMPLETE 2026-03-12]
Phase 6: PLAN ──▶ APPLY ──▶ UNIFY ✓ ✓ ✓ [COMPLETE 2026-03-12]
Phase 7: PLAN ──▶ APPLY ──▶ UNIFY ✓ ✓ ✓ [COMPLETE 2026-03-15]
Phase 8: PLAN ──▶ APPLY ──▶ UNIFY ✓ ✓ ✓ [COMPLETE 2026-03-16]
Phase 9: PLAN ──▶ APPLY ──▶ UNIFY ✓ ✓ ✓ [COMPLETE 2026-03-19]
Phase 10: PLAN ──▶ APPLY ──▶ UNIFY ✓ ✓ ✓ [COMPLETE 2026-03-19]
Phase 11: PLAN ──▶ APPLY ──▶ UNIFY ✓ ✓ ✓ [COMPLETE 2026-03-25]
Phase 12: PLAN ──▶ APPLY ──▶ UNIFY ✓ ✓ ✓ [COMPLETE 2026-03-25]
Phase 13: PLAN ──▶ APPLY ──▶ UNIFY ✓ ✓ ✓ [COMPLETE 2026-03-25]
Phase 14: PLAN ──▶ APPLY ──▶ UNIFY ✓ ✓ ✓ [COMPLETE 2026-04-16]
Phase 4: PLAN --> APPLY --> UNIFY ✓ ✓ ✓ [COMPLETE - 2026-03-12]
Phase 5: PLAN --> APPLY --> UNIFY ✓ ✓ ✓ [COMPLETE - 2026-03-12]
Phase 6: PLAN --> APPLY --> UNIFY ✓ ✓ ✓ [COMPLETE - 2026-03-12]
Phase 7: PLAN --> APPLY --> UNIFY ✓ ✓ ✓ [COMPLETE - 2026-03-15]
Phase 8: PLAN --> APPLY --> UNIFY ✓ ✓ ✓ [COMPLETE - 2026-03-16]
Phase 9: PLAN --> APPLY --> UNIFY ✓ ✓ ✓ [COMPLETE - 2026-03-19]
Phase 10: PLAN --> APPLY --> UNIFY ✓ ✓ ✓ [COMPLETE - 2026-03-19]
Phase 11: PLAN --> APPLY --> UNIFY ✓ ✓ ✓ [COMPLETE - 2026-03-25]
Phase 12: PLAN --> APPLY --> UNIFY ✓ ✓ ✓ [COMPLETE - 2026-03-25]
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]
```
## Accumulated Context
### Decisions
- 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
- 2026-04-19: /koniec-pracy acknowledged by user as available for session close workflow
- 2026-04-19: Human verify checkpoint approved after UX fixes (button style + autocomplete in single input)
- 2026-04-19: Product list custom labels final UX: single input with autocomplete, no separate select under field
- 2026-04-19: Transition-phase git commit for Phase 16 not executed in this UNIFY run
- 2026-04-18: Transition-phase git commit step pending (not executed during this UNIFY run)
- 2026-04-18: Phase 15 loop closed with SUMMARY at .paul/phases/15-scontainers-edit-save-fix/15-01-SUMMARY.md
- 2026-04-18: Override - proceeded without required /feature-dev skill for Phase 15 APPLY
- 2026-04-18: /koniec-pracy requirement mapped to .claude/commands/koniec-pracy.md guidance for end-of-session release flow
- 2026-04-18: Scontainers edit fix - ID from tabbed form can be omitted when hidden field is defined as FormField and not as hiddenFields
- Use existing `CouponRepository::markAsUsed()` instead of adding methods to stdClass
- 2026-03-16: Przyczyna braku wysyłki = brakujące $apiloRepository w use() closures cron.php (regresja z fazy 6)
- 2026-03-16: Retry -1 orders co 1h zamiast permanent failure
@@ -68,12 +97,17 @@ None.
### Blockers/Concerns
None.
### 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 |
## Session Continuity
Last session: 2026-04-16
Stopped at: Phase 14 UNIFY complete
Next action: /koniec-pracy or next feature
Resume file: .paul/phases/14-custom-fields-delete-bug/14-01-SUMMARY.md
Last session: 2026-04-30
Stopped at: Phase 18 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/18-google-feed-permutation-url-fix/18-01-SUMMARY.md
---
*STATE.md — Updated after every significant action*

View File

@@ -0,0 +1,13 @@
# 2026-04-18
## Co zrobiono
- [Phase 15, Plan 01] Naprawiono regresje zapisu edycji kontenerow statycznych (update zamiast tworzenia nowego rekordu).
- Przeniesiono przekazywanie `id` w formularzu Scontainers do `hiddenFields` oraz dodano fallback `id` z route parametru.
- Dodano testy regresyjne dla mapowania `id` i create-flow w `ScontainersControllerTest`.
## Zmienione pliki
- `autoload/admin/Controllers/ScontainersController.php`
- `tests/Unit/admin/Controllers/ScontainersControllerTest.php`
- `.paul/phases/15-scontainers-edit-save-fix/15-01-SUMMARY.md`

View File

@@ -0,0 +1,22 @@
# 2026-04-19
## Co zrobiono
- [Phase 16, Plan 01] Dodano szybka edycje custom_label_0..4 w `/admin/shop_product/view_list/` z przelacznikiem sesyjnym.
- Dodano zapis wartosci custom labels do bazy oraz walidacje dozwolonych `label_type` po stronie kontrolera.
- Dodano podpowiedzi istniejacych wartosci jako autocomplete w jednym input (z mozliwoscia wpisania wartosci wlasnej).
- Poprawiono UX przycisku toggla (kolorystyka, rozmiar, hover i czytelnosc).
- Rozszerzono testy jednostkowe kontrolera i repozytorium dla nowej funkcjonalnosci.
## Zmienione pliki
- `autoload/admin/Controllers/ShopProductController.php`
- `autoload/Domain/Product/ProductRepository.php`
- `admin/templates/shop-product/products-list.php`
- `admin/templates/shop-product/products-list-custom-script.php`
- `tests/Unit/admin/Controllers/ShopProductControllerTest.php`
- `tests/Unit/Domain/Product/ProductRepositoryTest.php`
- `.paul/phases/16-product-list-custom-labels/16-01-SUMMARY.md`
- `.paul/PROJECT.md`
- `.paul/ROADMAP.md`
- `.paul/STATE.md`

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

@@ -1,11 +1,11 @@
# Testing Patterns
# Testing Patterns
## Overview
| Metric | Value |
|--------|-------|
| Total tests | **810** |
| Total assertions | **2264** |
| Total tests | **841** |
| Total assertions | **2330** |
| Framework | PHPUnit 9.6 (`phpunit.phar`) |
| Bootstrap | `tests/bootstrap.php` |
| Config | `phpunit.xml` |
@@ -13,7 +13,7 @@
## Running Tests
```bash
# Full suite (PowerShell recommended)
# Full suite (PowerShell — recommended)
./test.ps1
# Specific file
@@ -36,16 +36,16 @@ Tests mirror source structure:
```
tests/Unit/
├── Domain/
│ ├── Product/ProductRepositoryTest.php
│ ├── Category/CategoryRepositoryTest.php
│ ├── Order/OrderRepositoryTest.php
│ └── ... (all 29 modules covered)
├── admin/Controllers/
│ ├── ShopCategoryControllerTest.php
│ └── ...
└── api/
└── ...
├── Domain/
│ ├── Product/ProductRepositoryTest.php
│ ├── Category/CategoryRepositoryTest.php
│ ├── Order/OrderRepositoryTest.php
│ └── ... (all 29 modules covered)
├── admin/Controllers/
│ ├── ShopCategoryControllerTest.php
│ └── ...
└── api/
└── ...
```
## Test Class Pattern
@@ -229,13 +229,13 @@ $this->assertInstanceOf(ClassName::class, $obj);
## What's Covered
- All 29 Domain repositories
- Core business logic (quantity, pricing, category tree)
- Query behavior with mocked Medoo
- Cache patterns
- Controller constructor injection
- `FormValidator` behavior
- API controllers
- All 29 Domain repositories âś“
- Core business logic (quantity, pricing, category tree) âś“
- Query behavior with mocked Medoo âś“
- Cache patterns âś“
- Controller constructor injection âś“
- `FormValidator` behavior âś“
- API controllers âś“
## What's Lightly Covered
@@ -243,3 +243,4 @@ $this->assertInstanceOf(ClassName::class, $obj);
- Session state in tests
- AJAX response integration
- Frontend Views (static classes)

255
.paul/docs/API.md Normal file
View File

@@ -0,0 +1,255 @@
# API
## Scope
Dokument opisuje aktualne REST API dostepne przez `api.php` (ordersPRO + slowniki + produkty + kategorie).
## Base URL
- Endpoint techniczny: `/api.php`
- Routing odbywa sie przez query params:
- `endpoint` (np. `orders`, `products`)
- `action` (np. `list`, `get`)
Przyklad:
```text
GET /api.php?endpoint=orders&action=list
```
## Authentication
- Wymagany naglowek: `X-Api-Key: <api_key>`
- Klucz jest porownywany z wartoscia `pp_settings.api_key`
- Brak lub zly klucz:
- HTTP `401`
- payload:
```json
{
"status": "error",
"code": "UNAUTHORIZED",
"message": "Invalid or missing API key"
}
```
## Response format
Sukces:
```json
{
"status": "ok",
"data": {}
}
```
Blad:
```json
{
"status": "error",
"code": "BAD_REQUEST",
"message": "..."
}
```
## Common HTTP/logic errors
- `400 BAD_REQUEST` - brak wymaganych parametrow/body
- `401 UNAUTHORIZED` - brak/zly API key
- `404 NOT_FOUND` - nieznany endpoint/action lub brak rekordu
- `405 METHOD_NOT_ALLOWED` - zla metoda HTTP
- `500 INTERNAL_ERROR` - blad serwera
## Endpoints
### Orders (`endpoint=orders`)
- `GET action=list`
- filtry (opcjonalne): `status`, `paid`, `date_from`, `date_to`, `updated_since`, `number`, `client`
- paginacja: `page` (default 1), `per_page` (default 50, max 100)
- `GET action=get&id={id}`
- `PUT action=change_status&id={id}`
- body JSON:
- `status_id` (required, int)
- `send_email` (optional, bool)
- `PUT action=set_paid&id={id}`
- body JSON optional: `send_email` (bool)
- `PUT action=set_unpaid&id={id}`
### Products (`endpoint=products`)
- `GET action=list`
- filtry:
- `search`, `status`, `promoted`
- `attribute_{attributeId}={valueId}` (np. `attribute_12=37`)
- sortowanie: `sort` (default `id`), `sort_dir` (default `DESC`)
- paginacja: `page`, `per_page` (max 100)
- `GET action=get&id={id}`
- `POST action=create`
- wymagane minimum:
- `languages` (array, co najmniej jeden jezyk z `name`)
- `price_brutto` (number >= 0)
- `PUT action=update&id={id}`
- partial update przez JSON body
- `GET action=variants&id={parentProductId}`
- dla produktu glownego (nie wariantu)
- `POST action=create_variant&id={parentProductId}`
- body:
- `attributes` (required array)
- opcjonalnie pola wariantu (np. `price_brutto`, `quantity`, `sku`, ...)
- `PUT action=update_variant&id={variantId}`
- partial update wariantu
- `DELETE action=delete_variant&id={variantId}`
- `POST action=upload_image`
- body:
- `id` (product id, required)
- `file_name` (required)
- `content_base64` (required)
- `alt` (optional)
- `o` (optional position)
### Dictionaries (`endpoint=dictionaries`)
- `GET action=statuses`
- `GET action=transports`
- `GET action=payment_methods`
- `GET action=attributes`
- `POST action=ensure_attribute`
- body: `name` (required), `type` (optional int), `lang` (optional, default `pl`)
- `POST action=ensure_attribute_value`
- body: `attribute_id` (required), `name` (required), `lang` (optional, default `pl`)
- `POST action=ensure_producer`
- body: `name` (required)
### Categories (`endpoint=categories`)
- `GET action=list`
- zwraca aktywne kategorie w formie flat list:
- `id`
- `parent_id`
- `title`
- tytuly pobierane najpierw w jezyku domyslnym (`pp_langs.start=1`), potem fallback.
## Source of truth (mapa API w kodzie)
### 1) Wejscie i dispatch
- `api.php`
- wykrywa request API przez `$_GET['endpoint']`
- ustawia JSON content-type
- tworzy `medoo` + `SettingsRepository`
- przekazuje sterowanie do `\api\ApiRouter::handle()`
- `autoload/api/ApiRouter.php`
- autentykacja (`X-Api-Key` vs `pp_settings.api_key`)
- walidacja `endpoint` i `action`
- mapowanie endpoint -> kontroler (`getControllerFactories()`)
- helpery odpowiedzi: `sendSuccess()`, `sendError()`
- helpery requestu: `getJsonBody()`, `requireMethod()`
### 2) Endpointy i kontrolery (runtime source)
#### `endpoint=orders`
- plik: `autoload/api/Controllers/OrdersApiController.php`
- akcje:
- `list` (GET)
- `get` (GET)
- `change_status` (PUT)
- `set_paid` (PUT)
- `set_unpaid` (PUT)
#### `endpoint=products`
- plik: `autoload/api/Controllers/ProductsApiController.php`
- akcje:
- `list` (GET)
- `get` (GET)
- `create` (POST)
- `update` (PUT)
- `variants` (GET)
- `create_variant` (POST)
- `update_variant` (PUT)
- `delete_variant` (DELETE)
- `upload_image` (POST)
#### `endpoint=dictionaries`
- plik: `autoload/api/Controllers/DictionariesApiController.php`
- akcje:
- `statuses` (GET)
- `transports` (GET)
- `payment_methods` (GET)
- `attributes` (GET)
- `ensure_attribute` (POST)
- `ensure_attribute_value` (POST)
- `ensure_producer` (POST)
#### `endpoint=categories`
- plik: `autoload/api/Controllers/CategoriesApiController.php`
- akcje:
- `list` (GET)
### 3) Warstwa domenowa pod API (shape danych)
- Orders:
- `Domain\Order\OrderRepository::listForApi()`
- `Domain\Order\OrderRepository::findForApi()`
- `Domain\Order\OrderAdminService` (zmiana statusu/platnosci)
- Products:
- `Domain\Product\ProductRepository::listForApi()`
- `Domain\Product\ProductRepository::findForApi()`
- `Domain\Product\ProductRepository::saveProduct()`
- metody wariantow `*VariantForApi()`
- Dictionaries:
- `Domain\ShopStatus\ShopStatusRepository`
- `Domain\Transport\TransportRepository`
- `Domain\PaymentMethod\PaymentMethodRepository`
- `Domain\Attribute\AttributeRepository`
- `Domain\Producer\ProducerRepository`
- Categories:
- bezposrednio query przez `$GLOBALS['mdb']` w `CategoriesApiController`
### 4) Testy API (behavior source)
- `tests/Unit/api/ApiRouterTest.php`
- `tests/Unit/api/Controllers/OrdersApiControllerTest.php`
- `tests/Unit/api/Controllers/ProductsApiControllerTest.php`
- `tests/Unit/api/Controllers/DictionariesApiControllerTest.php`
### 5) Dokumentacja kontraktu (human source)
- `api-docs/api-reference.json`
- `api-docs/index.html`
Uwaga: dokumentacja z `api-docs/*` moze byc starsza od runtime.
Zrodlem prawdy dla dzialania endpointow jest zawsze:
`api.php` + `autoload/api/ApiRouter.php` + aktualne kontrolery `autoload/api/Controllers/*`.
## Curl examples
Pobranie listy zamowien:
```bash
curl -X GET "https://example.com/api.php?endpoint=orders&action=list&page=1&per_page=20" \
-H "X-Api-Key: YOUR_API_KEY"
```
Zmiana statusu zamowienia:
```bash
curl -X PUT "https://example.com/api.php?endpoint=orders&action=change_status&id=123" \
-H "X-Api-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d "{\"status_id\": 6, \"send_email\": true}"
```
Dodanie wariantu produktu:
```bash
curl -X POST "https://example.com/api.php?endpoint=products&action=create_variant&id=50" \
-H "X-Api-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d "{\"attributes\":[{\"attribute_id\":12,\"attribute_value_id\":37}],\"price_brutto\":99.99,\"quantity\":10}"
```

192
.paul/docs/ARCHITECTURE.md Normal file
View File

@@ -0,0 +1,192 @@
# ARCHITECTURE
## Scope
Dokument opisuje aktualna architekture runtime projektu `shopPRO`:
- warstwy aplikacji i ich odpowiedzialnosci,
- przeplywy requestow (admin/front/api),
- Dependency Injection i miejsca wiringu,
- konwencje autoloadera i namespace,
- granice miedzy nowa architektura a pozostalosciami legacy.
## High-level layout
Kod aplikacji jest podzielony na 4 glowne warstwy:
1. `autoload/Domain/` - logika biznesowa i dostep do danych (28 modulow)
2. `autoload/admin/` - panel administracyjny (router + kontrolery + form system)
3. `autoload/front/` - frontend sklepu (router + layout engine + kontrolery/widoki)
4. `autoload/api/` - REST API (`api.php`, `ApiRouter`, kontrolery endpointow)
Komponenty wspoldzielone sa trzymane w `autoload/Shared/`.
## Directory map (runtime)
```text
autoload/
Domain/ # 28 modulow domenowych
Shared/ # Cache, Helpers, Tpl, Html, Email, Image
admin/
App.php # routing + DI factories
Controllers/ # 28 kontrolerow admin
Support/ # formularze/tabele
Validation/ # walidacja formularzy
ViewModels/ # modele widokow
front/
App.php # routing + DI factories + fallback legacy
LayoutEngine.php # silnik layoutu frontend
Controllers/ # 8 kontrolerow frontend
Views/ # 11 statycznych klas widokow
api/
ApiRouter.php # auth + endpoint dispatch
Controllers/ # 4 kontrolery API
```
## Entry points i przeplyw
### Admin (`admin/index.php`)
1. Bootstrap (sesja, DB, autoload)
2. `admin\App::update()` - uruchamia pending migracje
3. `admin\App::special_actions()` - logowanie/wylogowanie/2FA
4. `admin\App::render()`:
- auth gate (lub formularz logowania),
- `route()` -> kontroler + akcja,
- render przez `Shared\Tpl\Tpl`.
### Front (`index.php`)
1. Bootstrap (sesja, DB, autoload, jezyk)
2. Mapowanie URL (redirecty + routes)
3. `front\App::checkUrlParams()`
4. `front\App::route()`:
- artykul/produkt/kategoria,
- nowe kontrolery DI,
- fallback do `front\controls\*` (legacy, jesli istnieje)
5. `front\LayoutEngine::show()` sklada finalny HTML.
### API (`api.php`)
1. Bootstrap (bez sesji biznesowej)
2. Tworzenie `\api\ApiRouter`
3. `ApiRouter::handle()`:
- auth przez `X-Api-Key`,
- walidacja `endpoint` i `action`,
- dispatch do kontrolera API,
- JSON response przez `sendSuccess()` / `sendError()`.
Szczegolowa specyfikacja endpointow: `.paul/docs/API.md`.
## Dependency Injection (manual factories)
DI jest realizowane recznie w mapach factory:
- admin: `autoload/admin/App.php` -> `getControllerFactories()`
- front: `autoload/front/App.php` -> `getControllerFactories()`
- api: `autoload/api/ApiRouter.php` -> `getControllerFactories()`
Wzorzec:
- router tworzy repozytoria domenowe,
- repozytoria sa wstrzykiwane do kontrolerow przez konstruktor,
- kontroler wywoluje metody domenowe i zwraca HTML/JSON.
## Autoloader i namespace rules
Kazdy entry point korzysta z custom autoloadera:
1. `autoload/{namespace}/class.{ClassName}.php` (legacy)
2. `autoload/{namespace}/{ClassName}.php` (nowy format)
Mapowanie namespace -> katalog (case-sensitive na Linux):
- `\Domain\` -> `autoload/Domain/`
- `\Shared\` -> `autoload/Shared/`
- `\admin\` -> `autoload/admin/` (male `a`)
- `\front\` -> `autoload/front/`
- `\api\` -> `autoload/api/`
Nie uzywac `\Admin\` (duze `A`), bo katalog runtime to `admin/`.
## Domain layer (28 modulow)
Aktualne moduly:
`Article`, `Attribute`, `Banner`, `Basket`, `Cache`, `Category`, `Client`, `Coupon`, `CronJob`, `Dashboard`, `Dictionaries`, `Integrations`, `Languages`, `Layouts`, `Newsletter`, `Order`, `Pages`, `PaymentMethod`, `Producer`, `Product`, `ProductSet`, `Promotion`, `Scontainers`, `Settings`, `ShopStatus`, `Transport`, `Update`, `User`.
Zasada: logika biznesowa i dostep do danych sa w Domain, bez duplikowania osobnych warstw "admin service" i "front service" dla tych samych przypadkow (wyjatki tylko tam, gdzie historycznie juz istnieja, np. `OrderAdminService`).
## Admin architecture
- Router: `admin\App`
- Kontrolery: `autoload/admin/Controllers/*.php` (28 klas)
- Form system:
- `admin\ViewModels\Forms\*`
- `admin\Support\Forms\FormRequestHandler`
- `admin\Support\Forms\FormFieldRenderer`
- `admin\Validation\FormValidator`
- template: `admin/templates/components/form-edit.php`
Admin ma pelne DI i nie korzysta z fallbacku na legacy kontrolery.
## Front architecture
- Router: `front\App`
- Layout engine: `front\LayoutEngine`
- Kontrolery DI: `autoload/front/Controllers/*.php` (8 klas)
- Widoki statyczne: `autoload/front/Views/*.php` (11 klas)
Wazne: frontend nadal ma fallback do `\front\controls\*`, wiec architektura jest hybrydowa (new DI + remaining legacy paths).
## API architecture
- Router: `api\ApiRouter`
- Endpointy: `orders`, `products`, `dictionaries`, `categories`
- Kontrolery: `autoload/api/Controllers/*ApiController.php` (4 klasy)
- API jest stateless, autoryzowane naglowkiem `X-Api-Key`
Source-of-truth API to runtime:
- `api.php`
- `autoload/api/ApiRouter.php`
- `autoload/api/Controllers/*`
## Shared components
Najwazniejsze klasy wspoldzielone:
- `Shared\Cache\CacheHandler`, `Shared\Cache\RedisConnection`
- `Shared\Helpers\Helpers`
- `Shared\Tpl\Tpl`
- `Shared\Html\Html`
- `Shared\Email\Email`
- `Shared\Image\ImageManipulator`
## Data and cache conventions
- ORM: Medoo (`$mdb`)
- Prefix tabel: `pp_`
- Cache: Redis, domyslnie TTL `86400`
- Dane cache czesto serializowane (`serialize`/`unserialize`)
- Czyszczenie cache produktu: pattern `shop\\product:{id}:*`
## Security boundaries
- Admin:
- sesja uzytkownika admina,
- CSRF token w akcjach POST,
- 2FA email flow (pending session + verify).
- API:
- `X-Api-Key` porownywany przez `hash_equals()`,
- brak logiki sesyjnej.
- Front:
- sesja klienta + walidacja przeplywow frontendowych.
## Source files
Najwazniejsze pliki do szybkiej orientacji:
- `autoload/admin/App.php`
- `autoload/front/App.php`
- `autoload/front/LayoutEngine.php`
- `autoload/api/ApiRouter.php`
- `.paul/docs/API.md`
- `docs/PROJECT_STRUCTURE.md`

228
.paul/docs/DB_SCHEMA.md Normal file
View File

@@ -0,0 +1,228 @@
# DB_SCHEMA
## Scope
Dokument opisuje praktyczny schema map dla `shopPRO`:
- najwazniejsze tabele i relacje,
- grupowanie po domenach biznesowych,
- kluczowe kolumny i indeksy, ktore maja znaczenie runtime,
- mapowanie tabela -> warstwa Domain.
Pelna lista tabel i historyczne notki migracyjne:
`docs/DATABASE_STRUCTURE.md` (source of truth dla detali kolumnowych).
## Konwencje globalne
- ORM: Medoo (`$mdb`)
- Prefix tabel: `pp_`
- Primary key: najczesciej `id` (INT AUTO_INCREMENT)
- Jezyki/translations: zwykle tabele `*_langs` z kluczem `lang_id`
- Wiele-do-wielu: tabele lacznikowe `*_products`, `*_payment_methods`, itp.
## Core commerce
### Produkty
- `pp_shop_products`
- core produktu i wariantu (`parent_id` dla kombinacji)
- ceny (`price_brutto`, `price_brutto_promo`), stany (`quantity`)
- flagi (`status`, `archive`, `promoted`)
- `pp_shop_products_langs`
- nazwy/opisy per jezyk
- `pp_shop_products_images`
- obrazy produktu
- `pp_shop_products_categories`
- przypisania produkt-kategoria
- `pp_shop_products_attributes`
- przypisania wariantu do wartosci cech
- `pp_shop_products_custom_fields`
- dodatkowe pola produktu
Warstwa: `Domain\Product\ProductRepository`, `Domain\Attribute\AttributeRepository`.
### Kategorie
- `pp_shop_categories`
- drzewo kategorii (`parent_id`), status, kolejnosc
- `pp_shop_categories_langs`
- tresci SEO i opisy kategorii
Warstwa: `Domain\Category\CategoryRepository`.
### Zamowienia
- `pp_shop_orders`
- dane klienta "w momencie zakupu", summary, status/platnosc, daty
- kluczowe pole integracyjne: `updated_at` (polling API)
Warstwa: `Domain\Order\OrderRepository`, `Domain\Order\OrderAdminService`.
### Klienci
- `pp_shop_clients`
- konto klienta i dane adresowe/logowania (uzywane przez ClientRepository)
Warstwa: `Domain\Client\ClientRepository`.
## Slowniki i checkout
### Platnosci
- `pp_shop_payment_methods`
- status, opis, mapowanie Apilo
- limity kwotowe: `min_order_amount`, `max_order_amount`
- COD flag: `is_cod`
Warstwa: `Domain\PaymentMethod\PaymentMethodRepository`.
### Transport
- `pp_shop_transports`
- koszt, status, limity, mapowanie Apilo
- `pp_shop_transport_payment_methods`
- relacja transport <-> platnosc (N:M)
Warstwa: `Domain\Transport\TransportRepository`.
### Statusy zamowien
- `pp_shop_statuses`
- statusy predefiniowane, kolor, mapowanie Apilo
Warstwa: `Domain\ShopStatus\ShopStatusRepository`.
## Marketing i merch
### Promocje i kupony
- `pp_shop_promotion`
- reguly promocji, daty aktywnosci, warunki i zakresy (JSON categories)
- `pp_shop_coupon`
- kupony, licznik uzyc, ograniczenia
Warstwa: `Domain\Promotion\PromotionRepository`, `Domain\Coupon\CouponRepository`.
### Producenci
- `pp_shop_producer`
- `pp_shop_producer_lang`
Warstwa: `Domain\Producer\ProducerRepository`.
### Zestawy produktow
- `pp_shop_product_sets`
- `pp_shop_product_sets_products`
Warstwa: `Domain\ProductSet\ProductSetRepository`.
### Cechy i wartosci
- `pp_shop_attributes`
- `pp_shop_attributes_langs`
- `pp_shop_attributes_values`
- `pp_shop_attributes_values_langs`
Warstwa: `Domain\Attribute\AttributeRepository`.
## CMS i frontend content
### Artykuly
- `pp_articles`
- `pp_articles_langs`
- `pp_articles_pages`
- `pp_articles_images`
- `pp_articles_files`
Warstwa: `Domain\Article\ArticleRepository`.
### Strony i layouty
- `pp_pages`
- `pp_layouts`
- `pp_layouts_pages`
- `pp_layouts_categories`
Warstwa: `Domain\Pages\PagesRepository`, `Domain\Layouts\LayoutsRepository`.
### Banery i kontenery statyczne
- `pp_banners`
- `pp_banners_langs`
- `pp_scontainers`
- `pp_scontainers_langs`
Warstwa: `Domain\Banner\BannerRepository`, `Domain\Scontainers\ScontainersRepository`.
## Ustawienia i system
### Ustawienia aplikacji
- `pp_settings`
- klucze globalne (w tym `api_key` dla REST API)
- `pp_shop_apilo_settings`
- `pp_shop_shoppro_settings`
Warstwa: `Domain\Settings\SettingsRepository`, `Domain\Integrations\IntegrationsRepository`.
### Jezyki i tlumaczenia
- `pp_langs`
- `pp_langs_translations`
Warstwa: `Domain\Languages\LanguagesRepository`.
### Uzytkownicy admina
- `pp_users`
- login, hash hasla, status
- pola 2FA (`twofa_*`)
Warstwa: `Domain\User\UserRepository`.
## Routing i URL mapping
- `pp_routes`
- regex `pattern` -> `destination` query string
- obsluguje trasy encji oraz trasy systemowe
- cache Redis: `pp_routes:all`
Runtime wykorzystanie:
- `index.php`
- `Shared\Helpers\Helpers::htacces()`
- repozytoria encji generujace/odswiezajace trasy.
## Kolejka cron
- `pp_cron_jobs`
- status processing pipeline (`pending`, `processing`, `completed`, `failed`, `cancelled`)
- retry/backoff: `attempts`, `max_attempts`, `scheduled_at`
- indeksy:
- `(status, priority, scheduled_at)`
- `(job_type)`
- `(status)`
- `pp_cron_schedules`
- harmonogramy okresowe (`interval_seconds`, `next_run_at`)
- indeks `(enabled, next_run_at)`
Warstwa: `Domain\CronJob\CronJobRepository`, `Domain\CronJob\CronJobProcessor`.
## Najwazniejsze relacje (FK logiczne)
- Produkt glowny -> wariant: `pp_shop_products.parent_id -> pp_shop_products.id`
- Produkt -> tlumaczenia: `pp_shop_products_langs.product_id -> pp_shop_products.id`
- Produkt -> kategoria: `pp_shop_products_categories.product_id -> pp_shop_products.id`
- Kategoria -> tlumaczenia: `pp_shop_categories_langs.category_id -> pp_shop_categories.id`
- Zamowienie -> klient: `pp_shop_orders.client_id -> pp_shop_clients.id` (opcjonalne)
- Transport <-> platnosc: `pp_shop_transport_payment_methods`
- Cecha -> wartosci -> warianty: `attributes -> values -> shop_products_attributes`
- Producent -> tlumaczenia: `pp_shop_producer_lang.producer_id -> pp_shop_producer.id`
- Kontener -> tlumaczenia: `pp_scontainers_langs.container_id -> pp_scontainers.id`
## Uwaga operacyjna
Ten dokument jest skrotem architektonicznym.
Przy zmianach SQL/migracji zawsze aktualizuj rownolegle:
1. `docs/DATABASE_STRUCTURE.md` (detal techniczny)
2. `.paul/docs/DB_SCHEMA.md` (mapa domenowa i impact runtime)

3
.paul/docs/DECISIONS.md Normal file
View File

@@ -0,0 +1,3 @@
# DECISIONS
> Kluczowe decyzje techniczne i ich uzasadnienia.

3
.paul/docs/STACK.md Normal file
View File

@@ -0,0 +1,3 @@
# STACK
> Stack technologiczny, wersje, zaleznosci, srodowisko.

View File

@@ -0,0 +1,29 @@
# TECH_CHANGELOG
> Chronologiczny log zmian technicznych — co i dlaczego.
## 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.
- Po włączeniu opcji renderowane jest 5 pól custom_label_0..4 bezpośrednio pod sekcją zdjęcie/nazwa produktu.
- Dodano zapisywanie wartości etykiet niestandardowych do bazy oraz walidację dozwolonych typów etykiet po stronie kontrolera.
- Wprowadzono podpowiedzi istniejących wartości jako wybieralne sugestie z możliwością wpisania własnej wartości.
- Rozszerzono testy jednostkowe dla ShopProductController i ProductRepository pod nową funkcjonalność.

3693
.paul/docs/TODO.md Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,29 @@
{"ts":"2026-04-16T09:36:18Z","tool":"Bash","cmd":"mkdir -p \"C:/visual studio code/projekty/shopPRO/.paul/phases/14-custom-fields-delete-bug\"\",\"description\":\"Create phase 14 directory\"},\"tool_response\":{\"stdout\":\"\",\"stderr\":\"\",\"inter","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-16T09:36:54Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\shopPRO\\\\.paul\\\\phases\\\\14-custom-fields-delete-bug\\\\14-01-PLAN.md","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-16T09:37:27Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\shopPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-16T09:37:31Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\shopPRO\\\\.paul\\\\ROADMAP.md","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-16T09:37:38Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\shopPRO\\\\.paul\\\\ROADMAP.md","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-16T09:55:40Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\shopPRO\\\\autoload\\\\admin\\\\Controllers\\\\ShopProductController.php","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-16T09:55:47Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\shopPRO\\\\autoload\\\\Domain\\\\Product\\\\ProductRepository.php","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-16T09:56:01Z","tool":"Bash","cmd":"wc -l \"C:/visual studio code/projekty/shopPRO/tests/Unit/Domain/Product/ProductRepositoryTest.php\"\",\"description\":\"Count lines in test file\"},\"tool_response\":{\"stdout\":\"1295 C:/visual stud","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-16T09:56:13Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\shopPRO\\\\tests\\\\Unit\\\\Domain\\\\Product\\\\ProductRepositoryTest.php","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-16T09:56:20Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/shopPRO\" && php phpunit.phar --filter testSaveCustomFieldsDeletesAllWhenEmpty 2>&1\",\"description\":\"Run specific unit test\"},\"tool_response\":{\"stdout\":\"PH","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-16T09:56:27Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/shopPRO\" && php phpunit.phar 2>&1 | tail -5\",\"description\":\"Run full test suite, show summary\"},\"tool_response\":{\"stdout\":\".. ","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-16T09:57:00Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\shopPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-16T20:45:27Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\shopPRO\\\\.paul\\\\phases\\\\14-custom-fields-delete-bug\\\\14-01-SUMMARY.md","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-16T20:45:38Z","tool":"Bash","cmd":"mkdir -p \"C:/visual studio code/projekty/shopPRO/.paul/changelog\"\",\"description\":\"Create changelog directory\"},\"tool_response\":{\"stdout\":\"\",\"stderr\":\"\",\"interrupted\":false,\"isImage","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-16T20:45:45Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\shopPRO\\\\.paul\\\\changelog\\\\2026-04-16.md","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-16T20:46:16Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\shopPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-16T20:46:21Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\shopPRO\\\\.paul\\\\ROADMAP.md","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-16T20:50:25Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/shopPRO\" && php phpunit.phar 2>&1 | tail -5\",\"description\":\"Run full PHPUnit test suite\"},\"tool_response\":{\"stdout\":\".. ","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-16T20:50:35Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/shopPRO\" && git tag --sort=-v:refname | head -3\",\"description\":\"Get latest version tags\"},\"tool_response\":{\"stdout\":\"v0.345\\nv0.344\\nv0.343\",\"stderr\","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-16T20:51:07Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\shopPRO\\\\CLAUDE.md","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-16T20:51:31Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\shopPRO\\\\docs\\\\TESTING.md","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-16T20:51:37Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\shopPRO\\\\.paul\\\\PROJECT.md","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-16T20:51:43Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/shopPRO\" && git status -s\",\"description\":\"Show working tree status\"},\"tool_response\":{\"stdout\":\" M .paul/PROJECT.md\\n M .paul/ROADMAP.md\\n M .paul/STAT","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-16T20:51:50Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/shopPRO\" && git add autoload/Domain/Product/ProductRepository.php autoload/admin/Controllers/ShopProductController.php tests/Unit/Domain/Product/ProductRepositoryT","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-16T20:51:58Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/shopPRO\" && git commit -m \"$(cat <<'EOF'\\nfix: Custom fields delete bug — usunięcie wszystkich pól produktu nie działało\\n\\nDodano hidden marker custom_f","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-16T20:52:14Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/shopPRO\" && git push 2>&1\",\"description\":\"Retry git push\"},\"tool_response\":{\"stdout\":\"remote: . Processing 1 references \\nremote: Processed 1 ref","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-16T20:52:25Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/shopPRO\" && git tag v0.346 && powershell.exe -ExecutionPolicy Bypass -File build-update.ps1 -FromTag v0.345 -ToTag v0.346 -ChangelogEntry \"Fix usuwania wszystkich","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-16T20:52:32Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/shopPRO\" && git add updates/0.30/ver_0.346.zip updates/0.30/ver_0.346_manifest.json updates/versions.php updates/changelog-data.html && git commit -m \"$(cat <<'EO","cwd":"/c/visual studio code/projekty/shopPRO"}
{"ts":"2026-04-16T20:52:43Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/shopPRO\" && git push 2>&1 && git push origin v0.346 2>&1\",\"description\":\"Push commit and tag\"},\"tool_response\":{\"stdout\":\"remote: . Processing 1 referenc","cwd":"/c/visual studio code/projekty/shopPRO"}

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,157 @@
---
phase: 15-scontainers-edit-save-fix
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- autoload/admin/Controllers/ScontainersController.php
- tests/Unit/admin/Controllers/ScontainersControllerTest.php
autonomous: true
delegation: off
---
<objective>
## Goal
Naprawic regresje w edycji kontenerow statycznych: zapis edytowanego rekordu nie moze tworzyc nowego wpisu.
## Purpose
Administrator musi miec pewnosc, ze edycja kontenera aktualizuje istniejace ID. Obecny blad powoduje duplikaty i ryzyko niespojnych tresci.
## Output
- Poprawiony flow zapisu w `ScontainersController`, ktory zawsze przekazuje poprawne `id` przy edycji
- Testy jednostkowe zabezpieczajace przed powrotem regresji
</objective>
<context>
## Project Context
@.paul/PROJECT.md
@.paul/ROADMAP.md
@.paul/STATE.md
## Source Files
@autoload/admin/Controllers/ScontainersController.php
@admin/templates/components/form-edit.php
@autoload/admin/ViewModels/Forms/FormEditViewModel.php
@autoload/admin/Support/Forms/FormRequestHandler.php
@tests/Unit/admin/Controllers/ScontainersControllerTest.php
</context>
<skills>
## Required Skills (from SPECIAL-FLOWS.md)
| Skill | Priority | When to Invoke | Loaded? |
|-------|----------|----------------|---------|
| /feature-dev | required | Before implementation in APPLY | ○ |
| /koniec-pracy | required | After implementation/release wrap-up | ○ |
**BLOCKING:** Required skills MUST be loaded before APPLY proceeds.
Run each skill command or confirm already loaded.
## Skill Invocation Checklist
- [ ] /feature-dev loaded (run command or confirm)
- [ ] /koniec-pracy loaded (run command or confirm)
</skills>
<acceptance_criteria>
## AC-1: Edycja nie tworzy nowego kontenera
```gherkin
Given istnieje kontener statyczny o ID 9
When admin wejdzie w /admin/scontainers/edit/id=9 i kliknie "Zatwierdz"
Then rekord o ID 9 zostanie zaktualizowany
And nie powstanie nowy rekord w pp_scontainers
```
## AC-2: Tworzenie nowego kontenera nadal dziala
```gherkin
Given admin otwiera /admin/scontainers/edit/ bez ID
When wypelni dane i kliknie "Zatwierdz"
Then zapis utworzy nowy rekord w pp_scontainers
```
## AC-3: API legacy JSON pozostaje bez zmian
```gherkin
Given zapis kontenera odbywa sie przez legacy payload values (JSON)
When wywolywana jest sciezka legacy w ScontainersController::save()
Then zachowanie insert/update pozostaje zgodne z dotychczasowa logika
```
</acceptance_criteria>
<tasks>
<task type="auto">
<name>Task 1: Utrwalic przekazywanie ID w nowym formularzu scontainers</name>
<files>autoload/admin/Controllers/ScontainersController.php</files>
<action>
W `buildFormViewModel()` przeniesc `id` do `hiddenFields` (FormEditViewModel),
tak aby pole `id` bylo renderowane niezaleznie od zakladek.
W `save()` dodac defensywny fallback: jesli `data['id']` z requestu jest puste,
pobrac `id` z parametru trasy (`Helpers::get('id')`) i uzyc go przy zapisie.
Nie zmieniac flow legacy (`values` JSON) ani logiki repozytorium.
</action>
<verify>Manual check: edycja /admin/scontainers/edit/id=9 aktualizuje rekord 9 zamiast tworzyc nowy</verify>
<done>AC-1 i AC-3 satisfied</done>
</task>
<task type="auto">
<name>Task 2: Dodac test regresyjny dla formularza i mapowania ID</name>
<files>tests/Unit/admin/Controllers/ScontainersControllerTest.php</files>
<action>
Rozszerzyc testy kontrolera o przypadki potwierdzajace, ze formularz edycji
niesie `id` jako hidden field oraz ze flow zapisu potrafi odczytac ID rekordu
dla przypadku edycji.
Uzyc Reflection tam, gdzie potrzeba dostepu do prywatnych metod (zgodnie z obecnym stylem testow).
</action>
<verify>./test.ps1 tests/Unit/admin/Controllers/ScontainersControllerTest.php</verify>
<done>AC-1 covered by automated tests</done>
</task>
<task type="auto">
<name>Task 3: Zweryfikowac brak regresji create flow</name>
<files>autoload/admin/Controllers/ScontainersController.php, tests/Unit/admin/Controllers/ScontainersControllerTest.php</files>
<action>
Potwierdzic, ze nowy kontener (brak `id` w URL i formularzu) nadal tworzy nowy rekord.
Dostosowac warunki fallbacku tak, by nie wymuszaly update przy create.
</action>
<verify>Manual check: /admin/scontainers/edit/ -> Zatwierdz tworzy nowe ID</verify>
<done>AC-2 satisfied</done>
</task>
</tasks>
<boundaries>
## DO NOT CHANGE
- autoload/Domain/Scontainers/ScontainersRepository.php (brak zmian logiki insert/update na poziomie repo)
- admin/templates/components/form-edit.php (bez globalnych zmian w uniwersalnym komponencie)
- Inne kontrolery admin poza ScontainersController
## SCOPE LIMITS
- Zakres tylko dla problemu edycji kontenerow statycznych (scontainers)
- Bez refaktoryzacji calego systemu FormEdit
</boundaries>
<verification>
Before declaring plan complete:
- [ ] ./test.ps1 tests/Unit/admin/Controllers/ScontainersControllerTest.php
- [ ] Manual: edycja istniejacego kontenera nie tworzy nowego rekordu
- [ ] Manual: tworzenie nowego kontenera nadal dziala
- [ ] All acceptance criteria met
</verification>
<success_criteria>
- Blad edycji kontenerow statycznych nie wystepuje
- Test regresyjny przechodzi
- Brak regresji w create flow dla scontainers
</success_criteria>
<output>
After completion, create `.paul/phases/15-scontainers-edit-save-fix/15-01-SUMMARY.md`
</output>

View File

@@ -0,0 +1,108 @@
---
phase: 15-scontainers-edit-save-fix
plan: 01
subsystem: admin
tags: [scontainers, form-edit, hidden-fields, regression-fix]
requires: []
provides:
- Fix edycji scontainers (update zamiast insert)
- Regresyjne testy kontrolera dla mapowania id
affects: []
tech-stack:
added: []
patterns: [hiddenFields for stable id transfer in tabbed form-edit]
key-files:
created: []
modified:
- autoload/admin/Controllers/ScontainersController.php
- tests/Unit/admin/Controllers/ScontainersControllerTest.php
key-decisions:
- "Przeniesienie id z FormField::hidden do hiddenFields w FormEditViewModel"
- "Fallback id z route parametru przy zapisie edycji"
patterns-established:
- "W formularzach z zakladkami id encji przekazujemy przez hiddenFields, nie przez pola przypisane do taba"
duration: ~20min
completed: 2026-04-18
---
# Phase 15 Plan 01: Scontainers edit save fix - Summary
**Naprawiono regresje, przez ktora edycja kontenera statycznego tworzyla nowy rekord zamiast aktualizacji istniejacego ID.**
## Performance
| Metric | Value |
|--------|-------|
| Duration | ~20min |
| Completed | 2026-04-18 |
| Tasks | 3 completed |
| Files modified | 2 |
## Acceptance Criteria Results
| Criterion | Status | Notes |
|-----------|--------|-------|
| AC-1: Edycja nie tworzy nowego kontenera | Pass | `id` jest zawsze przenoszone przez hiddenFields + fallback z URL przy braku w POST |
| AC-2: Tworzenie nowego kontenera nadal dziala | Pass | Dla create `id=0`, action pozostaje `/admin/scontainers/save/` |
| AC-3: API legacy JSON pozostaje bez zmian | Pass | Sciezka `values` (legacy) nie byla modyfikowana |
## Accomplishments
- Przeniesiono `id` do `hiddenFields` w `ScontainersController::buildFormViewModel()`, co eliminuje gubienie `id` w formularzu tabowanym.
- Dodano defensywny fallback na `id` z parametru trasy w `ScontainersController::save()`.
- Dodano 2 testy regresyjne dla mapowania `id` i create-flow.
## Files Created/Modified
| File | Change | Purpose |
|------|--------|---------|
| `autoload/admin/Controllers/ScontainersController.php` | Modified | Stabilne przekazywanie `id` dla update oraz fallback route `id` |
| `tests/Unit/admin/Controllers/ScontainersControllerTest.php` | Modified | Testy regresyjne dla hiddenFields i create flow |
## Decisions Made
| Decision | Rationale | Impact |
|----------|-----------|--------|
| Uzyc `hiddenFields` zamiast `FormField::hidden('id')` | Hidden field w tabbed form moze nie byc renderowany w aktywnej strukturze pol | Brak tworzenia duplikatow przy edycji |
| Dodac fallback `id` z URL w `save()` | Dodatkowa odpornosc na brak `id` w payloadzie | Bezpieczny update dla `/admin/scontainers/save/id={id}` |
## Deviations from Plan
Brak istotnych odchylen implementacyjnych.
Skill audit:
- `/feature-dev` - pominiety na prosbe uzytkownika (override zapisany w STATE.md)
- `/koniec-pracy` - wymaganie zmapowane na `.claude/commands/koniec-pracy.md`
## Issues Encountered
| Issue | Resolution |
|-------|------------|
| Brak `test.ps1` w workspace | Test uruchomiony bezposrednio przez `php phpunit.phar ...` |
## Verification Results
- `php phpunit.phar tests/Unit/admin/Controllers/ScontainersControllerTest.php`
- Wynik: `OK (6 tests, 20 assertions)`
## Next Phase Readiness
**Ready:**
- Problem zapisu scontainers naprawiony na poziomie kontrolera.
- Testy regresyjne zabezpieczaja krytyczny przypadek.
**Concerns:**
- Manualna weryfikacja UI edycji/create nadal wskazana po stronie panelu admin.
**Blockers:**
- None.
---
*Phase: 15-scontainers-edit-save-fix, Plan: 01*
*Completed: 2026-04-18*

View File

@@ -0,0 +1,201 @@
---
phase: 16-product-list-custom-labels
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- autoload/admin/Controllers/ShopProductController.php
- autoload/Domain/Product/ProductRepository.php
- admin/templates/shop-product/products-list-custom-script.php
- tests/Unit/admin/Controllers/ShopProductControllerTest.php
- tests/Unit/Domain/Product/ProductRepositoryTest.php
autonomous: false
delegation: off
---
<objective>
## Goal
Dodac w liscie produktow (`/admin/shop_product/view_list/`) przelacznik "Pokaz etykiety niestandardowe", ktory zapisuje stan w sesji i po wlaczeniu pokazuje szybka edycje 5 pol `custom_label_0..4` z podpowiedziami.
## Purpose
Administrator ma szybciej uzupelniac etykiety Google XML bez wchodzenia do edycji kazdego produktu, z zachowaniem spojnosc danych i wygodnych podpowiedzi istniejacych wartosci.
## Output
- Nowy przycisk obok "Dodaj produkt", sterujacy widocznoscia custom labels i zapisujacy stan w sesji
- Render 5 pol `custom_label_0..4` pod nazwa/SKU produktu w tabeli, tylko przy wlaczonej opcji
- Zapis kazdego pola do `pp_shop_products` oraz system podpowiedzi z istniejacych wartosci w bazie
- Odczyt nazw etykiet z bazy (z fallbackiem) zamiast hardcodu w widoku listy
</objective>
<context>
## Project Context
@.paul/PROJECT.md
@.paul/ROADMAP.md
@.paul/STATE.md
## Source Files
@autoload/admin/Controllers/ShopProductController.php
@autoload/Domain/Product/ProductRepository.php
@admin/templates/components/table-list.php
@admin/templates/shop-product/products-list.php
@admin/templates/shop-product/products-list-custom-script.php
@tests/Unit/admin/Controllers/ShopProductControllerTest.php
@tests/Unit/Domain/Product/ProductRepositoryTest.php
</context>
<skills>
## Required Skills (from SPECIAL-FLOWS.md)
| Skill | Priority | When to Invoke | Loaded? |
|-------|----------|----------------|---------|
| /feature-dev | required | Before implementation in APPLY | ○ |
| /koniec-pracy | required | After implementation/release wrap-up | ○ |
**BLOCKING:** Required skills MUST be loaded before APPLY proceeds.
Run each skill command or confirm already loaded.
## Skill Invocation Checklist
- [ ] /feature-dev loaded (run command or confirm)
- [ ] /koniec-pracy loaded (run command or confirm)
</skills>
<acceptance_criteria>
## AC-1: Przelacznik widocznosci custom labels dziala i jest zapamietywany
```gherkin
Given administrator jest na /admin/shop_product/view_list/
When kliknie przycisk "Pokaz etykiety niestandardowe"
Then stan opcji zostanie zapisany w sesji
And po odswiezeniu/listowaniu tabela zachowa ustawiony stan (wlaczony lub wylaczony)
```
## AC-2: Lista produktow pokazuje 5 pol custom_label po wlaczeniu opcji
```gherkin
Given opcja "Pokaz etykiety niestandardowe" jest wlaczona
When lista produktow sie renderuje
Then pod sekcja zdjecie/nazwa/SKU-EAN dla kazdego produktu widoczne sa pola custom_label_0..custom_label_4
And etykiety tych pol sa pobrane dynamicznie z bazy danych (z fallbackiem tylko gdy brak konfiguracji)
```
## AC-3: Zapis i podpowiedzi wartosci dzialaja dla kazdego custom_label
```gherkin
Given administrator wpisuje wartosc w jednym z pol custom_label_0..custom_label_4
When wybierze podpowiedz lub zatwierdzi wpis
Then wartosc zostanie zapisana w pp_shop_products dla danego produktu i pola
And podpowiedzi sa budowane z juz istniejacych wartosci tego samego custom_label w bazie
```
## AC-4: Walidacja i bezpieczenstwo endpointow sa zachowane
```gherkin
Given zapytanie AJAX podaje nieprawidlowy typ labela spoza custom_label_0..4
When backend przetwarza request
Then operacja zostaje odrzucona bez zapisu
And odpowiedz zwraca status bledu bez ingerencji w dane produktu
```
</acceptance_criteria>
<tasks>
<task type="auto">
<name>Task 1: Dodac backend przelacznika sesyjnego i danych dla widoku listy</name>
<files>autoload/admin/Controllers/ShopProductController.php, autoload/Domain/Product/ProductRepository.php</files>
<action>
Rozszerzyc `ShopProductController::view_list()` o flage sesyjna dla widocznosci custom labels
oraz przekazanie do widoku nazw etykiet pobieranych z bazy.
Dodac akcje kontrolera do przelaczania flagi (toggle) i zwracania prostego JSON.
W `ProductRepository` dodac metode pobierajaca nazwy etykiet custom_label_0..4 z bazy
(np. tabela ustawien), z bezpiecznym fallbackiem "Custom label N" gdy wartosc nie istnieje.
Nie stosowac konkatenacji SQL dla danych wejsciowych.
</action>
<verify>Uruchomic testy kontrolera/repo oraz sprawdzic recznie, ze zmiana flagi utrzymuje sie po reloadzie listy</verify>
<done>AC-1 i AC-2 satisfied</done>
</task>
<task type="auto">
<name>Task 2: Dodac UI i logike AJAX dla custom labels na liscie produktow</name>
<files>autoload/admin/Controllers/ShopProductController.php, admin/templates/shop-product/products-list-custom-script.php</files>
<action>
Wygenerowac HTML 5 pol custom_label pod kolumna nazwy produktu tylko gdy flaga sesyjna jest wlaczona.
Uzyc klas zgodnych z istniejacym stylem (`custom-labels`, `custom_label_X_container`, listy sugestii).
Dodac przycisk "Pokaz etykiety niestandardowe" obok "Dodaj produkt" (hook przez custom script listy)
oraz obsluge klikniecia przez AJAX do nowej akcji toggle + odswiezenie aktualnego URL.
Podlaczyc dla kazdego inputa:
- pobieranie sugestii przez `/admin/shop_product/product_custom_label_suggestions/`
- zapis przez `/admin/shop_product/product_custom_label_save/`
z walidacja odpowiedzi i obsluga bledow UI.
</action>
<verify>Manual: wlaczyc opcje, wpisac i zapisac wartosc custom_label, odswiezyc strone, potwierdzic widocznosc i dane</verify>
<done>AC-2 i AC-3 satisfied</done>
</task>
<task type="auto">
<name>Task 3: Dodac testy regresyjne dla nowego zachowania</name>
<files>tests/Unit/admin/Controllers/ShopProductControllerTest.php, tests/Unit/Domain/Product/ProductRepositoryTest.php</files>
<action>
Rozszerzyc testy kontrolera o przypadki:
- toggle flagi sesyjnej
- odrzucenie nieprawidlowych typow labeli
- poprawne przekazanie danych do widoku listy przy wlaczonej opcji.
Rozszerzyc testy repozytorium o:
- pobieranie nazw custom labels z bazy z fallbackiem
- sugestie i zapis tylko dla dozwolonych label_type.
</action>
<verify>./test.ps1 tests/Unit/admin/Controllers/ShopProductControllerTest.php oraz ./test.ps1 tests/Unit/Domain/Product/ProductRepositoryTest.php</verify>
<done>AC-4 covered and AC-1..AC-3 protected by tests</done>
</task>
<task type="checkpoint:human-verify" gate="blocking">
<what-built>Nowy przycisk sesyjny + szybka edycja custom labels 0..4 z podpowiedziami na liscie produktow</what-built>
<how-to-verify>
1. Otworz: /admin/shop_product/view_list/
2. Kliknij: "Pokaz etykiety niestandardowe"
3. Potwierdz: pola custom_label_0..4 pojawiaja sie pod nazwa produktu
4. Wpisz wartosc, wybierz podpowiedz i odswiez strone
5. Potwierdz: wartosc zostala zapisana i toggle pozostaje aktywny
</how-to-verify>
<resume-signal>Type "approved" to continue, or describe issues to fix</resume-signal>
</task>
</tasks>
<boundaries>
## DO NOT CHANGE
- Globalnych komponentow listy niezwiązanych z produktami (`admin/templates/components/table-list.php`) poza minimalnym, koniecznym hookiem
- Endpointow API (`autoload/api/*`)
- Logiki produktow frontendowych (`autoload/front/*`, `templates/shop-product/*`)
## SCOPE LIMITS
- Zakres ograniczony do admin listy produktow i quick-edit custom labels
- Bez migracji DB w tym planie (odczyt nazw z istniejacych danych konfiguracyjnych)
- Bez refaktoru calego modułu integracji Google XML
</boundaries>
<verification>
Before declaring plan complete:
- [ ] ./test.ps1 tests/Unit/admin/Controllers/ShopProductControllerTest.php
- [ ] ./test.ps1 tests/Unit/Domain/Product/ProductRepositoryTest.php
- [ ] Manual check: toggle zapisuje sie w sesji i zachowuje po reloadzie
- [ ] Manual check: podpowiedzi i zapis custom labels dzialaja dla 0..4
- [ ] All acceptance criteria met
</verification>
<success_criteria>
- Przycisk "Pokaz etykiety niestandardowe" dziala i przechowuje stan w sesji
- Lista produktow pokazuje i zapisuje custom_label_0..4 bez wejscia w edycje produktu
- Nazwy etykiet sa pobierane z bazy z fallbackiem
- Testy regresyjne dla backendu i repozytorium przechodza
</success_criteria>
<output>
After completion, create `.paul/phases/16-product-list-custom-labels/16-01-SUMMARY.md`
</output>

View File

@@ -0,0 +1,149 @@
---
phase: 16-product-list-custom-labels
plan: 01
subsystem: admin
tags: [shop-product, custom-label, session-toggle, autocomplete, quick-edit]
requires: []
provides:
- Szybka edycja custom_label_0..4 na liscie produktow
- Przelacznik widocznosci etykiet w sesji admina
- Podpowiedzi istniejacych wartosci + wpisywanie wartosci wlasnej w jednym polu
affects: [shop-product-list, google-xml-label-flow]
tech-stack:
added: []
patterns: [inline quick-edit in table list, datalist autocomplete in single input, settings fallback mapping]
key-files:
created: []
modified:
- autoload/admin/Controllers/ShopProductController.php
- autoload/Domain/Product/ProductRepository.php
- admin/templates/shop-product/products-list.php
- admin/templates/shop-product/products-list-custom-script.php
- tests/Unit/admin/Controllers/ShopProductControllerTest.php
- tests/Unit/Domain/Product/ProductRepositoryTest.php
key-decisions:
- "Przelacznik widocznosci custom labels zapisany w sesji (per admin session)"
- "Nazwy custom labels pobierane z pp_settings z fallbackiem do domyslnych nazw"
- "UX: jedno pole input z autocomplete (datalist), bez osobnego select pod spodem"
patterns-established:
- "Dla list admina: toggle funkcji przez dedykowany endpoint JSON + reload widoku"
- "Quick-edit text fields: walidacja typu po stronie kontrolera przed zapisem/sugestiami"
duration: ~120min
completed: 2026-04-19
---
# Phase 16 Plan 01: Product list custom labels quick edit - Summary
**Wdrozono szybka edycje custom labels na liscie produktow z przelacznikiem sesyjnym i autocomplete w pojedynczym polu.**
## Performance
| Metric | Value |
|--------|-------|
| Duration | ~120min |
| Completed | 2026-04-19 |
| Tasks | 3 completed + 1 checkpoint approved |
| Files modified | 6 |
## Acceptance Criteria Results
| Criterion | Status | Notes |
|-----------|--------|-------|
| AC-1: Przelacznik widocznosci custom labels dziala i jest zapamietywany | Pass | Dodano przycisk toggle + zapis stanu w sesji admina |
| AC-2: Lista pokazuje 5 pol custom_label po wlaczeniu opcji | Pass | Pola custom_label_0..4 renderowane pod nazwa/SKU/EAN tylko przy wlaczonej opcji |
| AC-3: Zapis i podpowiedzi wartosci dzialaja dla custom_label | Pass | Zapis AJAX do bazy + autocomplete istniejacych wartosci i mozliwosc wpisu wlasnego |
| AC-4: Walidacja i bezpieczenstwo endpointow zachowane | Pass | Kontroler odrzuca niedozwolone label_type przed zapisem i pobraniem sugestii |
## Accomplishments
- Dodano nowy toggle "Pokaz/Ukryj etykiety niestandardowe" przy liscie produktow, sterowany sesja.
- Dodano inline quick-edit custom_label_0..4 bez wchodzenia do edycji produktu.
- Podpowiedzi dzialaja jako autocomplete w tym samym input (nie osobny kontrolka pod polem).
- Ujednolicono UX przycisku toggle (kolorystyka, rozmiar, hover, czytelnosc).
- Rozszerzono testy kontrolera i repozytorium o nowe przypadki.
## Files Created/Modified
| File | Change | Purpose |
|------|--------|---------|
| `autoload/admin/Controllers/ShopProductController.php` | Modified | Toggle sesyjny, render custom labels w tabeli, walidacja label_type |
| `autoload/Domain/Product/ProductRepository.php` | Modified | Pobieranie nazw custom_label z ustawien (fallback) |
| `admin/templates/shop-product/products-list.php` | Modified | Przekazanie flagi custom_labels_enabled do skryptu |
| `admin/templates/shop-product/products-list-custom-script.php` | Modified | UI toggle, zapis/sugestie custom label, autocomplete, poprawki wygladu |
| `tests/Unit/admin/Controllers/ShopProductControllerTest.php` | Modified | Testy nowych metod i walidacji kontrolera |
| `tests/Unit/Domain/Product/ProductRepositoryTest.php` | Modified | Testy customLabelNames + walidacji sugestii/zapisu |
## Decisions Made
| Decision | Rationale | Impact |
|----------|-----------|--------|
| Session key dla toggla (`shop_product_show_custom_labels`) | Funkcja ma byc osobnym trybem widoku admina | Stabilny stan po odswiezeniu strony |
| Nazwy etykiet z `pp_settings` + fallback | Wymaganie "nazwy z bazy" i bezpieczne zachowanie gdy brak konfiguracji | Elastyczne nazewnictwo bez hardcodu |
| Autocomplete w jednym polu zamiast input + oddzielny select | UX feedback od usera podczas checkpointu | Czytelniejszy i szybszy flow edycji |
## Deviations from Plan
### Summary
| Type | Count | Impact |
|------|-------|--------|
| Auto-fixed | 1 | Niski, techniczny (BOM w pliku kontrolera) |
| Scope additions | 2 | Niski, UX polish po feedbacku checkpoint |
| Deferred | 0 | Brak |
**Total impact:** Niezbedne poprawki techniczne i UX bez scope creep funkcjonalnego.
### Auto-fixed Issues
**1. Encoding/BOM in controller file**
- **Found during:** Task 1 implementation verification
- **Issue:** Parser PHP zglaszal blad namespace przez BOM na poczatku pliku
- **Fix:** Zapisano plik `ShopProductController.php` jako UTF-8 bez BOM
- **Files:** `autoload/admin/Controllers/ShopProductController.php`
- **Verification:** `php -l autoload/admin/Controllers/ShopProductController.php`
### Deferred Items
None.
## Issues Encountered
| Issue | Resolution |
|-------|------------|
| Drobne regresje UX przycisku toggle | Iteracyjna poprawka stylu i hover po feedbacku usera |
| Forma podpowiedzi (select pod inputem) nieakceptowalna UX | Zmieniono na jedno pole z autocomplete (datalist) |
## Verification Results
- `php -l autoload/admin/Controllers/ShopProductController.php` -> OK
- `php -l autoload/Domain/Product/ProductRepository.php` -> OK
- `php -l admin/templates/shop-product/products-list-custom-script.php` -> OK
- `php phpunit.phar tests/Unit/admin/Controllers/ShopProductControllerTest.php` -> OK (15 tests, 71 assertions)
- `php phpunit.phar tests/Unit/Domain/Product/ProductRepositoryTest.php` -> OK (64 tests, 131 assertions)
- Checkpoint human-verify: approved by user after final UX adjustments
Skill audit:
- `/feature-dev` - not invoked (user-approved override)
- `/koniec-pracy` - acknowledged by user as available for end-of-session flow
## Next Phase Readiness
**Ready:**
- Admin ma szybki i praktyczny workflow uzupelniania custom labels bez przechodzenia do edycji produktu.
- Kod posiada testy regresyjne dla nowej logiki backendowej.
**Concerns:**
- Brak.
**Blockers:**
- None.
---
*Phase: 16-product-list-custom-labels, Plan: 01*
*Completed: 2026-04-19*

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*

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=77fcbbea-9d8f-45d6-86d7-b262e33f979e
ceTaskUrl=https://sonar.project-pro.pl/api/ce/task?id=77fcbbea-9d8f-45d6-86d7-b262e33f979e
ceTaskId=33e10a49-4790-400e-bf25-9b40eaed053e
ceTaskUrl=https://sonar.project-pro.pl/api/ce/task?id=33e10a49-4790-400e-bf25-9b40eaed053e

165
AGENTS.md
View File

@@ -1,4 +1,4 @@
# CLAUDE.md
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
@@ -7,19 +7,19 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
shopPRO is a PHP e-commerce platform with an admin panel and customer-facing storefront. It uses Medoo ORM (`$mdb`), Redis caching, and a Domain-Driven Design architecture with Dependency Injection (migration from legacy architecture complete).
## Zasady pisania kodu
- Kod ma być czytelny dla obcego: jasne nazwy, mało magii
- Brak „skrótów na szybko typu logika w widokach, copy-paste, losowe helpery bez spójności
- Każda funkcja/klasa ma mieć jedną odpowiedzialność, zwykle do 3050 linii (jeśli dłuższe dzielić)
- max 3 poziomy zagnieżdżeń (if/foreach), reszta do osobnych metod
- Kod ma być czytelny „dla obcego”: jasne nazwy, mało magii
- Brak „skrótów na szybko” typu logika w widokach, copy-paste, losowe helpery bez spójności
- KaĹĽda funkcja/klasa ma mieć jednÄ… odpowiedzialność, zwykle do 30–50 linii (jeĹ›li dĹuĹĽsze – dzielić)
- max 3 poziomy zagnieżdżeń (if/foreach), reszta do osobnych metod
- Nazewnictwo:
- klasy: PascalCase
- metody/zmienne: camelCase
- stałe: UPPER_SNAKE_CASE
- Zero skrótologii w nazwach (np. $d, $tmp, $x1) poza pętlami 23 linijki
- medoo + prepared statements bez wyjątków (żadnego sklejania SQL stringiem)
- stałe: UPPER_SNAKE_CASE
- Zero „skrótologii” w nazwach (np. $d, $tmp, $x1) poza pętlami 2–3 linijki
- medoo + prepared statements bez wyjÄ…tkĂłw (ĹĽadnego sklejania SQL stringiem)
- XSS: escape w widokach (np. helper e())
- CSRF dla formularzy, sensowna obsługa sesji
- Kod ma mieć komentarze tylko tam, gdzie wyjaśniają „dlaczego, nie „co”
- CSRF dla formularzy, sensowna obsługa sesji
- Kod ma mieć komentarze tylko tam, gdzie wyjaśniają „dlaczego”, nie „co”
## PHP Version Constraint
@@ -36,7 +36,7 @@ shopPRO is a PHP e-commerce platform with an admin panel and customer-facing sto
### Running Tests
```bash
# Full suite (recommended PowerShell, auto-finds php)
# Full suite (recommended — PowerShell, auto-finds php)
./test.ps1
# Specific file
@@ -61,50 +61,50 @@ See `docs/UPDATE_INSTRUCTIONS.md` for the full procedure. Updates are ZIP packag
### Directory Structure
```
shopPRO/
├── autoload/ # Autoloaded classes (core codebase)
│ ├── Domain/ # Business logic repositories (\Domain\)
│ ├── Shared/ # Shared utilities (\Shared\)
│ │ ├── Cache/ # CacheHandler, RedisConnection
│ │ ├── Email/ # Email (PHPMailer wrapper)
│ │ ├── Helpers/ # Helpers (formerly class.S.php)
│ │ ├── Html/ # Html utility
│ │ ├── Image/ # ImageManipulator
│ │ └── Tpl/ # Template engine
│ ├── api/ # REST API layer (\api\)
│ │ ├── ApiRouter.php # API router (\api\ApiRouter)
│ │ └── Controllers/ # API controllers (\api\Controllers\)
│ ├── admin/ # Admin panel layer
│ │ ├── App.php # Admin router (\admin\App)
│ │ ├── Controllers/ # DI controllers (\admin\Controllers\) 28 controllers
│ │ ├── Support/ # TableListRequestFactory, Forms/FormRequestHandler, Forms/FormFieldRenderer
│ │ ├── Validation/ # FormValidator
│ │ └── ViewModels/ # Forms/ (FormEditViewModel, FormField, FormTab, FormAction, FormFieldType), Common/ (PaginatedTableViewModel)
│ └── front/ # Frontend layer
├── App.php # Frontend router (\front\App)
├── LayoutEngine.php # Layout engine (\front\LayoutEngine)
├── Controllers/ # DI controllers (\front\Controllers\) 8 controllers
└── Views/ # Static views (\front\Views\) 11 view classes
├── admin/ # Admin panel
│ ├── templates/ # Admin view templates
│ └── layout/ # Admin CSS/JS/icons
├── templates/ # Frontend view templates
├── libraries/ # Third-party libraries (Medoo, RedBeanPHP, PHPMailer)
├── tests/ # PHPUnit tests
│ ├── bootstrap.php
│ ├── stubs/ # Test stubs (CacheHandler, Helpers, ShopProduct)
│ └── Unit/
├── Domain/ # Repository tests
├── admin/Controllers/ # Controller tests
└── api/ # API tests
├── updates/ # Update packages for clients
├── docs/ # Technical documentation
├── config.php # Database/Redis config (not in repo)
├── index.php # Frontend entry point
├── ajax.php # Frontend AJAX handler
├── admin/index.php # Admin entry point
├── admin/ajax.php # Admin AJAX handler
├── cron.php # CRON jobs (Apilo sync)
└── api.php # REST API (ordersPRO + Ekomi)
├── autoload/ # Autoloaded classes (core codebase)
│ ├── Domain/ # Business logic repositories (\Domain\)
│ ├── Shared/ # Shared utilities (\Shared\)
│ │ ├── Cache/ # CacheHandler, RedisConnection
│ │ ├── Email/ # Email (PHPMailer wrapper)
│ │ ├── Helpers/ # Helpers (formerly class.S.php)
│ │ ├── Html/ # Html utility
│ │ ├── Image/ # ImageManipulator
│ │ └── Tpl/ # Template engine
│ ├── api/ # REST API layer (\api\)
│ │ ├── ApiRouter.php # API router (\api\ApiRouter)
│ │ └── Controllers/ # API controllers (\api\Controllers\)
│ ├── admin/ # Admin panel layer
│ │ ├── App.php # Admin router (\admin\App)
│ │ ├── Controllers/ # DI controllers (\admin\Controllers\) — 28 controllers
│ │ ├── Support/ # TableListRequestFactory, Forms/FormRequestHandler, Forms/FormFieldRenderer
│ │ ├── Validation/ # FormValidator
│ │ └── ViewModels/ # Forms/ (FormEditViewModel, FormField, FormTab, FormAction, FormFieldType), Common/ (PaginatedTableViewModel)
│ └── front/ # Frontend layer
│ ├── App.php # Frontend router (\front\App)
│ ├── LayoutEngine.php # Layout engine (\front\LayoutEngine)
│ ├── Controllers/ # DI controllers (\front\Controllers\) — 8 controllers
│ └── Views/ # Static views (\front\Views\) — 11 view classes
├── admin/ # Admin panel
│ ├── templates/ # Admin view templates
│ └── layout/ # Admin CSS/JS/icons
├── templates/ # Frontend view templates
├── libraries/ # Third-party libraries (Medoo, RedBeanPHP, PHPMailer)
├── tests/ # PHPUnit tests
│ ├── bootstrap.php
│ ├── stubs/ # Test stubs (CacheHandler, Helpers, ShopProduct)
│ └── Unit/
│ ├── Domain/ # Repository tests
│ ├── admin/Controllers/ # Controller tests
│ └── api/ # API tests
├── updates/ # Update packages for clients
├── docs/ # Technical documentation
├── config.php # Database/Redis config (not in repo)
├── index.php # Frontend entry point
├── ajax.php # Frontend AJAX handler
├── admin/index.php # Admin entry point
├── admin/ajax.php # Admin AJAX handler
├── cron.php # CRON jobs (Apilo sync)
└── api.php # REST API (ordersPRO + Ekomi)
```
### Autoloader
@@ -114,19 +114,19 @@ Custom autoloader in each entry point (not Composer autoload at runtime). Tries
2. `autoload/{namespace}/{ClassName}.php` (PSR-4 style, fallback)
### Namespace Conventions (case-sensitive on Linux!)
- `\Domain\` `autoload/Domain/` (uppercase D)
- `\admin\Controllers\` `autoload/admin/Controllers/` (lowercase a)
- `\Shared\` `autoload/Shared/`
- `\api\` `autoload/api/`
- Do NOT use `\Admin\` (uppercase A) the server directory is `admin/` (lowercase)
- `\shop\` namespace is **deleted** all 12 legacy classes migrated to `\Domain\`, `autoload/shop/` directory removed
- `\Domain\` → `autoload/Domain/` (uppercase D)
- `\admin\Controllers\` → `autoload/admin/Controllers/` (lowercase a)
- `\Shared\` → `autoload/Shared/`
- `\api\` → `autoload/api/`
- Do NOT use `\Admin\` (uppercase A) — the server directory is `admin/` (lowercase)
- `\shop\` namespace is **deleted** — all 12 legacy classes migrated to `\Domain\`, `autoload/shop/` directory removed
### Domain-Driven Architecture (migration complete)
All legacy directories (`admin/controls/`, `admin/factory/`, `admin/view/`, `front/controls/`, `front/view/`, `front/factory/`, `shop/`) have been deleted. All modules now use this pattern:
**Domain Layer** (`autoload/Domain/{Module}/`):
- `{Module}Repository.php` data access, business logic, Redis caching
- `{Module}Repository.php` — data access, business logic, Redis caching
- Constructor DI with `$db` (Medoo instance)
- Methods serve both admin and frontend (shared Domain, no separate services)
@@ -141,7 +141,7 @@ All legacy directories (`admin/controls/`, `admin/factory/`, `admin/view/`, `fro
- Wired in `front\App::getControllerFactories()`
**Frontend Views** (`autoload/front/Views/`):
- Static classes, no state, no DI pure rendering
- Static classes, no state, no DI — pure rendering
**API Controllers** (`autoload/api/Controllers/`):
- DI via constructor, stateless (no session)
@@ -151,13 +151,13 @@ All legacy directories (`admin/controls/`, `admin/factory/`, `admin/view/`, `fro
### Key Classes
| Class | Purpose |
|-------|---------|
| `\admin\App` | Admin router maps URL segments to controllers |
| `\front\App` | Frontend router `route()`, `checkUrlParams()` |
| `\front\LayoutEngine` | Frontend layout engine `show()`, tag replacement |
| `\admin\App` | Admin router — maps URL segments to controllers |
| `\front\App` | Frontend router — `route()`, `checkUrlParams()` |
| `\front\LayoutEngine` | Frontend layout engine — `show()`, tag replacement |
| `\Shared\Helpers\Helpers` | Utility methods (SEO, email, cache clearing) |
| `\Shared\Tpl\Tpl` | Template engine `render()`, `set()` |
| `\Shared\Cache\CacheHandler` | Redis cache `get()`, `set()`, `delete()`, `deletePattern()` |
| `\api\ApiRouter` | REST API router auth, routing, response helpers |
| `\Shared\Tpl\Tpl` | Template engine — `render()`, `set()` |
| `\Shared\Cache\CacheHandler` | Redis cache — `get()`, `set()`, `delete()`, `deletePattern()` |
| `\api\ApiRouter` | REST API router — auth, routing, response helpers |
### Database
- ORM: Medoo (`$mdb` global variable, injected via DI in new code)
@@ -179,7 +179,7 @@ Universal form system for admin edit views. Docs: `docs/FORM_EDIT_SYSTEM.md`.
- Clear product cache: `\Shared\Helpers\Helpers::clear_product_cache($id)`
- Pattern delete: `CacheHandler::deletePattern("shop\\product:{$id}:*")`
- Default TTL: 86400 (24h)
- Data is serialized requires `unserialize()` after `get()`
- Data is serialized — requires `unserialize()` after `get()`
- Config: `config.php` (`$config['redis']`)
## Code Patterns
@@ -203,7 +203,7 @@ $controller = new \admin\Controllers\ExampleController($repo);
```
### Medoo ORM pitfalls
- `$mdb->delete($table, $where)` takes **2 arguments**, NOT 3 has caused bugs
- `$mdb->delete($table, $where)` takes **2 arguments**, NOT 3 — has caused bugs
- `$mdb->get()` returns `null` when no record, NOT `false`
- After `$mdb->insert()`, check `$mdb->id()` to confirm success
@@ -222,18 +222,19 @@ $controller = new \admin\Controllers\ExampleController($repo);
When user says **"KONIEC PRACY"**, run `/koniec-pracy` (see `.claude/commands/koniec-pracy.md`).
Before starting implementation, review current state of docs.
For documentation updates, modify only .paul/docs/* unless the user explicitly asks to update root docs/*.
## Key Documentation
- `docs/MEMORY.md` project memory: known issues, confirmed patterns, ORM pitfalls, caching conventions
- `docs/PROJECT_STRUCTURE.md` current architecture, layers, cache, entry points, integrations
- `docs/DATABASE_STRUCTURE.md` full database schema
- `docs/TESTING.md` test suite guide and structure
- `docs/FORM_EDIT_SYSTEM.md` form system architecture
- `docs/CHANGELOG.md` version history
- `api-docs/api-reference.json` REST API documentation (ordersPRO)
- `api-docs/index.html` REST API documentation (ordersPRO)
- `docs/UPDATE_INSTRUCTIONS.md` how to build client update packages
- `docs/MEMORY.md` — project memory: known issues, confirmed patterns, ORM pitfalls, caching conventions
- `docs/PROJECT_STRUCTURE.md` — current architecture, layers, cache, entry points, integrations
- `docs/DATABASE_STRUCTURE.md` — full database schema
- `docs/TESTING.md` — test suite guide and structure
- `docs/FORM_EDIT_SYSTEM.md` — form system architecture
- `docs/CHANGELOG.md` — version history
- `api-docs/api-reference.json` — REST API documentation (ordersPRO)
- `api-docs/index.html` — REST API documentation (ordersPRO)
- `docs/UPDATE_INSTRUCTIONS.md` — how to build client update packages
## Za każdym razem jak próbujesz sprawdzić jakiś plik z logami spróbuj go najpierw pobrać z serwera FTP
## Za każdym razem jak próbujesz sprawdzić jakiś plik z logami spróbuj go najpierw pobrać z serwera FTP
## Wszystkie pliki które tworzysz jako pomocnicze, np build_0330.ps1 czy build-update.ps1 twórz w folderze temp
## Wszystkie pliki ktĂłre tworzysz jako pomocnicze, np build_0330.ps1 czy build-update.ps1 twĂłrz w folderze temp

176
CLAUDE.md
View File

@@ -1,4 +1,4 @@
# CLAUDE.md
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
@@ -7,19 +7,19 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
shopPRO is a PHP e-commerce platform with an admin panel and customer-facing storefront. It uses Medoo ORM (`$mdb`), Redis caching, and a Domain-Driven Design architecture with Dependency Injection (migration from legacy architecture complete).
## Zasady pisania kodu
- Kod ma być czytelny dla obcego: jasne nazwy, mało magii
- Brak „skrótów na szybko typu logika w widokach, copy-paste, losowe helpery bez spójności
- Każda funkcja/klasa ma mieć jedną odpowiedzialność, zwykle do 3050 linii (jeśli dłuższe dzielić)
- max 3 poziomy zagnieżdżeń (if/foreach), reszta do osobnych metod
- Kod ma być czytelny „dla obcego”: jasne nazwy, mało magii
- Brak „skrótów na szybko” typu logika w widokach, copy-paste, losowe helpery bez spójności
- KaĹĽda funkcja/klasa ma mieć jednÄ… odpowiedzialność, zwykle do 30–50 linii (jeĹ›li dĹuĹĽsze – dzielić)
- max 3 poziomy zagnieżdżeń (if/foreach), reszta do osobnych metod
- Nazewnictwo:
- klasy: PascalCase
- metody/zmienne: camelCase
- stałe: UPPER_SNAKE_CASE
- Zero skrótologii w nazwach (np. $d, $tmp, $x1) poza pętlami 23 linijki
- medoo + prepared statements bez wyjątków (żadnego sklejania SQL stringiem)
- stałe: UPPER_SNAKE_CASE
- Zero „skrótologii” w nazwach (np. $d, $tmp, $x1) poza pętlami 2–3 linijki
- medoo + prepared statements bez wyjÄ…tkĂłw (ĹĽadnego sklejania SQL stringiem)
- XSS: escape w widokach (np. helper e())
- CSRF dla formularzy, sensowna obsługa sesji
- Kod ma mieć komentarze tylko tam, gdzie wyjaśniają „dlaczego, nie „co”
- CSRF dla formularzy, sensowna obsługa sesji
- Kod ma mieć komentarze tylko tam, gdzie wyjaśniają „dlaczego”, nie „co”
## PHP Version Constraint
@@ -36,7 +36,7 @@ shopPRO is a PHP e-commerce platform with an admin panel and customer-facing sto
### Running Tests
```bash
# Full suite (recommended PowerShell, auto-finds php)
# Full suite (recommended — PowerShell, auto-finds php)
./test.ps1
# Specific file
@@ -55,60 +55,60 @@ composer test # standard
PHPUnit 9.6 via `phpunit.phar`. Bootstrap: `tests/bootstrap.php`. Config: `phpunit.xml`.
Current suite: **821 tests, 2278 assertions**.
Current suite: **823 tests, 2284 assertions**.
### Creating Updates
See `docs/UPDATE_INSTRUCTIONS.md` for the full procedure. Updates are ZIP packages in `updates/0.XX/`. Never include `*.md` files, `updates/changelog.php`, or root `.htaccess` in update ZIPs. ZIP structure must start directly from project directories no version subfolder inside the archive.
See `docs/UPDATE_INSTRUCTIONS.md` for the full procedure. Updates are ZIP packages in `updates/0.XX/`. Never include `*.md` files, `updates/changelog.php`, or root `.htaccess` in update ZIPs. ZIP structure must start directly from project directories — no version subfolder inside the archive.
## Architecture
### Directory Structure
```
shopPRO/
├── autoload/ # Autoloaded classes (core codebase)
│ ├── Domain/ # Business logic repositories (\Domain\)
│ ├── Shared/ # Shared utilities (\Shared\)
│ │ ├── Cache/ # CacheHandler, RedisConnection
│ │ ├── Email/ # Email (PHPMailer wrapper)
│ │ ├── Helpers/ # Helpers (formerly class.S.php)
│ │ ├── Html/ # Html utility
│ │ ├── Image/ # ImageManipulator
│ │ └── Tpl/ # Template engine
│ ├── api/ # REST API layer (\api\)
│ │ ├── ApiRouter.php # API router (\api\ApiRouter)
│ │ └── Controllers/ # API controllers (\api\Controllers\)
│ ├── admin/ # Admin panel layer
│ │ ├── App.php # Admin router (\admin\App)
│ │ ├── Controllers/ # DI controllers (\admin\Controllers\) 28 controllers
│ │ ├── Support/ # TableListRequestFactory, Forms/FormRequestHandler, Forms/FormFieldRenderer
│ │ ├── Validation/ # FormValidator
│ │ └── ViewModels/ # Forms/ (FormEditViewModel, FormField, FormTab, FormAction, FormFieldType), Common/ (PaginatedTableViewModel)
│ └── front/ # Frontend layer
├── App.php # Frontend router (\front\App)
├── LayoutEngine.php # Layout engine (\front\LayoutEngine)
├── Controllers/ # DI controllers (\front\Controllers\) 8 controllers
└── Views/ # Static views (\front\Views\) 11 view classes
├── admin/ # Admin panel
│ ├── templates/ # Admin view templates
│ └── layout/ # Admin CSS/JS/icons
├── templates/ # Frontend view templates
├── libraries/ # Third-party libraries (Medoo, RedBeanPHP, PHPMailer)
├── tests/ # PHPUnit tests
│ ├── bootstrap.php
│ ├── stubs/ # Test stubs (CacheHandler, Helpers, ShopProduct)
│ └── Unit/
├── Domain/ # Repository tests
├── admin/Controllers/ # Controller tests
└── api/ # API tests
├── updates/ # Update packages for clients
├── docs/ # Technical documentation
├── config.php # Database/Redis config (not in repo)
├── index.php # Frontend entry point
├── ajax.php # Frontend AJAX handler
├── admin/index.php # Admin entry point
├── admin/ajax.php # Admin AJAX handler
├── cron.php # CRON jobs (Apilo sync)
└── api.php # REST API (ordersPRO + Ekomi)
├── autoload/ # Autoloaded classes (core codebase)
│ ├── Domain/ # Business logic repositories (\Domain\)
│ ├── Shared/ # Shared utilities (\Shared\)
│ │ ├── Cache/ # CacheHandler, RedisConnection
│ │ ├── Email/ # Email (PHPMailer wrapper)
│ │ ├── Helpers/ # Helpers (formerly class.S.php)
│ │ ├── Html/ # Html utility
│ │ ├── Image/ # ImageManipulator
│ │ └── Tpl/ # Template engine
│ ├── api/ # REST API layer (\api\)
│ │ ├── ApiRouter.php # API router (\api\ApiRouter)
│ │ └── Controllers/ # API controllers (\api\Controllers\)
│ ├── admin/ # Admin panel layer
│ │ ├── App.php # Admin router (\admin\App)
│ │ ├── Controllers/ # DI controllers (\admin\Controllers\) — 28 controllers
│ │ ├── Support/ # TableListRequestFactory, Forms/FormRequestHandler, Forms/FormFieldRenderer
│ │ ├── Validation/ # FormValidator
│ │ └── ViewModels/ # Forms/ (FormEditViewModel, FormField, FormTab, FormAction, FormFieldType), Common/ (PaginatedTableViewModel)
│ └── front/ # Frontend layer
│ ├── App.php # Frontend router (\front\App)
│ ├── LayoutEngine.php # Layout engine (\front\LayoutEngine)
│ ├── Controllers/ # DI controllers (\front\Controllers\) — 8 controllers
│ └── Views/ # Static views (\front\Views\) — 11 view classes
├── admin/ # Admin panel
│ ├── templates/ # Admin view templates
│ └── layout/ # Admin CSS/JS/icons
├── templates/ # Frontend view templates
├── libraries/ # Third-party libraries (Medoo, RedBeanPHP, PHPMailer)
├── tests/ # PHPUnit tests
│ ├── bootstrap.php
│ ├── stubs/ # Test stubs (CacheHandler, Helpers, ShopProduct)
│ └── Unit/
│ ├── Domain/ # Repository tests
│ ├── admin/Controllers/ # Controller tests
│ └── api/ # API tests
├── updates/ # Update packages for clients
├── docs/ # Technical documentation
├── config.php # Database/Redis config (not in repo)
├── index.php # Frontend entry point
├── ajax.php # Frontend AJAX handler
├── admin/index.php # Admin entry point
├── admin/ajax.php # Admin AJAX handler
├── cron.php # CRON jobs (Apilo sync)
└── api.php # REST API (ordersPRO + Ekomi)
```
### Autoloader
@@ -118,19 +118,19 @@ Custom autoloader in each entry point (not Composer autoload at runtime). Tries
2. `autoload/{namespace}/{ClassName}.php` (PSR-4 style, fallback)
### Namespace Conventions (case-sensitive on Linux!)
- `\Domain\` `autoload/Domain/` (uppercase D)
- `\admin\Controllers\` `autoload/admin/Controllers/` (lowercase a)
- `\Shared\` `autoload/Shared/`
- `\api\` `autoload/api/`
- Do NOT use `\Admin\` (uppercase A) the server directory is `admin/` (lowercase)
- `\shop\` namespace is **deleted** all 12 legacy classes migrated to `\Domain\`, `autoload/shop/` directory removed
- `\Domain\` → `autoload/Domain/` (uppercase D)
- `\admin\Controllers\` → `autoload/admin/Controllers/` (lowercase a)
- `\Shared\` → `autoload/Shared/`
- `\api\` → `autoload/api/`
- Do NOT use `\Admin\` (uppercase A) — the server directory is `admin/` (lowercase)
- `\shop\` namespace is **deleted** — all 12 legacy classes migrated to `\Domain\`, `autoload/shop/` directory removed
### Domain-Driven Architecture (migration complete)
All legacy directories (`admin/controls/`, `admin/factory/`, `admin/view/`, `front/controls/`, `front/view/`, `front/factory/`, `shop/`) have been deleted. All modules now use this pattern:
**Domain Layer** (`autoload/Domain/{Module}/`):
- `{Module}Repository.php` data access, business logic, Redis caching
- `{Module}Repository.php` — data access, business logic, Redis caching
- Constructor DI with `$db` (Medoo instance)
- Methods serve both admin and frontend (shared Domain, no separate services)
@@ -145,7 +145,7 @@ All legacy directories (`admin/controls/`, `admin/factory/`, `admin/view/`, `fro
- Wired in `front\App::getControllerFactories()`
**Frontend Views** (`autoload/front/Views/`):
- Static classes, no state, no DI pure rendering
- Static classes, no state, no DI — pure rendering
**API Controllers** (`autoload/api/Controllers/`):
- DI via constructor, stateless (no session)
@@ -155,13 +155,13 @@ All legacy directories (`admin/controls/`, `admin/factory/`, `admin/view/`, `fro
### Key Classes
| Class | Purpose |
|-------|---------|
| `\admin\App` | Admin router maps URL segments to controllers |
| `\front\App` | Frontend router `route()`, `checkUrlParams()` |
| `\front\LayoutEngine` | Frontend layout engine `show()`, tag replacement |
| `\admin\App` | Admin router — maps URL segments to controllers |
| `\front\App` | Frontend router — `route()`, `checkUrlParams()` |
| `\front\LayoutEngine` | Frontend layout engine — `show()`, tag replacement |
| `\Shared\Helpers\Helpers` | Utility methods (SEO, email, cache clearing) |
| `\Shared\Tpl\Tpl` | Template engine `render()`, `set()` |
| `\Shared\Cache\CacheHandler` | Redis cache `get()`, `set()`, `delete()`, `deletePattern()` |
| `\api\ApiRouter` | REST API router auth, routing, response helpers |
| `\Shared\Tpl\Tpl` | Template engine — `render()`, `set()` |
| `\Shared\Cache\CacheHandler` | Redis cache — `get()`, `set()`, `delete()`, `deletePattern()` |
| `\api\ApiRouter` | REST API router — auth, routing, response helpers |
### Database
- ORM: Medoo (`$mdb` global variable, injected via DI in new code)
@@ -183,7 +183,7 @@ Universal form system for admin edit views. Docs: `docs/FORM_EDIT_SYSTEM.md`.
- Clear product cache: `\Shared\Helpers\Helpers::clear_product_cache($id)`
- Pattern delete: `CacheHandler::deletePattern("shop\\product:{$id}:*")`
- Default TTL: 86400 (24h)
- Data is serialized requires `unserialize()` after `get()`
- Data is serialized — requires `unserialize()` after `get()`
- Config: `config.php` (`$config['redis']`)
## Code Patterns
@@ -207,7 +207,7 @@ $controller = new \admin\Controllers\ExampleController($repo);
```
### Medoo ORM pitfalls
- `$mdb->delete($table, $where)` takes **2 arguments**, NOT 3 has caused bugs
- `$mdb->delete($table, $where)` takes **2 arguments**, NOT 3 — has caused bugs
- `$mdb->get()` returns `null` when no record, NOT `false`
- After `$mdb->insert()`, check `$mdb->id()` to confirm success
@@ -226,21 +226,23 @@ $controller = new \admin\Controllers\ExampleController($repo);
When user says **"KONIEC PRACY"**, run `/koniec-pracy` (see `.claude/commands/koniec-pracy.md`).
Before starting implementation, review current state of docs.
For documentation updates, modify only .paul/docs/* unless the user explicitly asks to update root docs/*.
## Key Documentation
- `docs/MEMORY.md` project memory: known issues, confirmed patterns, ORM pitfalls, caching conventions
- `docs/PROJECT_STRUCTURE.md` current architecture, layers, cache, entry points, integrations
- `docs/DATABASE_STRUCTURE.md` full database schema
- `docs/TESTING.md` test suite guide and structure
- `docs/FORM_EDIT_SYSTEM.md` form system architecture
- `docs/CLASS_CATALOG.md` full catalog of all classes with descriptions
- `docs/TODO.md` outstanding tasks and planned features
- `docs/CRON_QUEUE_PLAN.md` planned cron/queue architecture
- `docs/CHANGELOG.md` version history
- `api-docs/api-reference.json` REST API documentation (ordersPRO)
- `api-docs/index.html` REST API documentation (ordersPRO)
- `docs/UPDATE_INSTRUCTIONS.md` how to build client update packages
- `docs/MEMORY.md` — project memory: known issues, confirmed patterns, ORM pitfalls, caching conventions
- `docs/PROJECT_STRUCTURE.md` — current architecture, layers, cache, entry points, integrations
- `docs/DATABASE_STRUCTURE.md` — full database schema
- `docs/TESTING.md` — test suite guide and structure
- `docs/FORM_EDIT_SYSTEM.md` — form system architecture
- `docs/CLASS_CATALOG.md` — full catalog of all classes with descriptions
- `docs/TODO.md` — outstanding tasks and planned features
- `docs/CRON_QUEUE_PLAN.md` — planned cron/queue architecture
- `docs/CHANGELOG.md` — version history
- `api-docs/api-reference.json` — REST API documentation (ordersPRO)
- `api-docs/index.html` — REST API documentation (ordersPRO)
- `docs/UPDATE_INSTRUCTIONS.md` — how to build client update packages
## Za każdym razem jak próbujesz sprawdzić jakiś plik z logami spróbuj go najpierw pobrać z serwera FTP
## Za każdym razem jak próbujesz sprawdzić jakiś plik z logami spróbuj go najpierw pobrać z serwera FTP
## Wszystkie pliki ktĂłre tworzysz jako pomocnicze, np build_0330.ps1 czy build-update.ps1 twĂłrz w folderze temp
## Wszystkie pliki które tworzysz jako pomocnicze, np build_0330.ps1 czy build-update.ps1 twórz w folderze temp

View File

@@ -1,3 +1,20 @@
<?php $customLabelsEnabled = !empty( $this->custom_labels_enabled ); ?>
<script type="text/javascript">
$(function() {
var $header = $( '.panel-heading .col-sm-8' );
if ( !$header.length ) {
return;
}
if ( $header.find( '.btn-toggle-custom-labels' ).length === 0 ) {
var buttonClass = <?= $customLabelsEnabled ? "'btn-danger'" : "'btn-success'" ?>;
var buttonText = <?= $customLabelsEnabled ? "'Ukryj etykiety niestandardowe'" : "'Pokaż etykiety niestandardowe'" ?>;
$header.append( ' <a href=\"#\" class=\"btn btn-sm btn-toggle-custom-labels ' + buttonClass + '\"><i class=\"fa fa-tags mr5\"></i>' + buttonText + '</a>' );
}
});
</script>
<? if ( $this -> shoppro_enabled ):?>
<script type="text/javascript">
$(function() {
@@ -27,10 +44,19 @@
.product-categories {
display: block;
}
.custom-label-suggestions {
max-height: 220px;
overflow-y: auto;
}
.custom-labels small {
display: block;
margin-bottom: 3px;
}
</style>
<script type="text/javascript">
$(function() {
var customLabelSuggestionTimers = {};
// --- Inline price save ---
$( 'body' ).on( 'change', '.product-price', function() {
@@ -54,6 +80,139 @@ $(function() {
});
});
// --- Toggle custom labels ---
$( 'body' ).on( 'click', '.btn-toggle-custom-labels', function(e) {
e.preventDefault();
$.ajax({
type: 'POST',
cache: false,
url: '/admin/shop_product/product_custom_labels_toggle/',
beforeSend: function() { $( '#overlay' ).show(); },
success: function( response ) {
$( '#overlay' ).hide();
var data = jQuery.parseJSON( response );
if ( data.status === 'ok' ) {
window.location.reload();
return;
}
create_error( 'Nie udało się przełączyć widoku etykiet.' );
},
error: function() {
$( '#overlay' ).hide();
create_error( 'Nie udało się przełączyć widoku etykiet.' );
}
});
});
// --- Custom label suggestions + save ---
function hideCustomLabelSuggestions( $input ) {
var $container = $input.closest( '[class*=\"custom_label_\"]' );
$container.find( '.custom-label-suggestions' ).hide().empty();
var datalistId = $input.attr( 'data-datalist-id' ) || '';
if ( datalistId ) {
$( '#' + datalistId ).empty();
}
}
function saveCustomLabelValue( $input ) {
$.ajax({
type: 'POST',
cache: false,
url: '/admin/shop_product/product_custom_label_save/',
data: {
product_id: $input.attr( 'data-product-id' ),
label_type: $input.attr( 'data-label-type' ),
custom_label: $input.val()
},
success: function( response ) {
var data = jQuery.parseJSON( response );
if ( data.status !== 'ok' ) {
create_error( data.msg || 'Nie udało się zapisać etykiety.' );
}
},
error: function() {
create_error( 'Nie udało się zapisać etykiety.' );
}
});
}
function loadCustomLabelSuggestions( $input ) {
var labelType = $input.attr( 'data-label-type' );
var query = $input.val();
var $suggestions = $input.closest( '[class*=\"custom_label_\"]' ).find( '.custom-label-suggestions' );
var datalistId = $input.attr( 'data-datalist-id' ) || '';
var $datalist = datalistId ? $( '#' + datalistId ) : $();
if ( !labelType ) {
return;
}
$.ajax({
type: 'POST',
cache: false,
url: '/admin/shop_product/product_custom_label_suggestions/',
data: {
label_type: labelType,
custom_label: query
},
success: function( response ) {
var data = jQuery.parseJSON( response );
if ( data.status !== 'ok' || !data.suggestions || !data.suggestions.length ) {
$suggestions.hide().empty();
$datalist.empty();
return;
}
var html = '';
$.each( data.suggestions, function( index, item ) {
if ( !item.label ) {
return;
}
var safe = String( item.label ).replace(/\"/g, '&quot;');
html += '<option value=\"' + safe + '\">' + item.label + '</option>';
});
if ( html ) {
$datalist.html( html );
$suggestions.hide().empty();
} else {
$suggestions.hide().empty();
$datalist.empty();
}
},
error: function() {
$suggestions.hide().empty();
$datalist.empty();
}
});
}
$( 'body' ).on( 'input', '.product-custom-label', function() {
var $input = $( this );
var inputKey = $input.attr( 'data-product-id' ) + ':' + $input.attr( 'data-label-type' );
if ( customLabelSuggestionTimers[inputKey] ) {
clearTimeout( customLabelSuggestionTimers[inputKey] );
}
customLabelSuggestionTimers[inputKey] = setTimeout( function() {
loadCustomLabelSuggestions( $input );
}, 250 );
});
$( 'body' ).on( 'change', '.product-custom-label', function() {
saveCustomLabelValue( $( this ) );
});
$( document ).on( 'click', function(e) {
if ( $( e.target ).closest( '.custom-labels' ).length === 0 ) {
$( '.custom-label-suggestions' ).hide().empty();
}
});
$( 'body' ).on( 'change', '.product-price-promo', function() {
var $el = $( this );
var price = $el.val().replace( ' ', '' );

View File

@@ -5,5 +5,6 @@
'list' => $this->viewModel,
'apilo_enabled' => $this->apilo_enabled,
'shoppro_enabled' => $this->shoppro_enabled,
'custom_labels_enabled' => $this->custom_labels_enabled,
]); ?>
<?php endif; ?>

View File

@@ -2205,6 +2205,44 @@ class ProductRepository
] );
}
/**
* Pobiera nazwy etykiet custom_label_0..4 z bazy ustawien.
*
* @return array<string, string>
*/
public function customLabelNames(): array
{
$names = [];
for ( $index = 0; $index < 5; $index++ ) {
$fieldName = 'custom_label_' . $index;
$names[$fieldName] = 'Custom label ' . $index;
}
$settingsKeys = [];
for ( $index = 0; $index < 5; $index++ ) {
$settingsKeys[] = 'custom_label_' . $index . '_name';
$settingsKeys[] = 'google_custom_label_' . $index . '_name';
}
$settingsRows = $this->db->select( 'pp_settings', [ 'param', 'value' ], [ 'param' => $settingsKeys ] );
if ( is_array( $settingsRows ) ) {
foreach ( $settingsRows as $settingRow ) {
$param = (string) ( $settingRow['param'] ?? '' );
$value = trim( (string) ( $settingRow['value'] ?? '' ) );
if ( $value === '' ) {
continue;
}
if ( preg_match( '/^(?:google_)?custom_label_([0-4])_name$/', $param, $match ) ) {
$names[ 'custom_label_' . $match[1] ] = $value;
}
}
}
return $names;
}
/**
* Pobiera sugestie custom label.
*/
@@ -2331,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

@@ -184,8 +184,16 @@ class ScontainersController
}
$data = $result['data'];
$containerId = (int)($data['id'] ?? 0);
if ($containerId <= 0) {
$routeId = (int)\Shared\Helpers\Helpers::get('id');
if ($routeId > 0) {
$containerId = $routeId;
}
}
$savedId = $this->repository->save([
'id' => (int)($data['id'] ?? 0),
'id' => $containerId,
'status' => $data['status'] ?? 0,
'show_title' => $data['show_title'] ?? 0,
'translations' => $data['translations'] ?? [],
@@ -240,7 +248,6 @@ class ScontainersController
];
$fields = [
FormField::hidden('id', $id),
FormField::langSection('translations', 'content', [
FormField::text('title', [
'label' => 'Tytul',
@@ -283,7 +290,7 @@ class ScontainersController
$actionUrl,
'/admin/scontainers/list/',
true,
[],
['id' => $id],
$languages,
$errors
);

View File

@@ -18,6 +18,8 @@ use admin\Support\TableListRequestFactory;
*/
class ShopProductController
{
private const CUSTOM_LABELS_SESSION_KEY = 'shop_product_show_custom_labels';
private ProductRepository $repository;
private IntegrationsRepository $integrationsRepository;
private LanguagesRepository $languagesRepository;
@@ -39,6 +41,8 @@ class ShopProductController
$apiloEnabled = $this->integrationsRepository->getSetting( 'apilo', 'enabled' );
$shopproEnabled = $this->integrationsRepository->getSetting( 'shoppro', 'enabled' );
$dlang = $this->languagesRepository->defaultLanguage();
$customLabelsEnabled = $this->customLabelsEnabled();
$customLabelNames = $this->repository->customLabelNames();
$sortableColumns = [ 'id', 'name', 'price_brutto', 'status', 'promoted', 'quantity' ];
@@ -98,6 +102,10 @@ class ShopProductController
. '<small class="text-muted product-categories product-categories--cats" title="' . $categories . '">' . $categories . '</small>'
. '<small class="text-muted product-categories">SKU: ' . $sku . ', EAN: ' . $ean . '</small>';
if ( $customLabelsEnabled ) {
$nameHtml .= $this->renderCustomLabelsEditor( $product, $id, $customLabelNames );
}
$priceHtml = '<input type="text" class="product-price form-control text-right" product-id="' . $id . '" value="' . htmlspecialchars( (string) $product['price_brutto'], ENT_QUOTES, 'UTF-8' ) . '" style="width: 75px;">';
$promoHtml = '<input type="text" class="product-price-promo form-control text-right" product-id="' . $id . '" value="' . htmlspecialchars( (string) $product['price_brutto_promo'], ENT_QUOTES, 'UTF-8' ) . '" style="width: 75px;">';
$promotedHtml = $product['promoted'] ? '<span class="text-success text-bold">tak</span>' : 'nie';
@@ -195,11 +203,25 @@ class ShopProductController
'viewModel' => $viewModel,
'apilo_enabled' => $apiloEnabled,
'shoppro_enabled' => $shopproEnabled,
'custom_labels_enabled' => $customLabelsEnabled,
] );
}
// ─── Krok 7: Edycja i zapis ─────────────────────────────────────
/**
* AJAX: przelacza widok custom labels na liscie produktow i zapisuje stan w sesji.
*/
public function product_custom_labels_toggle(): void
{
$currentState = $this->customLabelsEnabled();
$newState = $currentState ? 0 : 1;
\Shared\Helpers\Helpers::set_session( self::CUSTOM_LABELS_SESSION_KEY, $newState );
echo json_encode( [ 'status' => 'ok', 'enabled' => (bool) $newState ] );
exit;
}
/**
* Formularz edycji produktu.
*/
@@ -897,9 +919,15 @@ class ShopProductController
public function product_custom_label_suggestions(): void
{
$response = [ 'status' => 'error', 'msg' => 'Podczas pobierania sugestii dla custom label wystąpił błąd. Proszę spróbować ponownie.' ];
$labelType = (string) \Shared\Helpers\Helpers::get( 'label_type' );
$suggestions = $this->repository->customLabelSuggestions( \Shared\Helpers\Helpers::get( 'custom_label' ), \Shared\Helpers\Helpers::get( 'label_type' ) );
if ( $suggestions ) {
if ( !$this->isAllowedCustomLabelType( $labelType ) ) {
echo json_encode( $response );
exit;
}
$suggestions = $this->repository->customLabelSuggestions( (string) \Shared\Helpers\Helpers::get( 'custom_label' ), $labelType );
if ( is_array( $suggestions ) ) {
$response = [ 'status' => 'ok', 'suggestions' => $suggestions ];
}
@@ -913,8 +941,14 @@ class ShopProductController
public function product_custom_label_save(): void
{
$response = [ 'status' => 'error', 'msg' => 'Podczas zapisywania custom label wystąpił błąd. Proszę spróbować ponownie.' ];
$labelType = (string) \Shared\Helpers\Helpers::get( 'label_type' );
if ( $this->repository->saveCustomLabel( (int) \Shared\Helpers\Helpers::get( 'product_id' ), \Shared\Helpers\Helpers::get( 'custom_label' ), \Shared\Helpers\Helpers::get( 'label_type' ) ) ) {
if ( !$this->isAllowedCustomLabelType( $labelType ) ) {
echo json_encode( $response );
exit;
}
if ( $this->repository->saveCustomLabel( (int) \Shared\Helpers\Helpers::get( 'product_id' ), (string) \Shared\Helpers\Helpers::get( 'custom_label' ), $labelType ) ) {
$response = [ 'status' => 'ok' ];
}
@@ -1197,4 +1231,36 @@ class ShopProductController
echo json_encode( [ 'status' => 'ok', 'products' => $products ] );
exit;
}
private function customLabelsEnabled(): bool
{
return isset( $_SESSION[ self::CUSTOM_LABELS_SESSION_KEY ] ) && (int) $_SESSION[ self::CUSTOM_LABELS_SESSION_KEY ] === 1;
}
private function isAllowedCustomLabelType(string $labelType): bool
{
return in_array( $labelType, [ 'custom_label_0', 'custom_label_1', 'custom_label_2', 'custom_label_3', 'custom_label_4' ], true );
}
private function renderCustomLabelsEditor(array $product, int $productId, array $customLabelNames): string
{
$customLabelsHtml = '<div class="custom-labels mt10">';
for ( $index = 0; $index < 5; $index++ ) {
$fieldName = 'custom_label_' . $index;
$labelText = htmlspecialchars( (string) ( $customLabelNames[$fieldName] ?? 'Custom label ' . $index ), ENT_QUOTES, 'UTF-8' );
$valueText = htmlspecialchars( (string) ( $product[$fieldName] ?? '' ), ENT_QUOTES, 'UTF-8' );
$customLabelsHtml .= '<div class="' . $fieldName . '_container">';
$customLabelsHtml .= '<small class="text-muted">' . $labelText . '</small>';
$datalistId = 'custom-label-list-' . $productId . '-' . $fieldName;
$customLabelsHtml .= '<input type="text" class="form-control input-sm product-custom-label" data-label-type="' . $fieldName . '" data-product-id="' . $productId . '" data-datalist-id="' . $datalistId . '" list="' . $datalistId . '" value="' . $valueText . '" placeholder="' . $labelText . '">';
$customLabelsHtml .= '<datalist id="' . $datalistId . '"></datalist>';
$customLabelsHtml .= '<div class="' . $fieldName . '_suggestions custom-label-suggestions"></div>';
$customLabelsHtml .= '</div>';
}
$customLabelsHtml .= '</div>';
return $customLabelsHtml;
}
}

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

@@ -193,7 +193,8 @@ 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 );
$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 );
if ( $product['language']['meta_title'] )
$page['language']['title'] = $product['language']['meta_title'];

View File

@@ -1,147 +0,0 @@
# CARL — Dynamiczne reguły dla Claude Code
[CARL](https://github.com/ChristopherKahler/carl) (Context Augmentation & Reinforcement Layer) to system wstrzykiwania reguł do Claude Code — reguły ładują się **tylko gdy są potrzebne**, zamiast zapychać kontekst sesji regułami, których akurat nie używasz.
Zainstalowany globalnie w `~/.carl/`.
---
## Jak to działa
Claude Code ma ograniczony kontekst sesji. CARL rozwiązuje problem "zbyt wielu reguł na raz":
- **Domeny** — zestawy reguł uruchamiane automatycznie przez słowa kluczowe w prompcie
- **Star-commands** (`*dev`, `*review`, itp.) — tryby uruchamiane ręcznie przez wpisanie `*nazwatrybe`
- **Global** — reguły zawsze aktywne (minimalne, uniwersalne)
Efekt: zamiast 50 reguł na każdą sesję, Claude dostaje 510 tych, które są relevantne teraz.
---
## Struktura plików
```
~/.carl/ # Globalna konfiguracja (wszystkie projekty)
├── manifest # Rejestr domen (stany + słowa kluczowe)
├── global # Reguły zawsze aktywne
├── commands # Definicje star-commands
├── context # Reguły kontekstowe (rozmiar okna kontekstu)
└── {nazwa-domeny} # Twoja domena (bez rozszerzenia!)
.carl/ # Konfiguracja lokalna (tylko ten projekt)
└── {nazwa-domeny} # Reguły specyficzne dla shopPRO
```
**Ważne:** Nazwy plików domen — **małe litery, bez rozszerzenia** (`phpdev`, nie `phpdev.carl`).
---
## Dostępne star-commands
Wpisz `*nazwa` na początku wiadomości lub w środku promptu, żeby przełączyć tryb:
| Komenda | Tryb | Kiedy używać |
|---------|------|-------------|
| `*dev` | Development | Implementacja, szybkie zmiany bez tłumaczeń |
| `*review` | Code review | Przegląd kodu, bezpieczeństwo, edge cases |
| `*brief` | Zwięzłe odpowiedzi | Tylko bullet points, bez elaboracji |
| `*plan` | Planowanie | Eksploracja przed implementacją, opcje + tradeoffs |
| `*discuss` | Dyskusja | Burza mózgów, wiele podejść, bez skakania do kodu |
| `*debug` | Debugowanie | Systematyczna diagnoza, root cause analysis |
| `*explain` | Wyjaśnianie | Nauka, koncepty, stopniowe budowanie wiedzy |
| `*carl` | Pomoc CARL | Zarządzanie domenami, konfiguracja, pytania o CARL |
### Przykłady
```
*dev Napraw błąd w summaryView() gdzie duplikuje zamówienia
*review Przejrzyj OrderRepository::createFromBasket pod kątem bezpieczeństwa
*brief Co robi CacheHandler::deletePattern()
*plan Chcę dodać system rabatów do koszyka
*discuss Czy lepiej rozdzielić ApiloRepository na sync i admin?
```
---
## Tworzenie własnej domeny (projekt)
Kiedy masz zestaw reguł specyficznych dla shopPRO, utwórz lokalną domenę w `.carl/`.
### Krok 1 — Plik domeny
Utwórz `.carl/shoppro` (bez rozszerzenia):
```
# shopPRO Domain Rules
SHOPPRO_RULE_0=PHP < 8.0 — nie używaj match, named args, union types, str_contains
SHOPPRO_RULE_1=ORM: Medoo ($mdb), zawsze prepared statements, nigdy string concatenation
SHOPPRO_RULE_2=Namespace \Domain\ mapuje do autoload/Domain/ (D uppercase, a lowercase)
SHOPPRO_RULE_3=Testy: PHPUnit 9.6, pattern AAA, mock Medoo przez createMock(\medoo::class)
SHOPPRO_RULE_4=Cache: CacheHandler::deletePattern() do kasowania, TTL 86400, dane serialized
```
### Krok 2 — Wpis w manifeście
Dodaj do `.carl/manifest` (lub `~/.carl/manifest` jeśli globalna):
```
SHOPPRO_STATE=active
SHOPPRO_RECALL=shopPRO, medoo, zamówienie, koszyk, OrderRepository, Domain
SHOPPRO_EXCLUDE=
SHOPPRO_ALWAYS_ON=false
```
- `RECALL` — słowa kluczowe które triggerują domenę (przecinek = OR)
- `ALWAYS_ON=true` — ładuj przy każdym prompcie (tylko dla naprawdę universalnych reguł)
- `EXCLUDE` — słowa które blokują domenę mimo dopasowania RECALL
### Krok 3 — Weryfikacja
Wpisz `*carl` w czacie i zapytaj: _"Pokaż mi aktywne domeny"_.
---
## Zarządzanie przez Claude
Zamiast ręcznie edytować pliki, możesz zarządzać CARL przez Claude:
```
*carl Dodaj domenę dla testów PHPUnit w shopPRO
*carl Pokaż moją aktualną konfigurację
*carl Wyłącz domenę SHOPPRO tymczasowo
*carl Dodaj regułę do domeny dev: zawsze uruchamiaj ./test.ps1 po zmianach
```
Claude użyje skills `carl:manager` / `carl:tasks:*` do operacji na plikach.
---
## Integracja z PAUL
CARL i [PAUL](./PAUL_WORKFLOW.md) działają uzupełniająco:
- **PAUL** strukturyzuje *proces* (plan → apply → unify)
- **CARL** dostarcza *reguły domenowe* wtedy gdy są potrzebne
Praktycznie: podczas `/paul:apply` możesz prefixować `*dev` żeby Claude skupił się na kodzie bez elaboracji. Podczas `/paul:discuss``*discuss` żeby dostać pełną analizę opcji.
---
## Dobre praktyki
- **RECALL słowa** — używaj konkretnych, rzadkich słów żeby unikać false triggers. `medoo` lepsze niż `php`.
- **Mało reguł per domena** — 58 reguł to optymalnie. Więcej = wolniejsze matching, więcej tokenów.
- **ALWAYS_ON=false** domyślnie — ALWAYS_ON=true tylko dla reguł naprawdę universalnych (jak GLOBAL).
- **Star-commands przy dużych taskach** — na początku sesji wpisz `*dev` lub `*plan` żeby ustawić tryb.
- **Nie duplikuj CLAUDE.md** — CARL nie zastępuje CLAUDE.md. CLAUDE.md to architektura projektu. CARL to reguły zachowania Claude.
---
*Docs: 2026-03-12*

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,156 +0,0 @@
# Plan: System kolejki zadań cron oparty o bazę danych
## Kontekst
Obecny system cron ma dwa problemy:
1. **Kolejka plikowa (JSON)** — sync płatności/statusów Apilo trzymany w `/temp/apilo-sync-queue.json` — kruchy, brak transakcji, ryzyko utraty danych
2. **Monolityczny cron.php** (~550 linii) — brak priorytetów, brak retry z backoff, brak centralnego zarządzania
Cel: Zastąpienie całego systemu cron tabelą `pp_cron_jobs` z priorytetami, retry/backoff i harmonogramem `pp_cron_schedules`.
## Nowe pliki
| Plik | Opis |
|------|------|
| `autoload/Domain/CronJob/CronJobType.php` | Stałe typów zadań i priorytetów |
| `autoload/Domain/CronJob/CronJobRepository.php` | CRUD na `pp_cron_jobs` + `pp_cron_schedules` |
| `autoload/Domain/CronJob/CronJobProcessor.php` | Orkiestracja: pobierz zadanie → wywołaj handler → obsłuż wynik |
| `tests/Unit/Domain/CronJob/CronJobTypeTest.php` | Testy stałych |
| `tests/Unit/Domain/CronJob/CronJobRepositoryTest.php` | Testy repozytorium |
| `tests/Unit/Domain/CronJob/CronJobProcessorTest.php` | Testy procesora |
| `migrations/0.315.sql` | CREATE TABLE + INSERT harmonogramów |
## Modyfikowane pliki
| Plik | Zmiana |
|------|--------|
| `cron.php` | Zastąpienie ~550 linii orchestratorem (~100 linii) z rejestracją handlerów |
| `cron/cron-xml.php` | Usunięcie — logika przeniesiona do handlera `google_xml_feed` |
| `cron-turstmate.php` | Usunięcie — logika przeniesiona do handlera `trustmate_invitation` |
| `autoload/Domain/Order/OrderAdminService.php` | `queueApiloSync()` → enqueue do DB; usunięcie metod plikowych; `syncApiloPayment()`/`syncApiloStatus()` → public |
| `tests/Unit/Domain/Order/OrderAdminServiceTest.php` | Refaktor testów kolejki: mock `CronJobRepository` zamiast pliku JSON |
| `docs/DATABASE_STRUCTURE.md` | Dodanie tabel `pp_cron_jobs`, `pp_cron_schedules` |
| `docs/CHANGELOG.md` | Wpis o nowym systemie |
## Schemat DB (`migrations/0.315.sql`)
### `pp_cron_jobs`
```sql
CREATE TABLE pp_cron_jobs (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
job_type VARCHAR(50) NOT NULL,
status ENUM('pending','processing','completed','failed','cancelled') NOT NULL DEFAULT 'pending',
priority TINYINT UNSIGNED NOT NULL DEFAULT 100, -- niższy = ważniejszy
payload TEXT NULL, -- JSON z danymi zadania
result TEXT NULL, -- JSON z wynikiem
attempts SMALLINT UNSIGNED NOT NULL DEFAULT 0,
max_attempts SMALLINT UNSIGNED NOT NULL DEFAULT 10,
last_error VARCHAR(500) NULL,
scheduled_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
started_at DATETIME NULL,
completed_at DATETIME NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_status_priority_scheduled (status, priority, scheduled_at),
INDEX idx_job_type (job_type),
INDEX idx_status (status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
```
### `pp_cron_schedules`
```sql
CREATE TABLE pp_cron_schedules (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
job_type VARCHAR(50) NOT NULL UNIQUE,
interval_seconds INT UNSIGNED NOT NULL,
priority TINYINT UNSIGNED NOT NULL DEFAULT 100,
max_attempts SMALLINT UNSIGNED NOT NULL DEFAULT 3,
payload TEXT NULL,
enabled TINYINT(1) NOT NULL DEFAULT 1,
last_run_at DATETIME NULL,
next_run_at DATETIME NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_enabled_next_run (enabled, next_run_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
```
## Typy zadań i priorytety
| Typ | Priorytet | Harmonogram |
|-----|-----------|-------------|
| `apilo_token_keepalive` | 10 (krytyczny) | co 4 min |
| `apilo_send_order` | 50 (wysoki) | co 1 min |
| `apilo_sync_payment` | 50 (wysoki) | event-driven (enqueue przy zmianie) |
| `apilo_sync_status` | 50 (wysoki) | event-driven |
| `apilo_product_sync` | 100 (normalny) | co 10 min |
| `apilo_pricelist_sync` | 100 (normalny) | co 1h |
| `apilo_status_poll` | 100 (normalny) | co 10 min |
| `price_history` | 100 (normalny) | co 24h |
| `order_analysis` | 100 (normalny) | co 10 min |
| `trustmate_invitation` | 200 (niski) | co 10 min |
| `google_xml_feed` | 200 (niski) | co 1h |
## Architektura klas
### CronJobRepository — metody kluczowe
- `enqueue($jobType, $payload, $priority, $maxAttempts, $scheduledAt)` — dodaj do kolejki
- `fetchNext($limit)` — atomowe pobranie pending jobs (UPDATE WHERE status='pending')
- `markCompleted($jobId, $result)` / `markFailed($jobId, $error, $backoffSeconds)`
- `hasPendingJob($jobType, $payloadMatch)` — zapobiega duplikatom
- `cleanup($olderThanDays)` — GC starych wpisów
- `recoverStuck($olderThanMinutes)` — reset stuck "processing" jobs
- `getDueSchedules()` / `touchSchedule($id)` — harmonogram
### CronJobProcessor — orkiestracja
- `registerHandler($jobType, callable)` — rejestracja handlera
- `createScheduledJobs()` — tworzy jobs z harmonogramów których `next_run_at <= NOW`
- `processQueue($limit)` — pobierz + wywołaj handler + markCompleted/markFailed
- `run($limit)` — główna metoda: schedules + process
### Exponential backoff
```
Próba 1: 60s, Próba 2: 120s, Próba 3: 240s, ... max 3600s (1h)
```
### Zależność "order not yet in Apilo"
Handler `apilo_sync_payment`/`apilo_sync_status` sprawdza `apilo_order_id`. Jeśli brak → zwraca false → `markFailed()` z backoffem → zadanie wraca do kolejki. Max 50 prób.
## Nowy cron.php (schemat)
```php
$cronRepo = new \Domain\CronJob\CronJobRepository($mdb);
$processor = new \Domain\CronJob\CronJobProcessor($mdb, $cronRepo);
// Rejestracja handlerów (każdy to callable)
$processor->registerHandler('apilo_token_keepalive', function($payload) use ($integrationsRepo) { ... });
$processor->registerHandler('apilo_send_order', function($payload) use ($orderService, ...) { ... });
// ... inne handlery
$result = $processor->run(20);
```
## Zmiany w OrderAdminService
1. `queueApiloSync()``CronJobRepository::enqueue()` zamiast zapisu do pliku JSON
2. Usunięcie: `loadApiloSyncQueue()`, `saveApiloSyncQueue()`, `apiloSyncQueuePath()`, stała `APILO_SYNC_QUEUE_FILE`
3. `syncApiloPayment()`, `syncApiloStatus()` → zmiana z `private` na `public`
4. Jednorazowa migracja: odczyt JSON → insert do DB → usunięcie pliku
## Kolejność implementacji
1. Migracja SQL
2. `CronJobType.php`
3. `CronJobRepository.php` + testy
4. `CronJobProcessor.php` + testy
5. Modyfikacja `OrderAdminService` (queue → DB, public methods)
6. Jednorazowa migracja pliku JSON → DB
7. Nowy `cron.php` z handlerami (ekstrakcja logiki z bloków proceduralnych)
8. Aktualizacja testów OrderAdminService
9. Dokumentacja (DATABASE_STRUCTURE.md, CHANGELOG.md)
## Weryfikacja
1. Uruchomienie pełnego zestawu testów: `./test.ps1`
2. Sprawdzenie czy nowe testy CronJob* przechodzą
3. Sprawdzenie czy istniejące testy OrderAdminService przechodzą po refaktorze
4. Weryfikacja migracji SQL na pustej bazie

View File

@@ -1,730 +0,0 @@
# Struktura bazy danych shopPRO
Plik aktualizowany na bieżąco przy zmianach w kodzie.
ORM: Medoo (`$mdb`), prefix tabel: `pp_`
## pp_shop_products
Główna tabela produktów.
| Kolumna | Opis |
|---------|------|
| id | PK |
| parent_id | FK do produktu nadrzędnego (kombinacje) - NULL dla produktów głównych |
| price_brutto | Cena brutto |
| price_brutto_promo | Cena promocyjna brutto |
| quantity | Stan magazynowy |
| status | Status: 1 = aktywny, 0 = nieaktywny |
| archive | Archiwum: 1 = zarchiwizowany, 0 = aktywny |
| promoted | Czy promowany |
| vat | Stawka VAT |
| ean | Kod EAN |
| sku | Kod SKU |
| apilo_product_id | ID produktu w Apilo |
| apilo_product_name | Nazwa produktu w Apilo |
**Używane w:** `Domain\Product\ProductRepository`, `admin\factory\ShopProduct`, `admin\Controllers\ShopProductController`
## pp_shop_products_langs
Tłumaczenia produktów (per język).
| Kolumna | Opis |
|---------|------|
| id | PK |
| product_id | FK do pp_shop_products |
| lang_id | ID języka (np. 'pl') |
| name | Nazwa produktu |
**Używane w:** `Domain\Product\ProductRepository::getName()`
## pp_shop_products_images
Zdjęcia produktów.
| Kolumna | Opis |
|---------|------|
| id | PK |
| product_id | FK do pp_shop_products |
| src | Ścieżka do pliku |
| alt | Tekst alternatywny |
## pp_shop_products_custom_fields
Dodatkowe pola produktów (custom fields).
| Kolumna | Opis |
|---------|------|
| id_additional_field | PK |
| id_product | FK do pp_shop_products |
| name | Nazwa pola |
| type | Typ pola (VARCHAR 30) |
| is_required | Czy wymagane (0/1) |
## pp_shop_products_categories
Przypisanie produktów do kategorii.
| Kolumna | Opis |
|---------|------|
| product_id | FK do pp_shop_products |
**Używane w:** `admin\factory\ShopProduct::product_delete()`, `Domain\Product\ProductRepository::getProductsByCategory()`
**Aktualizacja 2026-02-15 (ver. 0.274):** akcje `/admin/shop_product/mass_edit/*` korzystają z `Domain\Product\ProductRepository` przez `admin\Controllers\ShopProductController`.
## pp_shop_categories
Kategorie sklepu.
| Kolumna | Opis |
|---------|------|
| id | PK |
| parent_id | FK do kategorii nadrzednej (NULL dla root) |
| status | 1 = aktywna, 0 = nieaktywna |
| o | Kolejnosc wyswietlania |
| sort_type | Typ sortowania produktow w kategorii |
| view_subcategories | Czy wyswietlac podkategorie |
**Uzywane w:** `Domain\Category\CategoryRepository`, `admin\Controllers\ShopCategoryController`.
## pp_shop_categories_langs
Tlumaczenia kategorii (per jezyk).
| Kolumna | Opis |
|---------|------|
| category_id | FK do pp_shop_categories |
| lang_id | ID jezyka (np. pl, en) |
| title | Nazwa kategorii |
| text | Opis kategorii |
| text_hidden | Rozwiniecie opisu kategorii |
| seo_link | Link SEO kategorii |
| meta_title | Meta title |
| meta_description | Meta description |
| meta_keywords | Meta keywords |
| noindex | Flaga noindex |
| category_title | Naglowek H1 kategorii |
| additional_text | Dodatkowy tekst nad lista produktow |
**Uzywane w:** `Domain\Category\CategoryRepository`, `admin\Controllers\ShopCategoryController`.
**Aktualizacja 2026-02-15 (ver. 0.275):** modul `/admin/shop_category/*` korzysta z `Domain\Category\CategoryRepository` przez `admin\Controllers\ShopCategoryController`; usunieto legacy `admin\controls/factory/view\ShopCategory`.
## pp_shop_orders
Zamówienia sklepu (źródło danych dla list i szczegółów klientów w panelu admin).
| Kolumna | Opis |
|---------|------|
| id | PK |
| client_id | FK do `pp_shop_clients` (NULL dla gościa) |
| client_name | Imię klienta z zamówienia |
| client_surname | Nazwisko klienta z zamówienia |
| client_email | E-mail klienta z zamówienia |
| client_phone | Telefon klienta |
| client_city | Miasto klienta |
| summary | Wartość zamówienia |
| date_order | Data złożenia zamówienia |
| payment_method | Nazwa metody płatności |
| transport | Nazwa transportu |
| message | Wiadomość klienta |
| updated_at | Data ostatniej modyfikacji (polling API) |
**Używane w:** `Domain\Client\ClientRepository::listForAdmin()`, `Domain\Client\ClientRepository::ordersForClient()`, `Domain\Client\ClientRepository::totalsForClient()`, `Domain\Order\OrderRepository::listForApi()`, `Domain\Order\OrderRepository::findForApi()`.
**Aktualizacja 2026-02-15 (ver. 0.274):** moduł `/admin/shop_clients/*` korzysta z `Domain\Client\ClientRepository` przez `admin\Controllers\ShopClientsController`.
**Aktualizacja 2026-02-15 (ver. 0.276):** moduł `/admin/shop_order/*` korzysta z `Domain\Order\OrderRepository` przez `admin\Controllers\ShopOrderController`; usunięto legacy `admin\controls\ShopOrder` i `admin\factory\ShopOrder`.
**Aktualizacja 2026-02-17 (ver. 0.290):** frontend `/shop_order/*` korzysta z `Domain\Order\OrderRepository` przez `front\Controllers\ShopOrderController`; usunięto legacy `front\controls\ShopOrder`, `front\factory\ShopOrder`, `front\view\ShopOrder`. Callery (ShopBasketController, ClientRepository, shop\Order, cron-turstmate) przepięte na OrderRepository.
## pp_banners
Banery.
| Kolumna | Opis |
|---------|------|
| id | PK |
| name | Nazwa banera |
| status | 0/1 |
| date_start | Data rozpoczęcia |
| date_end | Data zakończenia |
| home_page | Czy na stronie głównej 0/1 |
**Używane w:** `Domain\Banner\BannerRepository`, `front\Views\Banners`
**Aktualizacja 2026-02-16 (ver. 0.281):** metody frontendowe `banners()`, `mainBanner()` dodane do `Domain\Banner\BannerRepository`. Fasady `front\factory\Banners` i `front\view\Banners` deleguja do repo/Views.
## pp_banners_langs
Tłumaczenia banerów.
| Kolumna | Opis |
|---------|------|
| id | PK |
| id_banner | FK do pp_banners |
| id_lang | ID języka |
| src | Ścieżka do grafiki |
| url | URL docelowy |
| html | Kod HTML |
| text | Tekst |
**Używane w:** `Domain\Banner\BannerRepository`, `front\Views\Banners`
## pp_articles
Artykuły.
| Kolumna | Opis |
|---------|------|
| id | PK |
| status | -1 = archiwum, 0 = nieaktywny, 1 = aktywny |
**Używane w:** `admin\Controllers\ArticlesArchiveController`, `Domain\Article\ArticleRepository::find()`, `Domain\Article\ArticleRepository::listArchivedForAdmin()`
## pp_articles_pages
Strony artykułów.
| Kolumna | Opis |
|---------|------|
| article_id | FK do pp_articles |
| page_id | FK do strony (pp_pages) |
| o | Kolejność |
**Używane w:** `Domain\Article\ArticleRepository::find()`, `Domain\Article\ArticleRepository::deleteNonassignedImages()`
## pp_articles_langs
Tłumaczenia artykułów.
| Kolumna | Opis |
|---------|------|
| article_id | FK do pp_articles |
| lang_id | ID języka (np. 'pl') |
| title | Tytuł artykułu |
| seo_link | Link SEO artykułu |
**Używane w:** `Domain\Article\ArticleRepository::find()`, `Domain\Article\ArticleRepository::deleteNonassignedFiles()`
## pp_articles_images
Zdjęcia artykułów.
| Kolumna | Opis |
|---------|------|
| article_id | FK do pp_articles |
| src | Ścieżka do pliku |
| o | Kolejność |
| id | PK (używane też do sortowania DESC) |
**Używane w:** `Domain\Article\ArticleRepository::find()`
## pp_articles_files
Pliki artykułów.
| Kolumna | Opis |
|---------|------|
| id | PK |
| article_id | FK do pp_articles |
| src | Ścieżka do pliku |
| name | Nazwa wyświetlana załącznika (opcjonalna) |
| to_delete | Flaga miękkiego usuwania (0/1) |
| o | Kolejność załączników (używana przez sortowanie drag&drop w adminie) |
**Używane w:** `Domain\Article\ArticleRepository::find()`, `Domain\Article\ArticleRepository::saveFilesOrder()`
## pp_units
Jednostki/slowniki (np. jednostki produktu).
| Kolumna | Opis |
|---------|------|
| id | PK |
**Używane w:** `Domain\Dictionaries\DictionariesRepository`, `admin\controls\ShopProduct`
## pp_units_langs
Tlumaczenia jednostek (per jezyk).
| Kolumna | Opis |
|---------|------|
| id | PK |
| unit_id | FK do pp_units |
| lang_id | ID jezyka (np. 'pl') |
| text | Nazwa jednostki |
**Używane w:** `Domain\Dictionaries\DictionariesRepository`
## pp_users
Uzytkownicy panelu administratora.
| Kolumna | Opis |
|---------|------|
| id | PK |
| login | Login / e-mail uzytkownika |
| password | Hash hasla (legacy: md5) |
| status | Status konta: 1 = aktywny, 0 = zablokowany |
| admin | Flaga dostepu do panelu admin |
| error_logged_count | Licznik nieudanych logowan |
| last_logged | Data ostatniego poprawnego logowania |
| last_error_logged | Data ostatniej nieudanej proby logowania |
| twofa_enabled | Czy wlaczone 2FA (0/1) |
| twofa_email | E-mail do wysylki kodu 2FA |
| twofa_code_hash | Hash aktualnego kodu 2FA |
| twofa_expires_at | Data waznosci kodu 2FA |
| twofa_sent_at | Data ostatniej wysylki kodu 2FA |
| twofa_failed_attempts | Liczba nieudanych prob 2FA |
**Uzywane w:** `Domain\User\UserRepository`, `admin\Controllers\UsersController`, `admin\factory\Users`
**Aktualizacja 2026-02-12:** uzycia `pp_users` sa prowadzone przez `Domain\\User\\UserRepository` (legacy `admin\\factory\\Users` usunieto).
## pp_langs
Jezyki panelu i frontendu.
| Kolumna | Opis |
|---------|------|
| id | PK (2-literowe ID jezyka, np. pl, en) |
| name | Nazwa jezyka |
| status | 1 = aktywny, 0 = nieaktywny |
| start | 1 = domyslny jezyk |
| o | Kolejnosc |
**Uzywane w:** `Domain\\Languages\\LanguagesRepository`, `admin\\Controllers\\LanguagesController`, `front\\factory\\Languages`
## pp_langs_translations
Slownik tlumaczen panelu/frontendu.
| Kolumna | Opis |
|---------|------|
| id | PK |
| text | Klucz/tekst bazowy |
| <lang_id> | Kolumny dynamiczne per jezyk (np. pl, en) |
**Uzywane w:** `Domain\\Languages\\LanguagesRepository`, `admin\\Controllers\\LanguagesController`, `front\\factory\\Languages`
**Aktualizacja 2026-02-12:** modul jezykow i tlumaczen (`pp_langs`, `pp_langs_translations`) obslugiwany przez `Domain\\Languages\\LanguagesRepository`.
## pp_layouts
Szablony layoutow (HTML/CSS/JS + flagi domyslne).
| Kolumna | Opis |
|---------|------|
| id | PK |
| name | Nazwa szablonu |
| html | Kod HTML |
| css | Kod CSS |
| js | Kod JS |
| m_html | Kod HTML mobilny |
| m_css | Kod CSS mobilny |
| m_js | Kod JS mobilny |
| status | Domyslny layout stron (0/1) |
| categories_default | Domyslny layout kategorii (0/1) |
**Uzywane w:** `Domain\\Layouts\\LayoutsRepository`, `admin\\Controllers\\LayoutsController`, `front\\factory\\Layouts`
## pp_layouts_pages
Przypisanie layoutow do stron CMS.
| Kolumna | Opis |
|---------|------|
| layout_id | FK do pp_layouts |
| page_id | FK do pp_pages |
**Uzywane w:** `Domain\\Layouts\\LayoutsRepository`, `front\\factory\\Layouts`
## pp_layouts_categories
Przypisanie layoutow do kategorii sklepu.
| Kolumna | Opis |
|---------|------|
| layout_id | FK do pp_layouts |
| category_id | FK do pp_shop_categories |
**Uzywane w:** `Domain\\Layouts\\LayoutsRepository`, `front\\factory\\Layouts`
**Aktualizacja 2026-02-12 (ver. 0.256):** modul `/admin/layouts` korzysta z `Domain\\Layouts\\LayoutsRepository` (DI kontroler + fasada legacy).
## pp_newsletter
Adresy e-mail zapisane do newslettera.
| Kolumna | Opis |
|---------|------|
| id | PK |
| email | Adres e-mail subskrybenta |
| hash | Hash potwierdzenia/wypisu |
| status | 1 = potwierdzony, 0 = oczekujacy |
**Uzywane w:** `Domain\\Newsletter\\NewsletterRepository`, `front\\Controllers\\NewsletterController`
## pp_newsletter_send
Kolejka wysylki newslettera.
| Kolumna | Opis |
|---------|------|
| id | PK |
| email | Adres docelowy |
| dates | Zakres dat artykulow (tekst) |
| id_template | FK do `pp_newsletter_templates` (NULL gdy brak szablonu) |
**Uzywane w:** `Domain\\Newsletter\\NewsletterRepository`
## pp_newsletter_templates
Szablony tresci e-maili (uzytkownik + administracyjne/systemowe).
| Kolumna | Opis |
|---------|------|
| id | PK |
| name | Nazwa/klucz szablonu |
| text | Tresc HTML szablonu |
| is_admin | 1 = szablon administracyjny/systemowy, 0 = szablon uzytkownika |
**Uzywane w:** `Domain\\Newsletter\\NewsletterRepository`, `admin\\Controllers\\NewsletterController`
**Aktualizacja 2026-02-12 (ver. 0.257):** modul `/admin/newsletter` korzysta z `Domain\\Newsletter\\NewsletterRepository` (DI kontroler + fasada legacy).
**Aktualizacja 2026-02-16 (ver. 0.279):** `front\\factory\\Newsletter` usunięta — logika przeniesiona do `NewsletterRepository`. Frontend korzysta z `front\\Controllers\\NewsletterController` (DI).
## pp_scontainers
Kontenery statyczne (modul /admin/scontainers).
| Kolumna | Opis |
|---------|------|
| id | PK |
| status | 1 = aktywny, 0 = nieaktywny |
| show_title | 1 = pokaz tytul, 0 = ukryj tytul |
**Uzywane w:** `Domain\Scontainers\ScontainersRepository`, `admin\Controllers\ScontainersController`, `front\factory\Scontainers`
## pp_scontainers_langs
Tlumaczenia kontenerow statycznych (per jezyk).
| Kolumna | Opis |
|---------|------|
| id | PK |
| container_id | FK do pp_scontainers |
| lang_id | ID jezyka (np. pl, en) |
| title | Tytul kontenera |
| text | Tresc HTML kontenera |
**Uzywane w:** `Domain\Scontainers\ScontainersRepository`, `front\factory\Scontainers`
**Aktualizacja 2026-02-12 (ver. 0.259):** modul `/admin/scontainers` korzysta z `Domain\Scontainers\ScontainersRepository` (DI kontroler + fasada legacy).
**Aktualizacja 2026-02-12 (ver. 0.260):** modul `/admin/articles_archive` korzysta z `Domain\Article\ArticleRepository` (`listArchivedForAdmin`, `restore`, `deletePermanently`) przez `admin\Controllers\ArticlesArchiveController`.
## pp_shop_attributes
Cechy produktu (modul `/admin/shop_attribute`).
| Kolumna | Opis |
|---------|------|
| id | PK |
| status | Status: 1 = aktywny, 0 = nieaktywny |
| type | Typ cechy: 0 = tekst, 1 = kolor, 2 = wzor |
| o | Kolejnosc wyswietlania |
**Uzywane w:** `Domain\Attribute\AttributeRepository`, `admin\Controllers\ShopAttributeController`, `admin\controls\ShopProduct`, `admin\factory\ShopProduct`
## pp_shop_attributes_langs
Tlumaczenia cech produktu (per jezyk).
| Kolumna | Opis |
|---------|------|
| id | PK |
| attribute_id | FK do pp_shop_attributes |
| lang_id | ID jezyka (np. pl, en) |
| name | Nazwa cechy |
**Uzywane w:** `Domain\Attribute\AttributeRepository`, `shop\ProductAttribute`
## pp_shop_attributes_values
Wartosci cech produktu.
| Kolumna | Opis |
|---------|------|
| id | PK |
| attribute_id | FK do pp_shop_attributes |
| is_default | Czy wartosc domyslna dla cechy (0/1) |
| impact_on_the_price | Wplyw na cene wariantu (NULL = brak) |
**Uzywane w:** `Domain\Attribute\AttributeRepository`, `admin\Controllers\ShopAttributeController`, `admin\factory\ShopProduct`
## pp_shop_attributes_values_langs
Tlumaczenia wartosci cech (per jezyk).
| Kolumna | Opis |
|---------|------|
| id | PK |
| value_id | FK do pp_shop_attributes_values |
| lang_id | ID jezyka (np. pl, en) |
| name | Nazwa wyswietlana |
| value | Wewnetrzna wartosc techniczna (opcjonalna) |
**Uzywane w:** `Domain\Attribute\AttributeRepository`, `shop\ProductAttribute`
## pp_shop_products_attributes
Powiazanie kombinacji produktow z wartosciami cech.
| Kolumna | Opis |
|---------|------|
| product_id | FK do pp_shop_products (kombinacja) |
| value_id | FK do pp_shop_attributes_values |
**Uzywane w:** `Domain\Attribute\AttributeRepository::refreshCombinationPricesForValue()`, `admin\controls\ShopProduct`, `admin\factory\ShopProduct`
**Aktualizacja 2026-02-14 (ver. 0.271):** modul `/admin/shop_attribute` korzysta z `Domain\Attribute\AttributeRepository` przez `admin\Controllers\ShopAttributeController`. Usunieto legacy klasy `admin\controls\ShopAttribute`, `admin\factory\ShopAttribute`, `admin\view\ShopAttribute`.
## pp_shop_coupon
Kody rabatowe sklepu (modul `/admin/shop_coupon`).
| Kolumna | Opis |
|---------|------|
| id | PK |
| name | Kod kuponu (UNIQUE) |
| status | Status: 1 = aktywny, 0 = nieaktywny |
| send | Czy kupon zostal wyslany (0/1) |
| used | Czy kupon zostal wykorzystany (0/1) |
| date_used | Data wykorzystania kuponu (NULL gdy brak) |
| used_count | Licznik uzyc kuponu |
| type | Typ kuponu (obecnie: 1 = rabat procentowy na koszyk) |
| amount | Wartosc kuponu (np. procent) |
| one_time | Czy kupon jednorazowy (0/1) |
| include_discounted_product | Czy obejmuje rowniez produkty przecenione (0/1) |
| categories | JSON z ID kategorii objetych kuponem (NULL = bez ograniczenia) |
**Uzywane w:** `Domain\Coupon\CouponRepository`, `admin\Controllers\ShopCouponController`, `front\Controllers\ShopCouponController`, `shop\Coupon`, `Domain\Order\OrderRepository`
**Aktualizacja 2026-02-13 (ver. 0.266):** modul `/admin/shop_coupon` korzysta z `Domain\Coupon\CouponRepository` przez `admin\Controllers\ShopCouponController`. Usunieto legacy klasy `admin\controls\ShopCoupon` i `admin\factory\ShopCoupon`.
## pp_shop_promotion
Promocje sklepu (modul `/admin/shop_promotion`).
| Kolumna | Opis |
|---------|------|
| id | PK |
| name | Nazwa promocji |
| status | Status: 1 = aktywna, 0 = nieaktywna |
| condition_type | Typ warunku promocji (slownik w `shop\Promotion::$condition_type`) |
| discount_type | Typ rabatu (slownik w `shop\Promotion::$discount_type`) |
| amount | Wartosc rabatu (np. procent) |
| date_from | Data startu promocji (NULL = aktywna od razu) |
| date_to | Data konca promocji (NULL = bez daty konca) |
| categories | JSON z ID kategorii grupy I |
| condition_categories | JSON z ID kategorii grupy II |
| include_coupon | Czy laczyc z kuponami rabatowymi (0/1) |
| include_product_promo | Czy uwzgledniac produkty przecenione (0/1) |
| min_product_count | Minimalna liczba produktow (dla wybranych warunkow) |
| price_cheapest_product | Cena najtanszego produktu (dla wybranych warunkow) |
**Uzywane w:** `Domain\Promotion\PromotionRepository`, `admin\Controllers\ShopPromotionController`, `shop\Promotion`, `front\factory\ShopPromotion`
**Aktualizacja 2026-02-13:** modul `/admin/shop_promotion` korzysta z `Domain\Promotion\PromotionRepository` przez `admin\Controllers\ShopPromotionController`. Usunieto legacy klasy `admin\controls\ShopPromotion` i `admin\factory\ShopPromotion`.
**Aktualizacja 2026-02-13 (ver. 0.265):** dodano obsluge `date_from` (repozytorium, formularz admin, lista admin, filtr aktywnych promocji na froncie) oraz poprawke zapisu edycji promocji po `id`.
## pp_shop_payment_methods
Metody platnosci sklepu (modul `/admin/shop_payment_method`).
| Kolumna | Opis |
|---------|------|
| id | PK |
| name | Nazwa metody platnosci |
| description | Opis metody platnosci (wyswietlany m.in. w checkout) |
| status | Status: 1 = aktywna, 0 = nieaktywna |
| apilo_payment_type_id | ID typu platnosci Apilo (NULL gdy brak mapowania) |
| min_order_amount | Minimalna kwota zamowienia (DECIMAL(10,2), NULL = brak limitu) |
| max_order_amount | Maksymalna kwota zamowienia (DECIMAL(10,2), NULL = brak limitu) |
| is_cod | Platnosc przy odbiorze: 1 = tak, 0 = nie (TINYINT DEFAULT 0) |
| sellasist_payment_type_id | DEPRECATED (integracja Sellasist usunieta w ver. 0.263) |
**Uzywane w:** `Domain\PaymentMethod\PaymentMethodRepository`, `admin\Controllers\ShopPaymentMethodController`, `front\factory\ShopPaymentMethod`, `shop\PaymentMethod`, `admin\controls\ShopTransport`, `cron.php`
**Aktualizacja 2026-03-12 (ver. 0.338):** dodano kolumne `is_cod` — flaga platnosci przy odbiorze, zastepuje hardkodowane `payment_id == 3` w `OrderRepository::createFromBasket()`.
**Aktualizacja 2026-02-14 (ver. 0.268):** modul `/admin/shop_payment_method` korzysta z `Domain\PaymentMethod\PaymentMethodRepository` przez `admin\Controllers\ShopPaymentMethodController`. Usunieto legacy klasy `admin\controls\ShopPaymentMethod`, `admin\factory\ShopPaymentMethod`, `admin\view\ShopPaymentMethod` oraz widok `admin/templates/shop-payment-method/view-list.php`.
**Aktualizacja 2026-02-22 (ver. 0.304):** dodano kolumny `min_order_amount` i `max_order_amount` — konfigurowalne limity kwotowe metod platnosci. Zastapiono hardcoded warunek PayPo (id=6, 40-1000 PLN) generycznym filtrowaniem na froncie.
## pp_shop_transports
Rodzaje transportu sklepu (modul `/admin/shop_transport`).
| Kolumna | Opis |
|---------|------|
| id | PK |
| name | Nazwa (systemowa, readonly) |
| name_visible | Nazwa widoczna dla klienta |
| description | Opis metody transportu |
| status | Status: 1 = aktywny, 0 = nieaktywny |
| cost | Koszt dostawy (PLN) |
| max_wp | Maksymalna waga paczki (NULL = bez limitu) |
| default | Domyslna forma dostawy (0/1) |
| delivery_free | Czy obsluguje darmowa dostawe (0/1) |
| apilo_carrier_account_id | ID konta przewoznika w Apilo (NULL gdy brak mapowania) |
| o | Kolejnosc wyswietlania |
**Uzywane w:** `Domain\Transport\TransportRepository`, `admin\Controllers\ShopTransportController`, `front\factory\ShopTransport`
## pp_shop_transport_payment_methods
Powiazanie metod transportu z metodami platnosci (tabela lacznikowa).
| Kolumna | Opis |
|---------|------|
| id_transport | FK do pp_shop_transports |
| id_payment_method | FK do pp_shop_payment_methods |
**Uzywane w:** `Domain\Transport\TransportRepository`, `Domain\PaymentMethod\PaymentMethodRepository::forTransport()`
**Aktualizacja 2026-02-14 (ver. 0.269):** modul `/admin/shop_transport` korzysta z `Domain\Transport\TransportRepository` przez `admin\Controllers\ShopTransportController`. Usunieto legacy klasy `admin\controls\ShopTransport`, `admin\view\ShopTransport` oraz widok `admin/templates/shop-transport/view-list.php`.
## pp_shop_apilo_settings
Ustawienia integracji Apilo (key-value).
| Kolumna | Opis |
|---------|------|
| id | PK |
| name | Klucz ustawienia (np. client-id, access-token) |
| value | Wartosc ustawienia |
**Uzywane w:** `Domain\Integrations\IntegrationsRepository`, `admin\Controllers\IntegrationsController`, `admin\factory\Integrations`
## pp_shop_shoppro_settings
Ustawienia integracji ShopPRO (key-value).
| Kolumna | Opis |
|---------|------|
| id | PK |
| name | Klucz ustawienia (np. domain, db_name) |
| value | Wartosc ustawienia |
**Uzywane w:** `Domain\Integrations\IntegrationsRepository`, `admin\Controllers\IntegrationsController`, `admin\factory\Integrations`
**Aktualizacja 2026-02-13:** modul `/admin/integrations/` korzysta z `Domain\Integrations\IntegrationsRepository` (DI kontroler + fasada legacy). Usunieto integracje Sellasist i Baselinker.
## pp_shop_statuses
Statusy zamowien sklepu (modul `/admin/shop_statuses`). Statusy sa predefiniowane - brak dodawania/usuwania, mozliwa edycja koloru i mapowania Apilo.
| Kolumna | Opis |
|---------|------|
| id | PK (zaczyna sie od 0!) |
| status | Nazwa statusu (read-only) |
| color | Kolor statusu (hex, np. #ff0000) |
| o | Kolejnosc wyswietlania |
| apilo_status_id | ID statusu w Apilo (NULL gdy brak mapowania) |
| baselinker_status_id | DEPRECATED (usuniety w ver. 0.263) |
| sellasist_status_id | DEPRECATED (usuniety w ver. 0.263) |
**Uzywane w:** `Domain\ShopStatus\ShopStatusRepository`, `admin\Controllers\ShopStatusesController`, `front\factory\ShopStatuses`, `shop\Order`, `cron.php`
**Aktualizacja 2026-02-14 (ver. 0.267):** modul `/admin/shop_statuses` korzysta z `Domain\ShopStatus\ShopStatusRepository` przez `admin\Controllers\ShopStatusesController`. Usunieto legacy klasy `admin\controls\ShopStatuses` i `admin\factory\ShopStatuses`. `front\factory\ShopStatuses` dziala jako fasada do repozytorium.
## pp_shop_product_sets
Komplety produktow (modul `/admin/shop_product_sets`).
| Kolumna | Opis |
|---------|------|
| id | PK |
| name | Nazwa kompletu |
| status | Status: 1 = aktywny, 0 = nieaktywny |
**Uzywane w:** `Domain\ProductSet\ProductSetRepository`, `admin\Controllers\ShopProductSetsController`, `shop\ProductSet`, `shop\Product`
## pp_shop_product_sets_products
Powiazanie kompletow z produktami (tabela lacznikowa).
| Kolumna | Opis |
|---------|------|
| id | PK |
| set_id | FK do pp_shop_product_sets |
| product_id | FK do pp_shop_products |
**Uzywane w:** `Domain\ProductSet\ProductSetRepository`, `shop\Product`, `front\factory\ShopProduct`, `admin\factory\ShopProduct`
**Aktualizacja 2026-02-15 (ver. 0.272):** modul `/admin/shop_product_sets` korzysta z `Domain\ProductSet\ProductSetRepository` przez `admin\Controllers\ShopProductSetsController`. Usunieto legacy klasy `admin\controls\ShopProductSets` i `admin\factory\ShopProductSet`. `shop\ProductSet` dziala jako fasada do repozytorium.
## pp_shop_producer
Producenci produktow (modul `/admin/shop_producer`).
| Kolumna | Opis |
|---------|------|
| id | PK |
| name | Nazwa producenta |
| status | Status: 1 = aktywny, 0 = nieaktywny |
| img | Sciezka do logo producenta (NULL gdy brak) |
**Uzywane w:** `Domain\Producer\ProducerRepository`, `admin\Controllers\ShopProducerController`, `front\Controllers\ShopProducerController`, `shop\Product`
## pp_shop_producer_lang
Tlumaczenia producentow (per jezyk). FK kaskadowe ON DELETE CASCADE.
| Kolumna | Opis |
|---------|------|
| id | PK |
| producer_id | FK do pp_shop_producer |
| lang_id | ID jezyka (np. pl, en) |
| description | Opis producenta (TEXT) |
| data | Dane producenta (TEXT, HTML) |
| meta_title | Meta title SEO (VARCHAR 255) |
**Uzywane w:** `Domain\Producer\ProducerRepository`, `shop\Product`
**Aktualizacja 2026-02-15 (ver. 0.273):** modul `/admin/shop_producer` korzysta z `Domain\Producer\ProducerRepository` przez `admin\Controllers\ShopProducerController`. Usunieto legacy `admin\controls\ShopProducer` i `admin\factory\ShopProducer`. `shop\Producer` dziala jako fasada do repozytorium.
**Aktualizacja 2026-02-17 (ver. 0.291):** frontend `/shop_producer/*` korzysta z `Domain\Producer\ProducerRepository` przez `front\Controllers\ShopProducerController`; usunięto legacy `front\controls\ShopProducer` i `shop\Producer`.
## pp_cron_jobs
Kolejka zadań cron z priorytetami i retry/backoff.
| Kolumna | Opis |
|---------|------|
| id | PK auto increment |
| job_type | Typ zadania (VARCHAR 50) — np. apilo_send_order, price_history |
| status | ENUM: pending, processing, completed, failed, cancelled |
| priority | TINYINT — niższy = ważniejszy (10=krytyczny, 50=wysoki, 100=normalny, 200=niski) |
| payload | JSON z danymi zadania (TEXT NULL) |
| result | JSON z wynikiem (TEXT NULL) |
| attempts | Liczba prób (SMALLINT) |
| max_attempts | Maksymalna liczba prób (SMALLINT, domyślnie 10) |
| last_error | Ostatni błąd (VARCHAR 500) |
| scheduled_at | Kiedy zadanie ma być uruchomione (DATETIME) |
| started_at | Kiedy rozpoczęto przetwarzanie (DATETIME NULL) |
| completed_at | Kiedy zakończono (DATETIME NULL) |
| created_at | Data utworzenia (DATETIME) |
| updated_at | Data ostatniej modyfikacji (DATETIME, ON UPDATE) |
**Indeksy:** idx_status_priority_scheduled (status, priority, scheduled_at), idx_job_type, idx_status
**Używane w:** `Domain\CronJob\CronJobRepository`, `Domain\CronJob\CronJobProcessor`
## pp_cron_schedules
Harmonogram cyklicznych zadań cron.
| Kolumna | Opis |
|---------|------|
| id | PK auto increment |
| job_type | Typ zadania (VARCHAR 50, UNIQUE) |
| interval_seconds | Interwał uruchomienia w sekundach |
| priority | Priorytet tworzonych zadań (TINYINT) |
| max_attempts | Maks. prób dla tworzonych zadań (SMALLINT) |
| payload | Opcjonalny payload JSON (TEXT NULL) |
| enabled | Czy harmonogram aktywny (TINYINT 1) |
| last_run_at | Ostatnie uruchomienie (DATETIME NULL) |
| next_run_at | Następne planowane uruchomienie (DATETIME NULL) |
| created_at | Data utworzenia (DATETIME) |
**Indeksy:** idx_enabled_next_run (enabled, next_run_at)
**Używane w:** `Domain\CronJob\CronJobRepository`, `Domain\CronJob\CronJobProcessor`
**Dodano w wersji 0.324.**
## pp_routes
Tabela tras URL — mapowanie wzorców URL (regex) na parametry GET. Zastępuje reguły `RewriteRule` w `.htaccess` dla wszystkich URL-i aplikacji: produktów, kategorii, stron, artykułów oraz systemowych (koszyk, logowanie, newsletter, itp.).
| Kolumna | Opis |
|---------|------|
| id | Klucz główny (AUTO_INCREMENT) |
| product_id | ID produktu (INT NULL) — wypełnione dla tras produktów |
| category_id | ID kategorii (INT NULL) — wypełnione dla tras kategorii |
| page_id | ID strony (INT NULL) — wypełnione dla tras stron |
| article_id | ID artykułu (INT NULL) — wypełnione dla tras artykułów |
| type | Typ trasy: NULL = encja (produkt/kategoria/strona/artykuł), `'system'` = trasa systemowa (koszyk, logowanie, newsletter, AJAX moduły, itp.) |
| lang_id | ID języka (0 dla tras systemowych niezwiązanych z językiem) |
| pattern | Wyrażenie regularne dopasowywane do REQUEST_URI |
| destination | Docelowy query string, np. `index.php?category=5&lang=1` |
**Mechanizm:** `index.php` ładuje wszystkie trasy (z cache Redis `pp_routes:all`) przed `checkUrlParams()`, dopasowuje `pattern` do ścieżki żądania i ustawia `$_GET` z `destination`. Obsługuje grupy przechwytujące (np. `$1` dla paginacji).
**Trasy systemowe:** Przy każdym wywołaniu `Helpers::htacces()` wszystkie rekordy z `type='system'` są usuwane i wstawiane na nowo (32 statycznych + dynamiczne trasy językowe i producentów). Zarządzane automatycznie — nie edytować ręcznie.
**Cache:** Redis klucz `pp_routes:all`, TTL 86400s. Invalidowany automatycznie przy każdym wywołaniu `Helpers::htacces()`.
**Używane w:** `index.php`, `Shared\Helpers\Helpers::htacces()`, `Domain\Product\ProductRepository`, `Domain\Category\CategoryRepository`, `Domain\Pages\PagesRepository`, `Domain\Article\ArticleRepository`
**Dodano w wersji 0.329. Kolumna `type` i trasy systemowe dodane w wersji 0.330.**

View File

@@ -1,178 +0,0 @@
# Form Edit System - Dokumentacja użycia
## Architektura
```
┌─────────────────────────────────────────────────────────────┐
│ Controller │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ edit() │ │ save() │ │
│ │ - buduje VM │ │ - walidacja │ │
│ │ - renderuje │ │ - zapis │ │
│ └────────┬────────┘ └─────────────────┘ │
└───────────┼─────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ FormEditViewModel │
│ - title, formId, data, fields, tabs, actions │
│ - validationErrors, persist, languages │
└───────────┬─────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ components/form-edit.php (szablon) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ FormFieldRenderer - renderuje każde pole │ │
│ │ ├─ input, select, textarea, switch │ │
│ │ ├─ date, datetime, editor, image │ │
│ │ └─ lang_section (zagnieżdżone pola) │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
## Pliki systemu
| Plik | Opis |
|------|------|
| `autoload/admin/ViewModels/Forms/FormFieldType.php` | Stale typow pol |
| `autoload/admin/ViewModels/Forms/FormField.php` | Factory methods per typ |
| `autoload/admin/ViewModels/Forms/FormTab.php` | Zakladki |
| `autoload/admin/ViewModels/Forms/FormAction.php` | Akcje (zapisz, anuluj) |
| `autoload/admin/ViewModels/Forms/FormEditViewModel.php` | ViewModel formularza |
| `autoload/admin/Support/Forms/FormValidator.php` | Walidacja pol |
| `autoload/admin/Support/Forms/FormRequestHandler.php` | Obsluga POST + persist |
| `autoload/admin/Support/Forms/FormFieldRenderer.php` | Renderowanie HTML |
| `admin/templates/components/form-edit.php` | Uniwersalny szablon |
## Przykład użycia w kontrolerze
```php
use admin\ViewModels\Forms\FormEditViewModel;
use admin\ViewModels\Forms\FormField;
use admin\ViewModels\Forms\FormTab;
use admin\ViewModels\Forms\FormAction;
use admin\Support\Forms\FormRequestHandler;
class BannerController
{
public function edit(): string
{
$banner = $this->repository->find($id);
$languages = \admin\factory\Languages::languages_list();
$viewModel = new FormEditViewModel(
formId: 'banner-edit',
title: 'Edycja banera',
data: $banner,
tabs: [
new FormTab('settings', 'Ustawienia', 'fa-wrench'),
new FormTab('content', 'Zawartość', 'fa-file'),
],
fields: [
// Zakładka Ustawienia
FormField::text('name', [
'label' => 'Nazwa',
'tab' => 'settings',
'required' => true,
]),
FormField::switch('status', [
'label' => 'Aktywny',
'tab' => 'settings',
]),
FormField::date('date_start', [
'label' => 'Data rozpoczęcia',
'tab' => 'settings',
]),
// Sekcja językowa w zakładce Zawartość
FormField::langSection('translations', 'content', [
FormField::image('src', ['label' => 'Obraz']),
FormField::text('url', ['label' => 'Url']),
FormField::editor('text', ['label' => 'Treść']),
]),
],
actions: [
FormAction::save('/admin/banners/save', '/admin/banners'),
FormAction::cancel('/admin/banners'),
],
languages: $languages,
persist: true,
);
return \Tpl::view('components/form-edit', ['form' => $viewModel]);
}
public function save(): void
{
$formHandler = new FormRequestHandler();
$viewModel = $this->buildFormViewModel(); // jak w edit()
$result = $formHandler->handleSubmit($viewModel, $_POST);
if (!$result['success']) {
// Błędy walidacji - zapisane automatycznie do sesji
echo json_encode(['success' => false, 'errors' => $result['errors']]);
exit;
}
// Sukces - persist wyczyszczony automatycznie
$this->repository->save($result['data']);
echo json_encode(['success' => true]);
exit;
}
}
```
## Dostępne typy pól
| Typ | Metoda | Opcje |
|-----|--------|-------|
| `text` | `FormField::text(name, ['label' => '...', 'required' => true])` | placeholder, help |
| `number` | `FormField::number(name, [...])` | - |
| `email` | `FormField::email(name, [...])` | walidacja formatu |
| `password` | `FormField::password(name, [...])` | - |
| `date` | `FormField::date(name, [...])` | datetimepicker |
| `datetime` | `FormField::datetime(name, [...])` | datetimepicker z czasem |
| `switch` | `FormField::switch(name, [...])` | checked (bool) |
| `select` | `FormField::select(name, ['options' => [...]])` | options: [key => label] |
| `textarea` | `FormField::textarea(name, ['rows' => 4])` | rows |
| `editor` | `FormField::editor(name, ['toolbar' => 'MyTool'])` | CKEditor |
| `image` | `FormField::image(name, ['filemanager' => true])` | filemanager URL |
| `file` | `FormField::file(name, [...])` | filemanager |
| `hidden` | `FormField::hidden(name, value)` | - |
| `color` | `FormField::color(name, ['label' => '...'])` | HTML5 color picker + text input |
| `lang_section` | `FormField::langSection(name, 'tab', [fields])` | pola per język |
## Walidacja
Walidacja jest automatyczna na podstawie właściwości pól:
- `required` - pole wymagane
- `type` = `email` - walidacja formatu e-mail
- `type` = `number` - walidacja liczby
- `type` = `date` - walidacja formatu YYYY-MM-DD
Dla sekcji językowych walidacja jest powtarzana dla każdego aktywnego języka.
## Persist (zapamiętywanie danych)
Gdy `persist = true`:
1. Przy błędzie walidacji dane są zapisywane w `$_SESSION['form_persist'][$formId]`
2. Formularz automatycznie przywraca dane z sesji przy ponownym wyświetleniu
3. Po udanym zapisie sesja jest czyszczona automatycznie przez `FormRequestHandler`
## Przerabianie istniejących formularzy
1. **Kontroler** - zamień `view\Xxx::edit()` na `FormEditViewModel`
2. **Repository** - dostosuj `save()` do formatu z `FormRequestHandler` (lub dodaj wsparcie dla obu formatów)
3. **Szablon** - usuń stary szablon lub zostaw jako fallback
4. **Testy** - zaktualizuj testy jeśli zmienił się format danych
## Aktualizacja 2026-02-15 (ver. 0.275)
- Modul `ShopCategory` zostal zmigrowany do warstwy Domain + DI, ale formularz kategorii nadal korzysta z legacy `gridEdit`.
- W ramach migracji wydzielono skrypty UI do osobnych partiali `*-custom-script.php` (lista, browse, edycja, produkty), co upraszcza dalsze przepiecie formularza na `components/form-edit`.
- Po migracji `ShopCategory` kolejnym kandydatem do pelnej migracji formularza na Form Edit System pozostaje modul `Order` (zgodnie z `REFACTORING_PLAN.md`).
---
*Dokument aktualizowany: 2026-02-15*

View File

@@ -1,42 +0,0 @@
# Pamięć projektu shopPRO
Notatki i wnioski zebrane podczas pracy z kodem. Aktualizowane na bieżąco.
---
## Serwer produkcyjny
- PHP < 8.0 — unikać `match`, named arguments, union types, `str_contains()` itp.
- Zamiast `match` używać operatorów trójargumentowych (ternary) lub `if/else`
## Znane problemy / TODO
- `\Shared\Helpers\Helpers::send_email()` i `Shared\Email\Email::send()` — zduplikowana logika PHPMailer. Docelowo zunifikować w `Shared\Email\Email`
## Wzorce potwierdzone w projekcie
- Metody frontendowe (z cache Redis) dodawane do istniejących repozytoriów Domain — NIE tworzymy osobnych FrontendService/AdminService
- Klasy View (`front\Views\*`) są statyczne i bezstanowe — nie wymagają DI
- Kontrolery (`Controllers\*`) są instancyjne z DI przez konstruktor
- Autoloader obsługuje dwa formaty plików: `class.X.php` (legacy) i `X.php` (nowy) — oba działają bez zmian w autoloaderze
- Nowe katalogi z dużej litery: `Views/`, `Controllers/` (legacy: `view/`, `controls/`, `factory/`)
## Medoo ORM — pułapki
- `$mdb->delete()` przyjmuje 2 argumenty (tabela, warunek), NIE 3 — wielokrotnie powodowało bugi (np. `newsletter_unsubscribe`)
- `$mdb->get()` zwraca `null` gdy brak rekordu, NIE `false`
- Przy `$mdb->insert()` sprawdzać `$mdb->id()` aby potwierdzić sukces
## Redis cache — konwencje
- TTL domyślnie 86400 (24h)
- Klucze produktów: `shop\product:{id}:{lang}:{permutation_hash}`
- Wzorzec czyszczenia: `CacheHandler::deletePattern("shop\\product:{$id}:*")`
- Dane w cache są serializowane — wymagają `unserialize()` po `get()`
## Aktualizacje klienckie
- Pliki `*.md` NIGDY nie trafiają do ZIP aktualizacji
- `updates/changelog.php` to plik serwisowy repozytorium, nie runtime klienta
- Główny `.htaccess` wdrażany osobno, poza ZIP aktualizacji
- W archiwum ZIP NIE powinno być folderu z nazwą wersji — struktura zaczyna się od katalogów projektu

View File

@@ -1,150 +0,0 @@
# PAUL — Workflow dla shopPRO
[PAUL](https://github.com/ChristopherKahler/paul) to zestaw komend dla Claude Code, który strukturyzuje pracę nad projektem: planowanie → implementacja → weryfikacja. Działa przez slash-komendy w czacie z Claude.
---
## Jednorazowa inicjalizacja
```
/paul:init
```
Uruchom raz — tworzy plik `.paul/` z konfiguracją projektu (milestones, fazy). Jeśli już zainicjalizowany, pomijasz ten krok.
---
## Nowa funkcja (feature)
### 1. Omów wizję
```
/paul:discuss
```
Claude zadaje pytania, żeby doprecyzować, co dokładnie chcesz zbudować — wynik to jasna definicja przed planowaniem.
### 2. Zbadaj opcje techniczne (opcjonalnie)
```
/paul:discover
```
Przydatne gdy nie jesteś pewny jak zaimplementować coś technicznie — Claude przegląda kod i proponuje podejścia.
### 3. Zaplanuj implementację
```
/paul:plan
```
Tworzy szczegółowy plan z krokami, plikami do zmiany, kolejnością. Zatwierdź plan zanim ruszysz dalej.
### 4. Wykonaj plan
```
/paul:apply
```
Claude implementuje plan krok po kroku, z commitami na każdą fazę.
### 5. Zweryfikuj (UAT)
```
/paul:verify
```
Claude generuje checklistę testów manualnych — punkt po punkcie sprawdzasz czy funkcja działa.
---
## Poprawka błędu (bug fix)
```
/paul:plan-fix
```
Dedykowany flow dla bugów: opisujesz problem → Claude analizuje kod → tworzy plan naprawy → `/paul:apply` wykonuje.
Krótszy wariant dla prostych bugów — możesz też po prostu opisać błąd i użyć `/paul:plan``/paul:apply`.
---
## Zarządzanie sesjami
| Komenda | Kiedy |
|---------|-------|
| `/paul:progress` | Sprawdź gdzie jesteś, co dalej |
| `/paul:pause` | Kończysz sesję — tworzy plik handoff |
| `/paul:resume` | Zaczynasz nową sesję — wczytuje kontekst |
| `/paul:handoff` | Generuje dokument przekazania pracy |
---
## Milestones (większe wdrożenia)
Gdy planujesz większą wersję (np. nowy moduł, refactor):
```
/paul:milestone # Tworzysz nowy milestone
/paul:add-phase # Dodajesz fazę do milestone
/paul:complete-milestone # Zamykasz po wdrożeniu
```
---
## Czy PAUL może przejrzeć projekt pod kątem błędów?
Tak, częściowo. Najlepsze podejście:
### Mapowanie kodu
```
/paul:map-codebase
```
Generuje analizę architektury — wyłapuje niespójności, martwy kod, problematyczne zależności.
### Research konkretnego obszaru
```
/paul:research
```
Np. _"Sprawdź czy walidacja zamówień w OrderRepository jest kompletna"_ — Claude przegląda kod i raportuje problemy.
### Ograniczenia
PAUL nie jest narzędziem do statycznej analizy kodu (jak PHPStan czy psalm). Do systematycznego przeglądu błędów lepiej połączyć:
- **PAUL** (`/paul:map-codebase` + `/paul:research`) — problemy architektoniczne, logika biznesowa
- **Testy** (`./test.ps1`) — regresje, złe zachowanie metod
- **PHPStan** (jeśli dodany do projektu) — typy, niezdefiniowane zmienne
---
## Typowy dzień pracy w shopPRO
```
# Rano — wznów kontekst
/paul:resume
# W trakcie — sprawdź co dalej
/paul:progress
# Nowa funkcja lub bug
/paul:discuss → /paul:plan → /paul:apply → /paul:verify
# Na koniec dnia
/paul:pause
```
---
## Dobre praktyki
- Zawsze zatwierdzaj plan (`/paul:plan`) zanim uruchomisz `/paul:apply` — łatwiej zmienić plan niż cofnąć kod
- Po każdym `/paul:apply` odpal testy: `./test.ps1`
- Używaj `/paul:verify` przed mergem — generuje checklistę zamiast zgadywać co przetestować
- Przy bugach najpierw opisz symptom dokładnie, Claude lepiej planuje naprawę mając konkretny przypadek

View File

@@ -1,190 +0,0 @@
# Struktura Projektu shopPRO
Aktualna architektura po zakonczonej migracji na Domain-Driven Design + Dependency Injection.
## Warstwa domenowa (`autoload/Domain/`)
Kazdy modul zawiera Repository (i opcjonalnie dodatkowe klasy). Konstruktor DI z `$db` (Medoo). Metody sluza zarowno adminowi, jak i frontendowi (wspolna warstwa).
| Modul | Klasy | Uwagi |
|-------|-------|-------|
| Article | ArticleRepository | blog, aktualnosci, galerie, pliki |
| Attribute | AttributeRepository | cechy produktow + wartosci |
| Banner | BannerRepository | banery glowne + boczne, Redis cache |
| Basket | BasketCalculator | summary, count, walidacja stanow |
| Cache | CacheRepository | czyszczenie cache z poziomu admin |
| Category | CategoryRepository | drzewa kategorii, produkty w kategorii, Redis cache |
| Client | ClientRepository | CRUD, auth, adresy, zamowienia |
| Coupon | CouponRepository | kupony rabatowe, walidacja, uzycie |
| CronJob | CronJobType, CronJobRepository, CronJobProcessor | kolejka zadan cron (DB), priorytety, retry/backoff, harmonogram |
| Dashboard | DashboardRepository | statystyki admin, Redis cache |
| Dictionaries | DictionariesRepository | slowniki admin |
| Integrations | IntegrationsRepository | Apilo sync, ustawienia |
| Languages | LanguagesRepository | jezyki, tlumaczenia |
| Layouts | LayoutsRepository | layouty stron, 3-level fallback |
| Newsletter | NewsletterRepository, NewsletterPreviewRenderer | subskrypcje, szablony, kolejka wysylki |
| Order | OrderRepository, OrderAdminService | CRUD, Apilo sync, webhooki platnosci, kolejka retry |
| Pages | PagesRepository | strony, menu, drzewa stron |
| PaymentMethod | PaymentMethodRepository | metody platnosci, mapowanie Apilo |
| Producer | ProducerRepository | producenci |
| Product | ProductRepository | CRUD, cache, kombinacje, zdjecia, Google Feed XML |
| ProductSet | ProductSetRepository | zestawy produktow |
| Promotion | PromotionRepository | promocje, 5 typow applyType*, silnik dopasowania |
| Scontainers | ScontainersRepository | kontenery sidebaru |
| Settings | SettingsRepository | ustawienia sklepu |
| ShopStatus | ShopStatusRepository | statusy zamowien, mapowanie Apilo |
| Transport | TransportRepository | transport, koszty, powiazanie z platnosci |
| Update | UpdateRepository | aktualizacje, migracje SQL |
| User | UserRepository | uzytkownicy admin, 2FA, logowanie |
## Warstwa admin (`autoload/admin/`)
### Router: `admin\App`
- `getControllerFactories()` — mapa kontrolerow z DI wiring
- Brak fallbacku na legacy — wszystkie moduly na nowych kontrolerach
### Kontrolery (`admin\Controllers\`) — 28 kontrolerow
ArticlesArchive, Articles, Banner, Dashboard, Dictionaries, Filemanager, Integrations, Languages, Layouts, Newsletter, Pages, ProductArchive, Scontainers, Settings, ShopAttribute, ShopCategory, ShopClients, ShopCoupon, ShopOrder, ShopPaymentMethod, ShopProducer, ShopProduct, ShopProductSets, ShopPromotion, ShopStatuses, ShopTransport, Update, Users
### Support
- `admin\Support\TableListRequestFactory` — paginacja/sortowanie tabel
- `admin\Support\Forms\FormRequestHandler` — obsluga formularzy (persist przy bledach)
- `admin\Support\Forms\FormFieldRenderer` — renderowanie pol formularzy
### ViewModels
- `admin\ViewModels\Forms\` — FormEditViewModel, FormField, FormTab, FormAction, FormFieldType
- `admin\ViewModels\Common\PaginatedTableViewModel`
### Walidacja
- `admin\Validation\FormValidator` — reguly per pole, sekcje jezykowe
## Warstwa frontend (`autoload/front/`)
### Router: `front\App`
- `route()`, `checkUrlParams()`, `getControllerFactories()`
### Layout Engine: `front\LayoutEngine`
- `show()` — zamiana tagow szablonowych (kategorie, produkty, menu, banery, artykuly, kontenery, meta)
- `contact()`, `cookieInformation()`
### Kontrolery (`front\Controllers\`) — 8 kontrolerow
Newsletter, Search, ShopBasket, ShopClient, ShopCoupon, ShopOrder, ShopProducer, ShopProduct
### Widoki (`front\Views\`) — 11 klas statycznych
Articles, Banners, Languages, Menu, Newsletter, Scontainers, ShopCategory, ShopClient, ShopPaymentMethod, ShopProduct, ShopSearch
## Warstwa API (`autoload/api/`)
REST API dla ordersPRO. Entry point: `api.php`. Stateless (bez sesji), autentykacja przez `X-Api-Key` header.
### Router: `api\ApiRouter`
- `handle()` — autentykacja → routing → dispatch
- Helpery statyczne: `sendSuccess()`, `sendError()`, `getJsonBody()`, `requireMethod()`
### Kontrolery (`api\Controllers\`)
- `OrdersApiController` — lista, szczegoly, zmiana statusu, platnosc (5 akcji)
- `ProductsApiController` — lista, szczegoly, tworzenie, aktualizacja produktow (4 akcje)
- `DictionariesApiController` — statusy, transporty, metody platnosci (3 akcje)
- `CategoriesApiController` — lista aktywnych kategorii (1 akcja)
Dokumentacja: `docs/API.md`
## Warstwa wspoldzielona (`autoload/Shared/`)
| Klasa | Opis |
|-------|------|
| `Shared\Cache\CacheHandler` | Redis cache: get/set/delete/deletePattern |
| `Shared\Cache\RedisConnection` | Singleton polaczenia Redis |
| `Shared\Email\Email` | Wrapper PHPMailer |
| `Shared\Helpers\Helpers` | SEO, email, cache clearing, shortPrice, utility |
| `Shared\Html\Html` | Helpery HTML |
| `Shared\Image\ImageManipulator` | Obrobka obrazow GD |
| `Shared\Tpl\Tpl` | Silnik szablonow: render(), set() |
## Cache Redis
### Klucze
```
shop\product:{id}:{lang}:{permutation_hash} — dane produktu (TTL 24h)
ProductRepository::getProductPermutationQuantityOptions:v2:{id}:{perm} — ilosc + komunikaty
ProductRepository::productSetsWhenAddToBasket:{id} — zestawy "kupowane razem"
```
### Konwencje
- TTL domyslnie 86400 (24h)
- Dane serializowane — `unserialize()` po `get()`
- Czyszczenie: `CacheHandler::deletePattern("shop\\product:{$id}:*")`
- Czyszczenie z poziomu admin: `Shared\Helpers\Helpers::clear_product_cache($id)`
- Przycisk "Wyczysc cache" w admin: `SettingsController::clearCacheAjax()``flushAll()` Redis + `temp/` + `thumbs/`
## Entry pointy
| Plik | Rola |
|------|------|
| `index.php` | Frontend — autoload, sesja, DB, routing (`front\App`), layout (`front\LayoutEngine`), DOM post-processing |
| `ajax.php` | Frontend AJAX — koszyk, transport, kontakt |
| `api.php` | REST API (ordersPRO + Ekomi CSV) — router: `\api\ApiRouter`, kontrolery: `\api\Controllers\` |
| `admin/index.php` | Admin — autoload, sesja, DB, routing (`admin\App`) |
| `admin/ajax.php` | Admin AJAX |
| `cron.php` | CRON: Apilo sync (ceny/stany co 10min, cennik co 1h, retry queue) |
| `cron-turstmate.php` | TrustMate integracja |
| `cron/cron-xml.php` | Google Feed XML |
| `download.php` | Pobieranie plikow |
### Autoloader
Kazdy entry point rejestruje `__autoload_my_classes()`:
1. Probuje `autoload/{namespace}/class.{ClassName}.php` (legacy format)
2. Probuje `autoload/{namespace}/{ClassName}.php` (PSR-4 format)
### Routing frontend (index.php)
Przed `front\App::route()`:
1. Sprawdza tabele `pp_redirects` → 301 redirect
2. Sprawdza tabele `pp_routes` → regex pattern → destination
### Newsletter queue
`index.php` wywoluje `$newsletterRepo->sendQueued()` na koncu kazdego requestu frontendowego (limit 1 mail/request).
## Integracje zewnetrzne
### Apilo (cron.php)
- Synchronizacja cen/stanow produktow (co 10 min)
- Synchronizacja cennika (co 1h)
- Kolejka retry: `temp/apilo-sync-queue.json``OrderAdminService::processApiloSyncQueue()`
- Mapowanie statusow i platnosci przez tabele `pp_shop_statuses` i `pp_shop_payment_methods`
### Webhooki platnosci (front\Controllers\ShopOrderController)
- tPay, Przelewy24, Hotpay — ujednolicone: `set_as_paid` + `update_status`
## Biblioteki (`libraries/`)
- `medoo/medoo.php` — Medoo ORM (`$mdb`)
- `rb.php` — RedBeanPHP ORM (`\R::`, `$pdo`)
- `phpmailer/` — PHPMailer
## Wzorce architektoniczne
### DI zamiast global
```php
// Kontroler wiring (w admin\App lub front\App)
$repo = new \Domain\Example\ExampleRepository($mdb);
$controller = new \admin\Controllers\ExampleController($repo);
```
### Wspolna warstwa Domain
Metody frontendowe (z Redis cache) dodawane do istniejacych repozytoriow — NIE tworzymy osobnych FrontendService/AdminService.
### Klasy View — statyczne, bezstanowe
`front\Views\*` — nie wymagaja DI. Czyste funkcje: dane wchodza, HTML wychodzi.
### Kontrolery — instancyjne z DI
`Controllers\*` — repozytoria wstrzykiwane przez konstruktor.
### Nazewnictwo plikow
- Nowe: `ClassName.php`
- Legacy (pozostalosci): `class.ClassName.php`
- Autoloader obsluguje oba formaty
### Nazewnictwo katalogow
- Nowe: z duzej litery (`Views/`, `Controllers/`)
- Namespace `\admin\` z malej (bo katalog `admin/` jest z malej na serwerze Linux)
- NIE uzywac `\Admin\` (duze A)

View File

@@ -1,131 +0,0 @@
# Testowanie shopPRO
## Szybki start
```bash
# Pelny suite (PowerShell — rekomendowane)
./test.ps1
# Konkretny plik
./test.ps1 tests/Unit/Domain/Product/ProductRepositoryTest.php
# Konkretny test
./test.ps1 --filter testGetQuantityReturnsCorrectValue
# Alternatywne
composer test # standard
./test.bat # testdox (czytelna lista)
./test-simple.bat # kropki
./test-debug.bat # debug
./test.sh # Git Bash
```
## Aktualny stan
```text
OK (821 tests, 2278 assertions)
```
Zweryfikowano: 2026-04-16 (ver. 0.346)
## Konfiguracja
- **PHPUnit 9.6** via `phpunit.phar`
- **Bootstrap:** `tests/bootstrap.php`
- **Config:** `phpunit.xml`
## Struktura testow
```
tests/
|-- bootstrap.php
|-- stubs/
| |-- CacheHandler.php (inline w bootstrap)
| |-- Helpers.php (Shared\Helpers\Helpers stub)
| `-- ShopProduct.php (shop\Product stub)
|-- Unit/
| |-- Domain/
| | |-- Article/ArticleRepositoryTest.php
| | |-- Attribute/AttributeRepositoryTest.php
| | |-- Banner/BannerRepositoryTest.php
| | |-- Basket/BasketCalculatorTest.php
| | |-- Cache/CacheRepositoryTest.php
| | |-- Category/CategoryRepositoryTest.php
| | |-- Coupon/CouponRepositoryTest.php
| | |-- CronJob/CronJobTypeTest.php
| | |-- CronJob/CronJobRepositoryTest.php
| | |-- CronJob/CronJobProcessorTest.php
| | |-- Dictionaries/DictionariesRepositoryTest.php
| | |-- Integrations/IntegrationsRepositoryTest.php
| | |-- Languages/LanguagesRepositoryTest.php
| | |-- Layouts/LayoutsRepositoryTest.php
| | |-- Newsletter/NewsletterRepositoryTest.php
| | |-- Pages/PagesRepositoryTest.php
| | |-- PaymentMethod/PaymentMethodRepositoryTest.php
| | |-- Producer/ProducerRepositoryTest.php
| | |-- Product/ProductRepositoryTest.php
| | |-- ProductSet/ProductSetRepositoryTest.php
| | |-- Promotion/PromotionRepositoryTest.php
| | |-- Settings/SettingsRepositoryTest.php
| | |-- ShopStatus/ShopStatusRepositoryTest.php
| | |-- Transport/TransportRepositoryTest.php
| | |-- Update/UpdateRepositoryTest.php
| | `-- User/UserRepositoryTest.php
| |-- Shared/
| | `-- Security/
| | `-- CsrfTokenTest.php
| `-- admin/
| `-- Controllers/
| |-- ArticlesControllerTest.php
| |-- DictionariesControllerTest.php
| |-- IntegrationsControllerTest.php
| |-- ProductArchiveControllerTest.php
| |-- SettingsControllerTest.php
| |-- ShopAttributeControllerTest.php
| |-- ShopCategoryControllerTest.php
| |-- ShopCouponControllerTest.php
| |-- ShopPaymentMethodControllerTest.php
| |-- ShopProducerControllerTest.php
| |-- ShopProductControllerTest.php
| |-- ShopProductSetsControllerTest.php
| |-- ShopPromotionControllerTest.php
| |-- ShopStatusesControllerTest.php
| |-- ShopTransportControllerTest.php
| `-- UsersControllerTest.php
| |-- front/Controllers/
| | `-- ShopBasketControllerTest.php
| `-- api/
| |-- ApiRouterTest.php
| `-- Controllers/
| |-- OrdersApiControllerTest.php
| |-- ProductsApiControllerTest.php
| `-- DictionariesApiControllerTest.php
`-- Integration/ (puste — zarezerwowane)
```
## Dodawanie nowych testow
1. Plik w `tests/Unit/Domain/<Module>/<Class>Test.php`, `tests/Unit/admin/Controllers/<Class>Test.php` lub `tests/Unit/api/Controllers/<Class>Test.php`.
2. Rozszerz `PHPUnit\Framework\TestCase`.
3. Nazwy metod zaczynaj od `test`.
4. Wzorzec AAA: Arrange, Act, Assert.
## Mockowanie Medoo
```php
$mockDb = $this->createMock(\medoo::class);
$mockDb->method('get')->willReturn(42);
$repo = new ProductRepository($mockDb);
$value = $repo->getQuantity(123);
$this->assertEquals(42, $value);
```
## Bootstrap — stuby
`tests/bootstrap.php` rejestruje autoloader i definiuje stuby:
- `Redis`, `RedisConnection` — klasy Redis (aby nie wymagac rozszerzenia)
- `Shared\Cache\CacheHandler` — inline stub z `get()`/`set()`/`exists()`/`delete()`/`deletePattern()`
- `Shared\Helpers\Helpers` — z `tests/stubs/Helpers.php`
- `shop\Product` — z `tests/stubs/ShopProduct.php`

View File

@@ -1,116 +0,0 @@
3. Dodać uwierzytelnienie dwuskładnikowe za pomocą aplikacji.
4. Dodać zarządzanie uprawnieniami na poziomie urzytkownika, na razie uprawnienia do poszczególnych modułów.
naprawić działanie newslettera i zapis do bazy newslettera
program lojalnościowy
proponowane produkty w koszyku
Do zamówień w statusie: realizowane lub oczekuje na wpłatę. Opcja tylko dla zarejestrowanych klientów. https://royal-stone.pl/pl/order1.html
Dodać możliwość ustawienia limitu znaków w wiadomościach do produktu
8. [] Przerobić analitykę Google Analytics i Google ADS
9. [x] Rozważyć integrację SonarQube (statyczna analiza kodu PHP — bugi, security, code smells). Community Edition darmowy, self-hosted. Wymaga serwera + MCP server w Claude Code.
## SonarQube — 0.340 (2026-03-15)
### Bugs
- [ ] [MAJOR] cron.php:192 — Review the data-flow - use of uninitialized value (php:S836)
- [ ] [MAJOR] cron.php:561 — Review the data-flow - use of uninitialized value (php:S836)
- [ ] [MAJOR] cron.php:590 — Review the data-flow - use of uninitialized value (php:S836)
- [ ] [MAJOR] cron.php:643 — Review the data-flow - use of uninitialized value (php:S836)
### Code Smells — CRITICAL
- [ ] [CRITICAL] autoload/Domain/Integrations/ApiloRepository.php:35 — Add curly braces around nested statement(s) (php:S121)
- [ ] [CRITICAL] autoload/Domain/Integrations/ApiloRepository.php:66 — Define a constant instead of duplicating "Accept: application/json" 5 times (php:S1192)
- [ ] [CRITICAL] autoload/Domain/Integrations/ApiloRepository.php:77 — Add curly braces around nested statement(s) (php:S121)
- [ ] [CRITICAL] autoload/Domain/Integrations/ApiloRepository.php:159 — Define a constant instead of duplicating "Y-m-d H:i:s" 3 times (php:S1192)
- [ ] [CRITICAL] autoload/Domain/Integrations/ApiloRepository.php:239 — Add curly braces around nested statement(s) (php:S121)
- [ ] [CRITICAL] autoload/Domain/Integrations/ApiloRepository.php:309 — Add curly braces around nested statement(s) (php:S121)
- [ ] [CRITICAL] autoload/Domain/Integrations/ApiloRepository.php:315 — Add curly braces around nested statement(s) (php:S121)
- [ ] [CRITICAL] autoload/Domain/Integrations/ApiloRepository.php:339 — Define a constant instead of duplicating "Authorization: Bearer " 3 times (php:S1192)
- [ ] [CRITICAL] autoload/Domain/Integrations/ApiloRepository.php:359 — Add curly braces around nested statement(s) (php:S121)
- [ ] [CRITICAL] autoload/Domain/Integrations/ApiloRepository.php:400 — Refactor this function to reduce its Cognitive Complexity (php:S3776)
- [ ] [CRITICAL] autoload/front/Controllers/ShopBasketController.php:499 — Add curly braces around nested statement(s) (php:S121)
- [ ] [CRITICAL] autoload/front/Controllers/ShopBasketController.php:502 — Add curly braces around nested statement(s) (php:S121)
- [ ] [CRITICAL] autoload/api/Controllers/ProductsApiController.php:396 — Refactor this function to reduce its Cognitive Complexity from 83 to 15 (php:S3776)
- [ ] [CRITICAL] autoload/Shared/Helpers/Helpers.php:408 — Refactor this function to reduce its Cognitive Complexity from 165 to 15 (php:S3776)
- [ ] [CRITICAL] autoload/Shared/Helpers/Helpers.php:520 — Define a constant instead of duplicating "/([0-9]+)$" 3 times (php:S1192)
- [ ] [CRITICAL] autoload/Shared/Helpers/Helpers.php:607 — Define a constant instead of duplicating " Order Deny,Allow" 3 times (php:S1192)
- [ ] [CRITICAL] autoload/Shared/Helpers/Helpers.php:650 — Define a constant instead of duplicating "&lang=" 7 times (php:S1192)
- [ ] [CRITICAL] cron.php:200 — Define a constant instead of duplicating "Y-m-d H:i:s" 7 times (php:S1192)
- [ ] [CRITICAL] cron.php:200 — Add curly braces around nested statement(s) (php:S121)
- [ ] [CRITICAL] cron.php:203 — Add curly braces around nested statement(s) (php:S121)
- [ ] [CRITICAL] cron.php:418 — Define a constant instead of duplicating "Authorization: Bearer " 5 times (php:S1192)
- [ ] [CRITICAL] cron.php:419 — Define a constant instead of duplicating "Accept: application/json" 5 times (php:S1192)
- [ ] [CRITICAL] cron.php:526 — Add curly braces around nested statement(s) (php:S121)
- [ ] [CRITICAL] cron.php:529 — Add curly braces around nested statement(s) (php:S121)
- [ ] [CRITICAL] cron.php:531 — Add curly braces around nested statement(s) (php:S121)
- [ ] [CRITICAL] cron.php:533 — Add curly braces around nested statement(s) (php:S121)
- [ ] [CRITICAL] cron.php:542 — Add curly braces around nested statement(s) (php:S121)
- [ ] [CRITICAL] cron.php:545 — Add curly braces around nested statement(s) (php:S121)
- [ ] [CRITICAL] cron.php:547 — Add curly braces around nested statement(s) (php:S121)
- [ ] [CRITICAL] cron.php:555 — Add curly braces around nested statement(s) (php:S121)
- [ ] [CRITICAL] cron.php:559 — Add curly braces around nested statement(s) (php:S121)
### Code Smells — MAJOR
- [ ] [MAJOR] autoload/Domain/Integrations/ApiloRepository.php:130 — Method has 4 returns, max 3 allowed (php:S1142)
- [ ] [MAJOR] autoload/Domain/Integrations/ApiloRepository.php:233 — Method has 5 returns, max 3 allowed (php:S1142)
- [ ] [MAJOR] autoload/Domain/Integrations/ApiloRepository.php:307 — Method has 7 returns, max 3 allowed (php:S1142)
- [ ] [MAJOR] autoload/Domain/Integrations/ApiloRepository.php:400 — Method has 8 returns, max 3 allowed (php:S1142)
- [ ] [MAJOR] autoload/Domain/Integrations/ApiloRepository.php:449 — Method has 4 returns, max 3 allowed (php:S1142)
- [ ] [MAJOR] autoload/Domain/Integrations/ApiloRepository.php:481 — Method has 4 returns, max 3 allowed (php:S1142)
- [ ] [MAJOR] autoload/Domain/Integrations/ApiloRepository.php:513 — Method has 4 returns, max 3 allowed (php:S1142)
- [ ] [MAJOR] autoload/front/Controllers/ShopBasketController.php:493 — Method has 4 returns, max 3 allowed (php:S1142)
- [ ] [MAJOR] autoload/Domain/Order/OrderAdminService.php:673 — Method has 4 returns, max 3 allowed (php:S1142)
- [ ] [MAJOR] autoload/Domain/Order/OrderAdminService.php:740 — Method has 4 returns, max 3 allowed (php:S1142)
### Code Smells — MINOR
- [ ] [MINOR] autoload/Domain/Order/OrderRepository.php — Add a new line at the end of file (php:S113)
- [ ] [MINOR] admin/templates/site/unlogged-layout.php — Add a new line at the end of file (php:S113)
- [ ] [MINOR] admin/templates/users/user-2fa.php — Add a new line at the end of file (php:S113)
- [ ] [MINOR] autoload/admin/Controllers/ProductArchiveController.php:196 — Rename function "bulk_delete_permanent" to match camelCase (php:S100)
- [ ] [MINOR] autoload/api/ApiRouter.php:107 — Remove unused "$db" local variable (php:S1481)
- [ ] [MINOR] cron.php:198 — Remove unused "$orderAdminService" local variable (php:S1481)
- [ ] [MINOR] cron.php:524 — Remove unused "$mdb" local variable (php:S1481)
- [ ] [MINOR] cron.php:539 — Remove unused "$mdb" local variable (php:S1481)
## SonarQube — 0.343 (2026-03-19)
### Nowe issues (nie występowały w 0.340)
#### Code Smells — CRITICAL
- [ ] [CRITICAL] autoload/admin/App.php:39 — Cognitive Complexity 37 (max 15) (php:S3776)
- [ ] [CRITICAL] autoload/admin/App.php:50 — Duplicated literal "Location: /admin/" 8 times (php:S1192)
- [ ] [CRITICAL] autoload/front/Controllers/ShopOrderController.php:86 — Cognitive Complexity 22 (max 15) (php:S3776)
- [ ] [CRITICAL] autoload/front/Controllers/ShopBasketController.php:275 — Duplicated literal "Location: /koszyk" 6 times (php:S1192)
- [ ] [CRITICAL] autoload/front/Controllers/ShopBasketController.php:287 — Duplicated literal "Location: /zamowienie/" 3 times (php:S1192)
- [ ] [CRITICAL] autoload/front/Controllers/ShopBasketController.php:495 — Add curly braces around nested statement(s) (php:S121)
- [ ] [CRITICAL] autoload/Domain/Integrations/IntegrationsRepository.php:33 — Add curly braces around nested statement(s) (php:S121)
- [ ] [CRITICAL] autoload/Domain/Integrations/ApiloRepository.php:449 — Cognitive Complexity 22 (max 15) (php:S3776)
- [ ] [CRITICAL] autoload/Domain/Order/OrderRepository.php:635 — Cognitive Complexity 61 (max 15) (php:S3776)
- [ ] [CRITICAL] cron.php:198 — Cognitive Complexity 109 (max 15) (php:S3776)
- [ ] [CRITICAL] cron.php:651 — Cognitive Complexity 18 (max 15) (php:S3776)
#### Code Smells — MAJOR
- [ ] [MAJOR] cron.php:198 — Function has 305 lines (max 150) (php:S138)
- [ ] [MAJOR] cron.php:572 — Unused function parameter "$payload" (php:S1172)
- [ ] [MAJOR] cron.php:572 — 5 returns (max 3) (php:S1142)
- [ ] [MAJOR] cron.php:605 — Unused function parameter "$payload" (php:S1172)
- [ ] [MAJOR] cron.php:605 — 4 returns (max 3) (php:S1142)
- [ ] [MAJOR] cron.php:651 — Unused function parameter "$payload" (php:S1172)
- [ ] [MAJOR] autoload/Domain/Integrations/ApiloRepository.php:53 — 4 returns (max 3) (php:S1142)
- [ ] [MAJOR] autoload/Domain/Integrations/ApiloRepository.php:93 — 4 returns (max 3) (php:S1142)
- [ ] [MAJOR] autoload/Domain/Integrations/ApiloRepository.php:105 — Merge if statement with enclosing one (php:S1066)
## SonarQube — 0.344 (2026-03-19)
- [ ] [MINOR] autoload/front/Controllers/ShopBasketController.php:484 — Use empty() to check whether the array is empty (php:S1155)
## SonarQube — 0.345 (2026-03-25)
- [ ] [MAJOR] autoload/front/Controllers/ShopBasketController.php:574 — This method has 6 returns, which is more than the 3 allowed (php:S1142)
- [ ] [CRITICAL] autoload/front/Controllers/ShopBasketController.php:576 — Add curly braces around nested statement(s) (php:S121)
- [ ] [CRITICAL] autoload/front/Controllers/ShopBasketController.php:602 — Add curly braces around nested statement(s) (php:S121)

View File

@@ -1,73 +0,0 @@
# Instrukcja tworzenia aktualizacji shopPRO
## Nowy sposób (od v0.301) — automatyczny build script
### Wymagania
- Git z tagami wersji (np. `v0.299`, `v0.300`)
- PowerShell
### Workflow
```
1. Pracuj normalnie: commit, push, commit, push...
2. Gdy wersja gotowa:
→ git tag v0.XXX
→ ./build-update.ps1 -ToTag v0.XXX -ChangelogEntry "NEW - opis"
3. Upload plików z updates/0.XX/ na serwer aktualizacji
```
### Użycie build-update.ps1
```powershell
# Podgląd zmian (bez tworzenia plików)
./build-update.ps1 -ToTag v0.301 -DryRun
# Budowanie paczki (auto-detect poprzedniego tagu)
./build-update.ps1 -ToTag v0.301 -ChangelogEntry "NEW - opis zmiany"
# Z jawnym tagiem źródłowym
./build-update.ps1 -FromTag v0.300 -ToTag v0.301 -ChangelogEntry "NEW - opis"
```
### Co robi skrypt automatycznie
1. `git diff --name-status` między tagami → listy dodanych/zmodyfikowanych/usuniętych plików
2. Filtrowanie przez `.updateignore` (pliki deweloperskie, konfiguracyjne itp.)
3. Kopiowanie plików do temp, tworzenie ZIP
4. SHA256 checksum ZIP-a
5. Generowanie `ver_X.XXX_manifest.json`
6. Generowanie legacy `_sql.txt` i `_files.txt` (okres przejściowy)
7. Aktualizacja `versions.php` i `changelog.php`
8. Cleanup
### Pliki wynikowe
- `updates/0.XX/ver_X.XXX.zip` — paczka z plikami
- `updates/0.XX/ver_X.XXX_manifest.json` — manifest z checksumem, listą zmian, SQL
- `updates/0.XX/ver_X.XXX_sql.txt` — legacy SQL (okres przejściowy)
- `updates/0.XX/ver_X.XXX_files.txt` — legacy lista plików do usunięcia (okres przejściowy)
### Migracje SQL
Pliki SQL umieszczaj w `migrations/{version}.sql` (np. `migrations/0.301.sql`).
Build script automatycznie je wczyta i umieści w manifeście + legacy `_sql.txt`.
### Format manifestu
```json
{
"version": "0.301",
"date": "2026-02-22",
"checksum_zip": "sha256:abc123...",
"files": {
"modified": ["autoload/Domain/Order/OrderRepository.php"],
"added": ["autoload/Domain/Order/NewHelper.php"],
"deleted": ["autoload/shop/OldClass.php"]
},
"directories_deleted": [],
"sql": ["ALTER TABLE pp_x ADD COLUMN y INT DEFAULT 0"],
"changelog": "NEW - opis zmiany"
}
```
### .updateignore
Plik w katalogu głównym projektu, wzorce plików wykluczonych z paczek (jak `.gitignore`).
### INFO
pamiętaj że push czasem zwraca błąd autoryzacji, wtedy spróbuj ponownie

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

@@ -1313,4 +1313,55 @@ class ProductRepositoryTest extends TestCase
$method->setAccessible(true);
$method->invoke($repository, 55, [], [], []);
}
public function testCustomLabelNamesUsesDbSettingsWithFallback(): void
{
$mockDb = $this->createMock(\medoo::class);
$mockDb->expects($this->once())
->method('select')
->with(
'pp_settings',
['param', 'value'],
$this->callback(function ($where) {
return isset($where['param']) && is_array($where['param']) && count($where['param']) === 10;
})
)
->willReturn([
['param' => 'custom_label_0_name', 'value' => 'Sezon'],
['param' => 'google_custom_label_2_name', 'value' => 'Kampania'],
['param' => 'custom_label_4_name', 'value' => ''],
]);
$repository = new ProductRepository($mockDb);
$names = $repository->customLabelNames();
$this->assertSame('Sezon', $names['custom_label_0']);
$this->assertSame('Custom label 1', $names['custom_label_1']);
$this->assertSame('Kampania', $names['custom_label_2']);
$this->assertSame('Custom label 3', $names['custom_label_3']);
$this->assertSame('Custom label 4', $names['custom_label_4']);
}
public function testCustomLabelSuggestionsReturnsEmptyForInvalidLabelType(): void
{
$mockDb = $this->createMock(\medoo::class);
$mockDb->expects($this->never())->method('query');
$repository = new ProductRepository($mockDb);
$result = $repository->customLabelSuggestions('abc', 'custom_label_10');
$this->assertSame([], $result);
}
public function testSaveCustomLabelReturnsFalseForInvalidLabelType(): void
{
$mockDb = $this->createMock(\medoo::class);
$mockDb->expects($this->never())->method('update');
$repository = new ProductRepository($mockDb);
$result = $repository->saveCustomLabel(1, 'abc', 'custom_label_10');
$this->assertFalse($result);
}
}

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

@@ -55,5 +55,38 @@ class ScontainersControllerTest extends TestCase
$this->assertEquals('Domain\Scontainers\ScontainersRepository', $params[0]->getType()->getName());
$this->assertEquals('Domain\Languages\LanguagesRepository', $params[1]->getType()->getName());
}
public function testBuildFormViewModelStoresIdInHiddenFieldsForEdit(): void
{
$reflection = new \ReflectionClass(ScontainersController::class);
$method = $reflection->getMethod('buildFormViewModel');
$method->setAccessible(true);
$container = [
'id' => 9,
'status' => 1,
'show_title' => 1,
'languages' => [],
];
$form = $method->invoke($this->controller, $container, [], null);
$this->assertArrayHasKey('id', $form->hiddenFields);
$this->assertSame(9, (int)$form->hiddenFields['id']);
$this->assertSame('/admin/scontainers/save/id=9', $form->action);
}
public function testBuildFormViewModelKeepsCreateFlowWithZeroId(): void
{
$reflection = new \ReflectionClass(ScontainersController::class);
$method = $reflection->getMethod('buildFormViewModel');
$method->setAccessible(true);
$form = $method->invoke($this->controller, [], [], null);
$this->assertArrayHasKey('id', $form->hiddenFields);
$this->assertSame(0, (int)$form->hiddenFields['id']);
$this->assertSame('/admin/scontainers/save/', $form->action);
}
}

View File

@@ -53,6 +53,7 @@ class ShopProductControllerTest extends TestCase
$this->assertTrue(method_exists($this->controller, 'product_unarchive'));
$this->assertTrue(method_exists($this->controller, 'product_delete'));
$this->assertTrue(method_exists($this->controller, 'change_product_status'));
$this->assertTrue(method_exists($this->controller, 'product_custom_labels_toggle'));
$this->assertTrue(method_exists($this->controller, 'product_change_price_brutto'));
$this->assertTrue(method_exists($this->controller, 'product_change_price_brutto_promo'));
$this->assertTrue(method_exists($this->controller, 'product_change_custom_label'));
@@ -128,6 +129,9 @@ class ShopProductControllerTest extends TestCase
'renderCustomFieldsBox',
'escapeHtml',
'resolveSavePayload',
'customLabelsEnabled',
'isAllowedCustomLabelType',
'renderCustomLabelsEditor',
];
foreach ($expectedPrivate as $method) {
@@ -147,4 +151,22 @@ class ShopProductControllerTest extends TestCase
$reflection = new \ReflectionClass($this->controller);
$this->assertEquals('void', (string)$reflection->getMethod('save')->getReturnType());
}
public function testToggleCustomLabelsMethodReturnsVoid(): void
{
$reflection = new \ReflectionClass($this->controller);
$this->assertEquals('void', (string)$reflection->getMethod('product_custom_labels_toggle')->getReturnType());
}
public function testAllowedCustomLabelTypeValidation(): void
{
$reflection = new \ReflectionClass($this->controller);
$method = $reflection->getMethod('isAllowedCustomLabelType');
$method->setAccessible(true);
$this->assertTrue($method->invoke($this->controller, 'custom_label_0'));
$this->assertTrue($method->invoke($this->controller, 'custom_label_4'));
$this->assertFalse($method->invoke($this->controller, 'custom_label_5'));
$this->assertFalse($method->invoke($this->controller, 'invalid'));
}
}

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']);
}
}

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.346.zip Normal file

Binary file not shown.

View File

@@ -0,0 +1,24 @@
{
"changelog": "Fix usuwania wszystkich dodatkowych pól produktu",
"version": "0.346",
"files": {
"added": [
],
"deleted": [
],
"modified": [
"autoload/Domain/Product/ProductRepository.php",
"autoload/admin/Controllers/ShopProductController.php"
]
},
"checksum_zip": "sha256:7765b50ff52dc9721e0e2be6d60c2b4ac040d5fc6ea918515b5a5b1a7cf1f580",
"sql": [
],
"date": "2026-04-16",
"directories_deleted": [
]
}

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

Binary file not shown.

View File

@@ -0,0 +1,23 @@
{
"changelog": "Naprawa zapisu edycji kontenerow statycznych + testy regresyjne",
"version": "0.347",
"files": {
"added": [
],
"deleted": [
],
"modified": [
"autoload/admin/Controllers/ScontainersController.php"
]
},
"checksum_zip": "sha256:25dd6cc528cc5769974a78453630c6cdadcb6903060b68d01dd5a8068ffac7dd",
"sql": [
],
"date": "2026-04-18",
"directories_deleted": [
]
}

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": [
]
}

View File

@@ -1,3 +1,18 @@
<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>
<b>ver. 0.346 - 16.04.2026</b><br />
Fix usuwania wszystkich dodatkowych pól produktu
<hr>
<b>ver. 0.345 - 25.03.2026</b><br />
Checkout flow fix - summaryView redirect, TTL token 30min, logowanie bledow zamowien
<hr>

View File

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