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 + + + +After completion, create `.paul/phases/12-summaryview-redirect-fix/12-01-SUMMARY.md` + 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 + + + +After completion, create `.paul/phases/13-basket-logging-ttl-token/13-01-SUMMARY.md` + 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