fix: Checkout flow — summaryView redirect fix + TTL token + order logging

- Usunięty błędny guard w summaryView() blokujący kolejne zamówienia
- Token zamówienia z jednorazowego na TTL 30 min (multi-tab safe)
- Logowanie błędów zamówień do logs/logs-order-YYYY-MM-DD.log
- Redirect przy złym tokenie na /koszyk-podsumowanie zamiast /koszyk
- Double-submit guard przeniesiony przed sprawdzenie tokena

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jacek
2026-03-25 21:32:46 +01:00
parent fc624ba0ef
commit 1c747af1b6
9 changed files with 687 additions and 40 deletions

View File

@@ -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*

View File

@@ -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: 11DataLayer 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: 13basket 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*

View File

@@ -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
---
<objective>
## 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()`.
</objective>
<context>
## 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
</context>
<skills>
## Required Skills (from SPECIAL-FLOWS.md)
No required skills for this hotfix — simple code removal, no new feature development.
</skills>
<acceptance_criteria>
## 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)
```
</acceptance_criteria>
<tasks>
<task type="auto">
<name>Task 1: Usunięcie błędnego guardu redirect w summaryView()</name>
<files>autoload/front/Controllers/ShopBasketController.php</files>
<action>
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
</action>
<verify>
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ą
</verify>
<done>AC-1 satisfied: summaryView() nie redirectuje na stare zamówienie; AC-2 satisfied: basketSave() double-submit guard nienaruszony</done>
</task>
</tasks>
<boundaries>
## 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ń
</boundaries>
<verification>
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
</verification>
<success_criteria>
- Blok redirect usunięty
- Wszystkie testy przechodzą
- Double-submit protection działa bez zmian
</success_criteria>
<output>
After completion, create `.paul/phases/12-summaryview-redirect-fix/12-01-SUMMARY.md`
</output>

View File

@@ -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*

View File

@@ -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
---
<objective>
## 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.
</objective>
<context>
## 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)
</context>
<skills>
## Required Skills (from SPECIAL-FLOWS.md)
No required skills for this hotfix.
</skills>
<acceptance_criteria>
## 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
```
</acceptance_criteria>
<tasks>
<task type="auto">
<name>Task 1: Dodanie stałej TTL i metody logOrder()</name>
<files>autoload/front/Controllers/ShopBasketController.php</files>
<action>
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.
</action>
<verify>Grep: `ORDER_SUBMIT_TOKEN_TTL` i `function logOrder` istnieją w pliku</verify>
<done>Infrastruktura dla AC-1 (logOrder) gotowa</done>
</task>
<task type="auto">
<name>Task 2: Przerobienie tokena na TTL + logowanie w basketSave()</name>
<files>autoload/front/Controllers/ShopBasketController.php</files>
<action>
**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.
</action>
<verify>
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ą
</verify>
<done>AC-1 (logowanie), AC-2 (TTL token), AC-3 (wygasanie + redirect), AC-4 (double-submit guard) satisfied</done>
</task>
</tasks>
<boundaries>
## 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
</boundaries>
<verification>
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
</verification>
<success_criteria>
- Wszystkie zadania ukończone
- Wszystkie weryfikacje przechodzą
- Brak nowych błędów ani ostrzeżeń
- Token działa z wieloma kartami przeglądarki
</success_criteria>
<output>
After completion, create `.paul/phases/13-basket-logging-ttl-token/13-01-SUMMARY.md`
</output>

View File

@@ -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*

View File

@@ -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 );
}
}

View File

@@ -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)
---

View File

@@ -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
## 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)