--- 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`