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:
@@ -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*
|
||||
|
||||
@@ -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*
|
||||
|
||||
125
.paul/phases/12-summaryview-redirect-fix/12-01-PLAN.md
Normal file
125
.paul/phases/12-summaryview-redirect-fix/12-01-PLAN.md
Normal 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>
|
||||
88
.paul/phases/12-summaryview-redirect-fix/12-01-SUMMARY.md
Normal file
88
.paul/phases/12-summaryview-redirect-fix/12-01-SUMMARY.md
Normal 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*
|
||||
270
.paul/phases/13-basket-logging-ttl-token/13-01-PLAN.md
Normal file
270
.paul/phases/13-basket-logging-ttl-token/13-01-PLAN.md
Normal 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>
|
||||
102
.paul/phases/13-basket-logging-ttl-token/13-01-SUMMARY.md
Normal file
102
.paul/phases/13-basket-logging-ttl-token/13-01-SUMMARY.md
Normal 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*
|
||||
@@ -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 );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user