diff --git a/.paul/ROADMAP.md b/.paul/ROADMAP.md
index 2cc48dd..e39c93e 100644
--- a/.paul/ROADMAP.md
+++ b/.paul/ROADMAP.md
@@ -43,6 +43,8 @@ Status: Planning
|-------|------|-------|--------|-----------|
| 10 | Edycja personalizacji produktu w koszyku | 1 | Done | 2026-03-19 |
| 11 | DataLayer GA4 analytics fix | 1 | Done | 2026-03-25 |
+| 12 | summaryView redirect fix — double order block | 1 | Done | 2026-03-25 |
+| 13 | Basket logging + TTL token fix | 1 | Done | 2026-03-25 |
## Phase Details
@@ -82,5 +84,17 @@ Status: Planning
**Reference:** `poprawki_datalayer_projectpro.md` — audyt analityki z pomysloweprezenty.pl
+### Phase 12 — summaryView redirect fix
+
+**Problem:** Po złożeniu pierwszego zamówienia, guard w `summaryView()` sprawdzał sesyjny `order-submit-last-order-id` i redirectował na stronę starego zamówienia. Blokował dostęp do `/koszyk-podsumowanie` dla kolejnych zamówień. Poprawka z instancji klienta (change.md) do wdrożenia globalnie.
+
+**Scope:** Usunięcie bloku redirect z `summaryView()` w `ShopBasketController.php`. Double-submit protection w `basketSave()` pozostaje bez zmian.
+
+### Phase 13 — Basket logging + TTL token fix
+
+**Problem:** Brak logowania w basketSave() uniemożliwia diagnozę błędów zamówień. Token zamówienia jednorazowy — nadpisywany przy każdym wejściu na podsumowanie, co powoduje że druga karta, "wstecz" lub odświeżenie unieważnia formularz.
+
+**Scope:** Dodanie metody logOrder() z 4 punktami logowania, zmiana tokena z jednorazowego na TTL 30 min, redirect przy błędzie tokena na /koszyk-podsumowanie zamiast /koszyk, nowy double-submit guard.
+
---
*Last updated: 2026-03-25*
diff --git a/.paul/STATE.md b/.paul/STATE.md
index 71888ba..0feb717 100644
--- a/.paul/STATE.md
+++ b/.paul/STATE.md
@@ -5,25 +5,25 @@
See: .paul/PROJECT.md (updated 2026-03-12)
**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 11 complete — DataLayer GA4 analytics fix
+**Current focus:** Phase 13 complete — basket logging + TTL token
## Current Position
-Milestone: Feature
-Phase: 11 — DataLayer GA4 analytics fix — Complete
-Plan: 11-01 complete (phase done)
-Status: UNIFY complete, phase 11 finished
-Last activity: 2026-03-25 — 11-01 UNIFY complete
+Milestone: Hotfix
+Phase: 13 — basket logging + TTL token — Planning
+Plan: 13-01 created, awaiting approval
+Status: UNIFY complete, phase 13 finished
+Last activity: 2026-03-25 — 13-01 UNIFY complete
Progress:
-- Phase 11: [██████████] 100% (COMPLETE)
+- Phase 13: [██████████] 100% (COMPLETE)
## Loop Position
-Current loop state (phase 11, plan 01):
+Current loop state (phase 13, plan 01):
```
PLAN ──▶ APPLY ──▶ UNIFY
- ✓ ✓ ✓ [Phase 11 complete]
+ ✓ ✓ ✓ [Phase 13 complete]
```
Previous phases:
@@ -36,6 +36,8 @@ Phase 8: PLAN ──▶ APPLY ──▶ UNIFY ✓ ✓ ✓ [COMPLETE — 2026-0
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]
```
## Accumulated Context
@@ -53,6 +55,10 @@ Phase 11: PLAN ──▶ APPLY ──▶ UNIFY ✓ ✓ ✓ [COMPLETE — 2026-
- 2026-03-25: view_cart event w basket.php (nie basket-details.php) — ten sam powód
- 2026-03-25: GA4 item format standard: item_id (string), item_name, price (number), quantity (int), google_business_vertical: "retail"
- 2026-03-25: Brak user_data w purchase — wymaga analizy RODO
+- 2026-03-25: summaryView() redirect guard usunięty — blokował kolejne zamówienia po pierwszym (z change.md instancji klienta)
+- 2026-03-25: Token zamówienia z jednorazowego na TTL 30 min — backward compat z plain string
+- 2026-03-25: logOrder() — logowanie błędów zamówień do logs/logs-order-YYYY-MM-DD.log
+- 2026-03-25: Redirect przy złym tokenie: /koszyk-podsumowanie zamiast /koszyk
### Deferred Issues
None.
@@ -63,9 +69,9 @@ None.
## Session Continuity
Last session: 2026-03-25
-Stopped at: Phase 11 UNIFY complete
+Stopped at: Phase 13 UNIFY complete
Next action: /koniec-pracy or next feature
-Resume file: .paul/phases/11-datalayer-ga4-fix/11-01-SUMMARY.md
+Resume file: .paul/phases/13-basket-logging-ttl-token/13-01-SUMMARY.md
---
*STATE.md — Updated after every significant action*
diff --git a/.paul/phases/12-summaryview-redirect-fix/12-01-PLAN.md b/.paul/phases/12-summaryview-redirect-fix/12-01-PLAN.md
new file mode 100644
index 0000000..b8c9734
--- /dev/null
+++ b/.paul/phases/12-summaryview-redirect-fix/12-01-PLAN.md
@@ -0,0 +1,125 @@
+---
+phase: 12-summaryview-redirect-fix
+plan: 01
+type: execute
+wave: 1
+depends_on: []
+files_modified: [autoload/front/Controllers/ShopBasketController.php]
+autonomous: true
+---
+
+
+## Goal
+Usunąć błędny guard w `summaryView()` który po złożeniu pierwszego zamówienia uniemożliwia złożenie kolejnego — redirectuje na stronę starego zamówienia zamiast pozwolić na wejście na podsumowanie koszyka.
+
+## Purpose
+Klient sklepu po złożeniu jednego zamówienia musi móc złożyć kolejne zamówienie bez problemu. Aktualny guard blokuje dostęp do `/koszyk-podsumowanie` redirectując na `/zamowienie/{hash}` poprzedniego zamówienia.
+
+## Output
+Zmodyfikowany `ShopBasketController.php` bez problematycznego bloku redirect w `summaryView()`.
+
+
+
+## Project Context
+@.paul/PROJECT.md
+@.paul/ROADMAP.md
+@.paul/STATE.md
+
+## Source Files
+@autoload/front/Controllers/ShopBasketController.php
+@change.md — opis błędu z instancji klienta
+
+
+
+## Required Skills (from SPECIAL-FLOWS.md)
+
+No required skills for this hotfix — simple code removal, no new feature development.
+
+
+
+
+## AC-1: Klient może złożyć drugie zamówienie po pierwszym
+```gherkin
+Given klient właśnie złożył zamówienie (sesja zawiera order-submit-last-order-id)
+When klient wraca na /koszyk-podsumowanie z nowym koszykiem
+Then widzi stronę podsumowania zamówienia (nie redirect na stare zamówienie)
+```
+
+## AC-2: Ochrona double-submit pozostaje nienaruszona
+```gherkin
+Given klient jest na stronie podsumowania i klika "złóż zamówienie"
+When formularz zostaje wysłany dwa razy (double-click)
+Then tylko jedno zamówienie zostaje złożone (mechanizm w basketSave() działa)
+```
+
+
+
+
+
+
+ Task 1: Usunięcie błędnego guardu redirect w summaryView()
+ autoload/front/Controllers/ShopBasketController.php
+
+ Usunąć blok kodu w metodzie `summaryView()` (linie 279-290):
+ ```php
+ $existingOrderId = isset( $_SESSION[ self::ORDER_SUBMIT_LAST_ORDER_ID_SESSION_KEY ] )
+ ? (int)$_SESSION[ self::ORDER_SUBMIT_LAST_ORDER_ID_SESSION_KEY ]
+ : 0;
+ if ( $existingOrderId > 0 )
+ {
+ $existingOrderHash = $this->orderRepository->findHashById( $existingOrderId );
+ if ( $existingOrderHash )
+ {
+ header( 'Location: /zamowienie/' . $existingOrderHash );
+ exit;
+ }
+ }
+ ```
+
+ NIE usuwać:
+ - Stałej `ORDER_SUBMIT_LAST_ORDER_ID_SESSION_KEY` (używana w `basketSave()` i `createOrderSubmitToken()`)
+ - Żadnego kodu w `basketSave()` — tam mechanizm double-submit działa poprawnie
+ - Linii 312 (w `basketSave()`) ani 378, 549 — te użycia klucza sesyjnego są poprawne
+
+
+ 1. Grep: brak bloku `$existingOrderId` w metodzie `summaryView()` (okolice linii 279)
+ 2. Grep: klucz `ORDER_SUBMIT_LAST_ORDER_ID_SESSION_KEY` nadal istnieje w `basketSave()` i `createOrderSubmitToken()`
+ 3. Testy: `./test.ps1` — wszystkie testy przechodzą
+
+ AC-1 satisfied: summaryView() nie redirectuje na stare zamówienie; AC-2 satisfied: basketSave() double-submit guard nienaruszony
+
+
+
+
+
+
+## DO NOT CHANGE
+- Metoda `basketSave()` — mechanizm double-submit protection
+- Metoda `createOrderSubmitToken()` — generowanie tokenu
+- Stała `ORDER_SUBMIT_LAST_ORDER_ID_SESSION_KEY` — używana w innych miejscach
+- Linie 312, 378, 549 — poprawne użycia klucza sesyjnego
+
+## SCOPE LIMITS
+- Tylko usunięcie bloku redirect w `summaryView()`, żadne inne zmiany
+- Brak zmian w logice koszyka, płatności ani zamówień
+
+
+
+
+Before declaring plan complete:
+- [ ] Blok redirect (dawne linie 279-290) usunięty z `summaryView()`
+- [ ] Stała `ORDER_SUBMIT_LAST_ORDER_ID_SESSION_KEY` nadal istnieje
+- [ ] Użycia klucza w `basketSave()` i `createOrderSubmitToken()` nienaruszone
+- [ ] `./test.ps1` — wszystkie testy przechodzą
+- [ ] Brak innych zmian w pliku
+
+
+
+- Blok redirect usunięty
+- Wszystkie testy przechodzą
+- Double-submit protection działa bez zmian
+
+
+
diff --git a/.paul/phases/12-summaryview-redirect-fix/12-01-SUMMARY.md b/.paul/phases/12-summaryview-redirect-fix/12-01-SUMMARY.md
new file mode 100644
index 0000000..80d4c55
--- /dev/null
+++ b/.paul/phases/12-summaryview-redirect-fix/12-01-SUMMARY.md
@@ -0,0 +1,88 @@
+---
+phase: 12-summaryview-redirect-fix
+plan: 01
+subsystem: frontend
+tags: [basket, checkout, redirect, session]
+
+requires:
+ - phase: none
+ provides: n/a
+provides:
+ - Fix summaryView() redirect blocking subsequent orders
+affects: []
+
+tech-stack:
+ added: []
+ patterns: []
+
+key-files:
+ created: []
+ modified: [autoload/front/Controllers/ShopBasketController.php]
+
+key-decisions:
+ - "Remove redirect guard from summaryView() — double-submit protection in basketSave() is sufficient"
+
+patterns-established: []
+
+duration: 3min
+completed: 2026-03-25
+---
+
+# Phase 12 Plan 01: summaryView redirect fix Summary
+
+**Usunięto błędny guard w summaryView() który po złożeniu pierwszego zamówienia blokował dostęp do podsumowania koszyka dla kolejnych zamówień.**
+
+## Performance
+
+| Metric | Value |
+|--------|-------|
+| Duration | ~3 min |
+| Completed | 2026-03-25 |
+| Tasks | 1 completed |
+| Files modified | 1 |
+
+## Acceptance Criteria Results
+
+| Criterion | Status | Notes |
+|-----------|--------|-------|
+| AC-1: Klient może złożyć drugie zamówienie po pierwszym | Pass | Blok redirect usunięty z summaryView() |
+| AC-2: Ochrona double-submit pozostaje nienaruszona | Pass | basketSave() guard nienaruszony (linie 299, 365, 536) |
+
+## Accomplishments
+
+- Usunięto blok kodu (12 linii) sprawdzający `order-submit-last-order-id` w `summaryView()` który redirectował na stare zamówienie
+- Double-submit protection w `basketSave()` pozostaje w pełni funkcjonalna
+- 820 testów, 2277 asercji — wszystkie przechodzą
+
+## Files Created/Modified
+
+| File | Change | Purpose |
+|------|--------|---------|
+| `autoload/front/Controllers/ShopBasketController.php` | Modified | Usunięty blok redirect (dawne linie 279-290) z summaryView() |
+
+## Decisions Made
+
+None — followed plan as specified
+
+## Deviations from Plan
+
+None — plan executed exactly as written
+
+## Issues Encountered
+
+None
+
+## Next Phase Readiness
+
+**Ready:**
+- Poprawka gotowa do wdrożenia w update package
+
+**Concerns:**
+- None
+
+**Blockers:**
+- None
+
+---
+*Phase: 12-summaryview-redirect-fix, Plan: 01*
+*Completed: 2026-03-25*
diff --git a/.paul/phases/13-basket-logging-ttl-token/13-01-PLAN.md b/.paul/phases/13-basket-logging-ttl-token/13-01-PLAN.md
new file mode 100644
index 0000000..68e72ee
--- /dev/null
+++ b/.paul/phases/13-basket-logging-ttl-token/13-01-PLAN.md
@@ -0,0 +1,270 @@
+---
+phase: 13-basket-logging-ttl-token
+plan: 01
+type: execute
+wave: 1
+depends_on: ["12-01"]
+files_modified: [autoload/front/Controllers/ShopBasketController.php]
+autonomous: true
+---
+
+
+## Goal
+Dodać logowanie błędów w basketSave() oraz przerobić token zamówienia z jednorazowego na czasowy (TTL 30 min), aby wiele kart/odświeżenie/wstecz nie unieważniały tokenu.
+
+## Purpose
+Klientka nie mogła złożyć zamówienia — brak logów uniemożliwiał diagnozę. Token jednorazowy nadpisywany przy każdym wejściu na podsumowanie powodował, że otworzenie drugiej karty, użycie "wstecz" lub odświeżenie strony unieważniało formularz.
+
+## Output
+Zmodyfikowany `ShopBasketController.php` z logowaniem i TTL-based tokenem.
+
+
+
+## Project Context
+@.paul/PROJECT.md
+@.paul/ROADMAP.md
+@.paul/STATE.md
+
+## Prior Work
+@.paul/phases/12-summaryview-redirect-fix/12-01-SUMMARY.md — usunięty redirect guard z summaryView()
+
+## Source Files
+@autoload/front/Controllers/ShopBasketController.php
+@change.md — opis zmian z instancji klienta (Zmiana 2)
+
+
+
+## Required Skills (from SPECIAL-FLOWS.md)
+
+No required skills for this hotfix.
+
+
+
+
+## AC-1: Logowanie błędów w basketSave()
+```gherkin
+Given basketSave() napotka błąd (double-submit, token invalid, exception, falsy order_id)
+When błąd wystąpi
+Then szczegóły są zapisywane do logs/logs-order-YYYY-MM-DD.log via metoda logOrder()
+```
+
+## AC-2: Token TTL 30 min — wiele kart działa
+```gherkin
+Given klient jest na stronie podsumowania zamówienia
+When otworzy drugą kartę z podsumowaniem lub odświeży stronę
+Then obie karty mają ten sam ważny token i mogą złożyć zamówienie
+```
+
+## AC-3: Token wygasa po 30 minutach
+```gherkin
+Given klient jest na stronie podsumowania z tokenem starszym niż 30 min
+When spróbuje złożyć zamówienie
+Then zostaje przekierowany na /koszyk-podsumowanie (nie /koszyk) i dostaje nowy token
+```
+
+## AC-4: Double-submit guard dla pustego koszyka
+```gherkin
+Given klient złożył zamówienie (koszyk pusty, order ID w sesji)
+When spróbuje ponownie wysłać formularz
+Then zostaje przekierowany na stronę istniejącego zamówienia
+```
+
+
+
+
+
+
+ Task 1: Dodanie stałej TTL i metody logOrder()
+ autoload/front/Controllers/ShopBasketController.php
+
+ 1. Dodać stałą po istniejących stałych (linia 7):
+ ```php
+ private const ORDER_SUBMIT_TOKEN_TTL = 1800;
+ ```
+
+ 2. Dodać prywatną metodę `logOrder()` przed zamknięciem klasy (po `consumeOrderSubmitToken`):
+ ```php
+ private function logOrder($message)
+ {
+ $logFile = __DIR__ . '/../../../logs/logs-order-' . date('Y-m-d') . '.log';
+ $line = '[' . date('Y-m-d H:i:s') . '] ' . $message . "\n";
+ @file_put_contents($logFile, $line, FILE_APPEND);
+ }
+ ```
+ Schemat nazewnictwa: `logs/logs-order-YYYY-MM-DD.log` (jak `logs-db-*`).
+ Użyć `@file_put_contents` z FILE_APPEND — błąd zapisu nie może crashować zamówienia.
+
+ Grep: `ORDER_SUBMIT_TOKEN_TTL` i `function logOrder` istnieją w pliku
+ Infrastruktura dla AC-1 (logOrder) gotowa
+
+
+
+ Task 2: Przerobienie tokena na TTL + logowanie w basketSave()
+ autoload/front/Controllers/ShopBasketController.php
+
+ **A. Zmiana `createOrderSubmitToken()` (linia 532):**
+ Zastąpić obecną implementację:
+ ```php
+ private function createOrderSubmitToken()
+ {
+ $sessionData = isset($_SESSION[self::ORDER_SUBMIT_TOKEN_SESSION_KEY])
+ ? $_SESSION[self::ORDER_SUBMIT_TOKEN_SESSION_KEY]
+ : null;
+
+ if (is_array($sessionData) && isset($sessionData['token'], $sessionData['created_at']))
+ {
+ if ((time() - $sessionData['created_at']) < self::ORDER_SUBMIT_TOKEN_TTL)
+ {
+ return $sessionData['token'];
+ }
+ }
+
+ $token = $this->generateOrderSubmitToken();
+ \Shared\Helpers\Helpers::set_session(self::ORDER_SUBMIT_TOKEN_SESSION_KEY, [
+ 'token' => $token,
+ 'created_at' => time()
+ ]);
+ \Shared\Helpers\Helpers::delete_session(self::ORDER_SUBMIT_LAST_ORDER_ID_SESSION_KEY);
+
+ return $token;
+ }
+ ```
+
+ **B. Zmiana `isValidOrderSubmitToken()` (linia 553):**
+ Zastąpić obecną implementację — backward compat ze starym stringowym tokenem + TTL check:
+ ```php
+ private function isValidOrderSubmitToken($token)
+ {
+ if (!$token)
+ return false;
+
+ $sessionData = isset($_SESSION[self::ORDER_SUBMIT_TOKEN_SESSION_KEY])
+ ? $_SESSION[self::ORDER_SUBMIT_TOKEN_SESSION_KEY]
+ : null;
+
+ if (!$sessionData)
+ return false;
+
+ // Backward compatibility: stary format (plain string)
+ if (is_string($sessionData))
+ {
+ $sessionToken = $sessionData;
+ }
+ elseif (is_array($sessionData) && isset($sessionData['token'], $sessionData['created_at']))
+ {
+ if ((time() - $sessionData['created_at']) >= self::ORDER_SUBMIT_TOKEN_TTL)
+ return false;
+ $sessionToken = $sessionData['token'];
+ }
+ else
+ {
+ return false;
+ }
+
+ if (function_exists('hash_equals'))
+ return hash_equals($sessionToken, $token);
+
+ return $sessionToken === $token;
+ }
+ ```
+
+ **C. Dodanie logowania w `basketSave()` — 4 miejsca:**
+
+ 1. **Double-submit (pusty koszyk + istniejące zamówienie)** — NOWY guard na początku basketSave(), PRZED sprawdzeniem tokena.
+ Dodać po linii 299 (po pobraniu $existingOrderId), PRZED `if (!$this->isValidOrderSubmitToken...)`:
+ ```php
+ $basket = \Shared\Helpers\Helpers::get_session('basket');
+ if (empty($basket) && $existingOrderId > 0)
+ {
+ $existingOrderHash = $this->orderRepository->findHashById($existingOrderId);
+ if ($existingOrderHash)
+ {
+ $this->logOrder('Double-submit detected, redirecting to existing order id=' . $existingOrderId);
+ header('Location: /zamowienie/' . $existingOrderHash);
+ exit;
+ }
+ }
+ ```
+
+ 2. **Token nieprawidłowy** — w istniejącym bloku `if (!$this->isValidOrderSubmitToken...)`, dodać logowanie PRZED komunikatem błędu.
+ Dodać linię:
+ ```php
+ $this->logOrder('Token validation failed. formToken=' . $orderSubmitToken . ' existingOrderId=' . $existingOrderId);
+ ```
+
+ 3. **Zmiana redirect przy złym tokenie** — w tym samym bloku zmienić redirect z `/koszyk` na `/koszyk-podsumowanie`:
+ ```php
+ header('Location: /koszyk-podsumowanie');
+ ```
+
+ 4. **createFromBasket exception** — w catch block, dodać logowanie:
+ ```php
+ $this->logOrder('createFromBasket exception: ' . $e->getMessage());
+ ```
+ (error_log zostaje też)
+
+ 5. **Falsy order_id** — po bloku `if ($order_id)`, dodać else:
+ ```php
+ else
+ {
+ $this->logOrder('createFromBasket returned falsy order_id. client_id=' . ($client['id'] ?? '?') . ' email=' . (\Shared\Helpers\Helpers::get('email', true) ?: '?'));
+ \Shared\Helpers\Helpers::error(\Shared\Helpers\Helpers::lang('zamowienie-zostalo-zlozone-komunikat-blad'));
+ header('Location: /koszyk');
+ exit;
+ }
+ ```
+
+ **D. Usunięcie starego double-submit bloku** z wnętrza `if (!$this->isValidOrderSubmitToken...)`:
+ Usunąć blok linii 303-311 (if existingOrderId > 0 → redirect) — ta logika jest teraz w nowym guardzie PRZED sprawdzeniem tokena.
+
+
+ 1. Grep: `logOrder` wywołane 4 razy w basketSave()
+ 2. Grep: `ORDER_SUBMIT_TOKEN_TTL` użyte w createOrderSubmitToken i isValidOrderSubmitToken
+ 3. Grep: `/koszyk-podsumowanie` jako redirect przy złym tokenie
+ 4. Grep: `is_array.*sessionData` w isValidOrderSubmitToken (backward compat)
+ 5. Testy: `php phpunit.phar` — wszystkie przechodzą
+
+ AC-1 (logowanie), AC-2 (TTL token), AC-3 (wygasanie + redirect), AC-4 (double-submit guard) satisfied
+
+
+
+
+
+
+## DO NOT CHANGE
+- Metoda `generateOrderSubmitToken()` — generowanie samego tokenu bez zmian
+- Metoda `consumeOrderSubmitToken()` — konsumowanie tokenu po złożeniu zamówienia bez zmian
+- Logika czyszczenia koszyka po złożeniu zamówienia (linie 367-374)
+- Logika sesji purchase piksel/adwords/analytics/ekomi (linie 376-379)
+- Redis flushAll po zamówieniu (linie 381-383)
+
+## SCOPE LIMITS
+- Tylko ShopBasketController.php — żadne inne pliki
+- Brak zmian w createFromBasket() ani OrderRepository
+- Brak zmian w szablonach widoków
+
+
+
+
+Before declaring plan complete:
+- [ ] `logOrder()` metoda istnieje i zapisuje do `logs/logs-order-YYYY-MM-DD.log`
+- [ ] Token przechowywany jako array `['token' => ..., 'created_at' => ...]`
+- [ ] `createOrderSubmitToken()` zwraca istniejący ważny token zamiast generować nowy
+- [ ] `isValidOrderSubmitToken()` sprawdza TTL + backward compat ze stringiem
+- [ ] 4 wywołania `logOrder()` w `basketSave()` (double-submit, token invalid, exception, falsy order_id)
+- [ ] Redirect przy złym tokenie → `/koszyk-podsumowanie` (nie `/koszyk`)
+- [ ] Nowy double-submit guard PRZED sprawdzeniem tokena
+- [ ] `php phpunit.phar` — wszystkie testy przechodzą
+- [ ] `consumeOrderSubmitToken()` i `generateOrderSubmitToken()` niezmienione
+
+
+
+- Wszystkie zadania ukończone
+- Wszystkie weryfikacje przechodzą
+- Brak nowych błędów ani ostrzeżeń
+- Token działa z wieloma kartami przeglądarki
+
+
+
diff --git a/.paul/phases/13-basket-logging-ttl-token/13-01-SUMMARY.md b/.paul/phases/13-basket-logging-ttl-token/13-01-SUMMARY.md
new file mode 100644
index 0000000..b8f205e
--- /dev/null
+++ b/.paul/phases/13-basket-logging-ttl-token/13-01-SUMMARY.md
@@ -0,0 +1,102 @@
+---
+phase: 13-basket-logging-ttl-token
+plan: 01
+subsystem: frontend
+tags: [basket, checkout, logging, token, session, TTL]
+
+requires:
+ - phase: 12-summaryview-redirect-fix
+ provides: summaryView() redirect guard removed
+provides:
+ - Order error logging to logs/logs-order-YYYY-MM-DD.log
+ - TTL-based order submit token (30 min, multi-tab safe)
+ - Double-submit guard with logging
+affects: []
+
+tech-stack:
+ added: []
+ patterns: [TTL-based session tokens with backward compatibility]
+
+key-files:
+ created: []
+ modified: [autoload/front/Controllers/ShopBasketController.php]
+
+key-decisions:
+ - "Token format: array ['token' => ..., 'created_at' => ...] with backward compat for plain string"
+ - "Token failure redirect: /koszyk-podsumowanie instead of /koszyk (user keeps context)"
+ - "Double-submit guard moved BEFORE token validation (empty basket + existing order)"
+
+patterns-established:
+ - "Order logging via logOrder() to logs/logs-order-YYYY-MM-DD.log"
+
+duration: 5min
+completed: 2026-03-25
+---
+
+# Phase 13 Plan 01: Basket logging + TTL token fix Summary
+
+**Dodano logowanie błędów zamówień do pliku + przerobiono token z jednorazowego na TTL 30 min, umożliwiając składanie zamówień z wielu kart/po odświeżeniu.**
+
+## Performance
+
+| Metric | Value |
+|--------|-------|
+| Duration | ~5 min |
+| Completed | 2026-03-25 |
+| Tasks | 2 completed |
+| Files modified | 1 |
+
+## Acceptance Criteria Results
+
+| Criterion | Status | Notes |
+|-----------|--------|-------|
+| AC-1: Logowanie błędów w basketSave() | Pass | 4 punkty logowania via logOrder() |
+| AC-2: Token TTL 30 min — wiele kart działa | Pass | createOrderSubmitToken() reuses valid token |
+| AC-3: Token wygasa po 30 min | Pass | isValidOrderSubmitToken() checks TTL, redirect → /koszyk-podsumowanie |
+| AC-4: Double-submit guard dla pustego koszyka | Pass | Nowy guard przed sprawdzeniem tokena |
+
+## Accomplishments
+
+- Dodano metodę `logOrder()` zapisującą do `logs/logs-order-YYYY-MM-DD.log` + 4 punkty logowania w `basketSave()`
+- Token zamówienia przerobiony z jednorazowego na TTL 30 min — wiele kart, odświeżenie, "wstecz" nie unieważniają tokena
+- Backward compatibility ze starymi stringowymi tokenami w sesji
+- Double-submit guard przeniesiony PRZED sprawdzenie tokena (pusty koszyk + istniejące zamówienie → redirect)
+- Redirect przy błędzie tokena zmieniony z `/koszyk` na `/koszyk-podsumowanie`
+
+## Files Created/Modified
+
+| File | Change | Purpose |
+|------|--------|---------|
+| `autoload/front/Controllers/ShopBasketController.php` | Modified | Stała TTL, logOrder(), TTL token, logowanie, double-submit guard |
+
+## Decisions Made
+
+| Decision | Rationale | Impact |
+|----------|-----------|--------|
+| Token jako array z created_at | Umożliwia TTL check bez dodatkowej sesji | Backward compat z plain string |
+| Redirect na /koszyk-podsumowanie | Użytkownik nie traci kontekstu, dostaje nowy token | Lepsza UX |
+| Double-submit guard przed token check | Pusty koszyk = pewny double-submit, nie trzeba sprawdzać tokena | Szybsze wykrycie |
+
+## Deviations from Plan
+
+None — plan executed exactly as written
+
+## Issues Encountered
+
+None
+
+## Next Phase Readiness
+
+**Ready:**
+- Poprawka gotowa do wdrożenia w update package
+- Fazy 12 + 13 razem stanowią kompletny fix checkout flow
+
+**Concerns:**
+- None
+
+**Blockers:**
+- None
+
+---
+*Phase: 13-basket-logging-ttl-token, Plan: 01*
+*Completed: 2026-03-25*
diff --git a/autoload/front/Controllers/ShopBasketController.php b/autoload/front/Controllers/ShopBasketController.php
index 7f2405b..e26e7fa 100644
--- a/autoload/front/Controllers/ShopBasketController.php
+++ b/autoload/front/Controllers/ShopBasketController.php
@@ -5,6 +5,7 @@ class ShopBasketController
{
private const ORDER_SUBMIT_TOKEN_SESSION_KEY = 'order-submit-token';
private const ORDER_SUBMIT_LAST_ORDER_ID_SESSION_KEY = 'order-submit-last-order-id';
+ private const ORDER_SUBMIT_TOKEN_TTL = 1800;
public static $title = [
'mainView' => 'Koszyk'
@@ -276,19 +277,6 @@ class ShopBasketController
exit;
}
- $existingOrderId = isset( $_SESSION[ self::ORDER_SUBMIT_LAST_ORDER_ID_SESSION_KEY ] )
- ? (int)$_SESSION[ self::ORDER_SUBMIT_LAST_ORDER_ID_SESSION_KEY ]
- : 0;
- if ( $existingOrderId > 0 )
- {
- $existingOrderHash = $this->orderRepository->findHashById( $existingOrderId );
- if ( $existingOrderHash )
- {
- header( 'Location: /zamowienie/' . $existingOrderHash );
- exit;
- }
- }
-
$client = \Shared\Helpers\Helpers::get_session( 'client' );
$orderSubmitToken = $this->createOrderSubmitToken();
@@ -311,20 +299,23 @@ class ShopBasketController
$orderSubmitToken = (string)\Shared\Helpers\Helpers::get( 'order_submit_token', true );
$existingOrderId = isset( $_SESSION[ self::ORDER_SUBMIT_LAST_ORDER_ID_SESSION_KEY ] ) ? (int)$_SESSION[ self::ORDER_SUBMIT_LAST_ORDER_ID_SESSION_KEY ] : 0;
+ $basket = \Shared\Helpers\Helpers::get_session( 'basket' );
+ if ( empty( $basket ) && $existingOrderId > 0 )
+ {
+ $existingOrderHash = $this->orderRepository->findHashById( $existingOrderId );
+ if ( $existingOrderHash )
+ {
+ $this->logOrder( 'Double-submit detected, redirecting to existing order id=' . $existingOrderId );
+ header( 'Location: /zamowienie/' . $existingOrderHash );
+ exit;
+ }
+ }
+
if ( !$this->isValidOrderSubmitToken( $orderSubmitToken ) )
{
- if ( $existingOrderId > 0 )
- {
- $existingOrderHash = $this->orderRepository->findHashById( $existingOrderId );
- if ( $existingOrderHash )
- {
- header( 'Location: /zamowienie/' . $existingOrderHash );
- exit;
- }
- }
-
+ $this->logOrder( 'Token validation failed. formToken=' . $orderSubmitToken . ' existingOrderId=' . $existingOrderId );
\Shared\Helpers\Helpers::error( \Shared\Helpers\Helpers::lang( 'zamowienie-zostalo-zlozone-komunikat-blad' ) );
- header( 'Location: /koszyk' );
+ header( 'Location: /koszyk-podsumowanie' );
exit;
}
@@ -367,6 +358,7 @@ class ShopBasketController
}
catch ( \Exception $e )
{
+ $this->logOrder( 'createFromBasket exception: ' . $e->getMessage() );
error_log( '[basketSave] createFromBasket exception: ' . $e->getMessage() );
\Shared\Helpers\Helpers::error( \Shared\Helpers\Helpers::lang( 'zamowienie-zostalo-zlozone-komunikat-blad' ) );
header( 'Location: /koszyk' );
@@ -400,6 +392,7 @@ class ShopBasketController
}
else
{
+ $this->logOrder( 'createFromBasket returned falsy order_id. client_id=' . ( $client['id'] ?? '?' ) . ' email=' . ( \Shared\Helpers\Helpers::get( 'email', true ) ?: '?' ) );
\Shared\Helpers\Helpers::error( \Shared\Helpers\Helpers::lang( 'zamowienie-zostalo-zlozone-komunikat-blad' ) );
header( 'Location: /koszyk' );
exit;
@@ -544,8 +537,23 @@ class ShopBasketController
private function createOrderSubmitToken()
{
+ $sessionData = isset( $_SESSION[ self::ORDER_SUBMIT_TOKEN_SESSION_KEY ] )
+ ? $_SESSION[ self::ORDER_SUBMIT_TOKEN_SESSION_KEY ]
+ : null;
+
+ if ( is_array( $sessionData ) && isset( $sessionData['token'], $sessionData['created_at'] ) )
+ {
+ if ( ( time() - $sessionData['created_at'] ) < self::ORDER_SUBMIT_TOKEN_TTL )
+ {
+ return $sessionData['token'];
+ }
+ }
+
$token = $this->generateOrderSubmitToken();
- \Shared\Helpers\Helpers::set_session( self::ORDER_SUBMIT_TOKEN_SESSION_KEY, $token );
+ \Shared\Helpers\Helpers::set_session( self::ORDER_SUBMIT_TOKEN_SESSION_KEY, [
+ 'token' => $token,
+ 'created_at' => time()
+ ] );
\Shared\Helpers\Helpers::delete_session( self::ORDER_SUBMIT_LAST_ORDER_ID_SESSION_KEY );
return $token;
@@ -568,10 +576,29 @@ class ShopBasketController
if ( !$token )
return false;
- $sessionToken = isset( $_SESSION[ self::ORDER_SUBMIT_TOKEN_SESSION_KEY ] ) ? (string)$_SESSION[ self::ORDER_SUBMIT_TOKEN_SESSION_KEY ] : '';
- if ( !$sessionToken )
+ $sessionData = isset( $_SESSION[ self::ORDER_SUBMIT_TOKEN_SESSION_KEY ] )
+ ? $_SESSION[ self::ORDER_SUBMIT_TOKEN_SESSION_KEY ]
+ : null;
+
+ if ( !$sessionData )
return false;
+ // Backward compatibility: stary format (plain string)
+ if ( is_string( $sessionData ) )
+ {
+ $sessionToken = $sessionData;
+ }
+ elseif ( is_array( $sessionData ) && isset( $sessionData['token'], $sessionData['created_at'] ) )
+ {
+ if ( ( time() - $sessionData['created_at'] ) >= self::ORDER_SUBMIT_TOKEN_TTL )
+ return false;
+ $sessionToken = $sessionData['token'];
+ }
+ else
+ {
+ return false;
+ }
+
if ( function_exists( 'hash_equals' ) )
return hash_equals( $sessionToken, $token );
@@ -582,4 +609,11 @@ class ShopBasketController
{
\Shared\Helpers\Helpers::delete_session( self::ORDER_SUBMIT_TOKEN_SESSION_KEY );
}
+
+ private function logOrder( $message )
+ {
+ $logFile = __DIR__ . '/../../../logs/logs-order-' . date( 'Y-m-d' ) . '.log';
+ $line = '[' . date( 'Y-m-d H:i:s' ) . '] ' . $message . "\n";
+ @file_put_contents( $logFile, $line, FILE_APPEND );
+ }
}
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index eea6650..0e0b965 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -4,12 +4,16 @@ Logi zmian z migracji na Domain-Driven Architecture. Najnowsze na gorze.
---
-## ver. 0.345 (2026-03-25) - DataLayer GA4 analytics fix
+## ver. 0.345 (2026-03-25) - DataLayer GA4 fix + checkout token fix
- **FIX**: `templates/shop-order/order-details.php` — event purchase: id→item_id (string), name→item_name, price via normalize_decimal (fix price:0), usunięty hardcoded value: 25.42, dodany google_business_vertical
- **FIX**: `templates/shop-basket/summary-view.php` — event begin_checkout: id→item_id, name→item_name, dodany google_business_vertical
- **FIX**: `templates/shop-product/product.php` — event view_item: dodany currency PLN, value, price jako number (nie string), google_business_vertical; event add_to_cart: dodany google_business_vertical, parseInt(quantity)
- **NEW**: `templates/shop-basket/basket.php` — nowy event view_cart na stronie koszyka z pełnym zestawem danych GA4 (item_id, item_name, price, quantity, currency, google_business_vertical)
+- **FIX**: `autoload/front/Controllers/ShopBasketController.php` — usunięty błędny guard w summaryView() blokujący kolejne zamówienia po pierwszym (redirect na stare zamówienie zamiast podsumowanie)
+- **FIX**: `autoload/front/Controllers/ShopBasketController.php` — token zamówienia z jednorazowego na TTL 30 min (wiele kart, odświeżenie, "wstecz" nie unieważniają formularza)
+- **NEW**: `autoload/front/Controllers/ShopBasketController.php` — logowanie błędów zamówień do `logs/logs-order-YYYY-MM-DD.log` (double-submit, token invalid, exception, falsy order_id)
+- **FIX**: `autoload/front/Controllers/ShopBasketController.php` — redirect przy złym tokenie na `/koszyk-podsumowanie` zamiast `/koszyk` (użytkownik nie traci kontekstu)
---
diff --git a/docs/TODO.md b/docs/TODO.md
index b01c2eb..df46f81 100644
--- a/docs/TODO.md
+++ b/docs/TODO.md
@@ -109,4 +109,8 @@ Dodać możliwość ustawienia limitu znaków w wiadomościach do produktu
- [ ] [MINOR] autoload/front/Controllers/ShopBasketController.php:484 — Use empty() to check whether the array is empty (php:S1155)
-## SonarQube — 0.345 (2026-03-25) — brak nowych issues
\ No newline at end of file
+## 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)
\ No newline at end of file