- 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>
271 lines
9.6 KiB
Markdown
271 lines
9.6 KiB
Markdown
---
|
|
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>
|