314 lines
13 KiB
Markdown
314 lines
13 KiB
Markdown
---
|
|
phase: 05-order-bugs-fix
|
|
plan: 05-01
|
|
type: fix
|
|
wave: 1
|
|
depends_on: []
|
|
files_modified:
|
|
- autoload/front/Controllers/ShopBasketController.php
|
|
- autoload/Domain/Order/OrderRepository.php
|
|
- autoload/Domain/PaymentMethod/PaymentMethodRepository.php
|
|
- autoload/admin/Controllers/ShopPaymentMethodController.php
|
|
- migrations/0.338.sql
|
|
- docs/DATABASE_STRUCTURE.md
|
|
autonomous: true
|
|
---
|
|
|
|
<objective>
|
|
## Goal
|
|
Fix 2 production bugs reported by customer: (1) duplicate orders on retry after error, (2) wrong initial status for cash-on-delivery orders.
|
|
|
|
## Purpose
|
|
Production issues affecting real customers. Bug 1 causes double-billed orders. Bug 2 causes wrong order flow for COD payments.
|
|
|
|
## Output
|
|
- `summaryView()` guards against re-submission after successful order
|
|
- `basketSave()` handles exceptions from `createFromBasket()` safely
|
|
- `is_cod` column added to `pp_shop_payment_methods`
|
|
- COD status promotion uses `is_cod` flag instead of hardcoded `payment_id == 3`
|
|
- Admin form for payment methods shows `is_cod` switch
|
|
</objective>
|
|
|
|
<context>
|
|
@.paul/STATE.md
|
|
@.paul/ROADMAP.md
|
|
@autoload/front/Controllers/ShopBasketController.php
|
|
@autoload/Domain/Order/OrderRepository.php
|
|
@autoload/Domain/PaymentMethod/PaymentMethodRepository.php
|
|
@autoload/admin/Controllers/ShopPaymentMethodController.php
|
|
</context>
|
|
|
|
<acceptance_criteria>
|
|
## AC-1: No duplicate order on retry
|
|
Given a customer submits an order and it is created successfully (order_id saved in session),
|
|
When the customer navigates back to `/podsumowanie` and tries to submit again,
|
|
Then they are redirected to the existing order page — no new order is created.
|
|
|
|
## AC-2: Exception in createFromBasket does not duplicate order
|
|
Given `createFromBasket()` throws an uncaught exception after the INSERT succeeds (partial failure),
|
|
When the customer retries submission with the same basket,
|
|
Then the exception is caught, an error message is shown, basket session is preserved, and no second order is inserted via normal retry flow (AC-1 guards subsequent summary visit).
|
|
|
|
## AC-3: COD flag is configurable in admin
|
|
Given an admin opens any payment method in `/admin/shop_payment_method/edit/`,
|
|
When they toggle "Płatność przy odbiorze" switch and save,
|
|
Then the `is_cod` flag is persisted in `pp_shop_payment_methods.is_cod`.
|
|
|
|
## AC-4: COD order gets correct initial status
|
|
Given a customer places an order with a payment method where `is_cod = 1`,
|
|
When the order is created,
|
|
Then `pp_shop_order_statuses` contains status_id = 4 ("Przyjęte do realizacji") and the old status 0 entry is updated.
|
|
</acceptance_criteria>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto">
|
|
<name>Fix BUG-1: Guard summaryView() against re-submission after successful order</name>
|
|
<files>autoload/front/Controllers/ShopBasketController.php</files>
|
|
<action>
|
|
In `summaryView()`, BEFORE calling `createOrderSubmitToken()`, check if `ORDER_SUBMIT_LAST_ORDER_ID_SESSION_KEY` is set in session. If it is, look up that order's hash via `$this->orderRepository->findHashById($existingOrderId)`. If the hash exists, redirect to `/zamowienie/{hash}` and exit.
|
|
|
|
This means the customer who navigates back to the summary page after a successful order is immediately redirected to their order instead of seeing the form again (which would regenerate a token and allow double-submission).
|
|
|
|
Do NOT call `createOrderSubmitToken()` in this guard path — just redirect.
|
|
|
|
Current problematic code at the top of `summaryView()`:
|
|
```php
|
|
$orderSubmitToken = $this->createOrderSubmitToken();
|
|
```
|
|
Must become:
|
|
```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;
|
|
}
|
|
}
|
|
$orderSubmitToken = $this->createOrderSubmitToken();
|
|
```
|
|
</action>
|
|
<verify>
|
|
1. Create a test order successfully
|
|
2. Navigate back to /podsumowanie in the same browser session
|
|
3. Confirm browser redirects to /zamowienie/{hash} without showing the summary form
|
|
</verify>
|
|
<done>AC-1 satisfied: navigating back to summary after successful order redirects, no form shown</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Fix BUG-1: Wrap createFromBasket in try-catch in basketSave()</name>
|
|
<files>autoload/front/Controllers/ShopBasketController.php</files>
|
|
<action>
|
|
In `basketSave()`, wrap the call to `$this->orderRepository->createFromBasket(...)` in a try-catch block. On exception: log with `error_log()`, show user error message via `Helpers::error()`, and redirect to `/koszyk`. Do NOT clear the basket session in the catch block.
|
|
|
|
Replace the current `if ($order_id = $this->orderRepository->createFromBasket(...))` pattern with:
|
|
|
|
```php
|
|
$order_id = null;
|
|
try {
|
|
$order_id = $this->orderRepository->createFromBasket(
|
|
// ... all current args unchanged ...
|
|
);
|
|
} catch (\Exception $e) {
|
|
error_log('[basketSave] createFromBasket exception: ' . $e->getMessage());
|
|
\Shared\Helpers\Helpers::error(\Shared\Helpers\Helpers::lang('zamowienie-zostalo-zlozone-komunikat-blad'));
|
|
header('Location: /koszyk');
|
|
exit;
|
|
}
|
|
|
|
if ($order_id) {
|
|
// ... existing success block unchanged ...
|
|
} else {
|
|
// ... existing error block unchanged ...
|
|
}
|
|
```
|
|
|
|
Use `\Exception` catch (not `\Throwable`) — the project targets PHP 7.4 which supports both, but `\Exception` covers the common cases (DB exceptions, mail exceptions). If there are any `\Error` throws in the chain they won't be caught — acceptable tradeoff for PHP 7.4 compatibility.
|
|
</action>
|
|
<verify>
|
|
Confirm no PHP syntax errors: `php -l autoload/front/Controllers/ShopBasketController.php`
|
|
</verify>
|
|
<done>AC-2 satisfied: exceptions from createFromBasket are caught and handled gracefully</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Fix BUG-2: Add is_cod column migration</name>
|
|
<files>migrations/0.338.sql, docs/DATABASE_STRUCTURE.md</files>
|
|
<action>
|
|
Create the migration file at `migrations/0.338.sql` (kolejna wersja po 0.337):
|
|
|
|
```sql
|
|
ALTER TABLE `pp_shop_payment_methods`
|
|
ADD COLUMN `is_cod` TINYINT(1) NOT NULL DEFAULT 0
|
|
COMMENT 'Platnosc przy odbiorze (cash on delivery): 1 = tak, 0 = nie';
|
|
```
|
|
|
|
Also update `docs/DATABASE_STRUCTURE.md` — in the `pp_shop_payment_methods` table section, add the new column:
|
|
| is_cod | Płatność przy odbiorze: 1 = tak, 0 = nie (TINYINT DEFAULT 0) |
|
|
|
|
The migration must be run on production DB manually (document this in the plan summary).
|
|
</action>
|
|
<verify>
|
|
File `migrations/0.338.sql` exists and contains valid ALTER TABLE statement.
|
|
`docs/DATABASE_STRUCTURE.md` mentions `is_cod` in `pp_shop_payment_methods` section.
|
|
</verify>
|
|
<done>AC-3 precondition: column definition prepared for migration</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Fix BUG-2: Add is_cod to PaymentMethodRepository normalization and queries</name>
|
|
<files>autoload/Domain/PaymentMethod/PaymentMethodRepository.php</files>
|
|
<action>
|
|
1. In `normalizePaymentMethod(array $row)`: add `$row['is_cod'] = (int)($row['is_cod'] ?? 0);`
|
|
|
|
2. In `findActiveById()`: the method already uses `SELECT *` via Medoo `get('pp_shop_payment_methods', '*', ...)` so `is_cod` will be included automatically once the column exists.
|
|
|
|
3. In `forTransport()`: the method uses explicit column list in raw SQL. Add `spm.is_cod` to the SELECT list (around line ~241, alongside `spm.apilo_payment_type_id`).
|
|
|
|
4. In `paymentMethodsByTransport()` (if exists as a separate raw SQL method): similarly add `spm.is_cod` to the SELECT. Search for any other raw SQL selects in this file that list columns explicitly and add `is_cod` to them.
|
|
|
|
5. In the `allActive()` / `paymentMethodsCached()` path: if `allActive()` uses raw SQL with explicit columns, add `spm.is_cod` there too. If it uses `SELECT *`, nothing needed.
|
|
|
|
Cache keys that include payment method data (`payment_method{id}`, `payment_methods`) will return stale data until Redis is flushed. The post-deploy step is to flush Redis cache.
|
|
</action>
|
|
<verify>
|
|
`php -l autoload/Domain/PaymentMethod/PaymentMethodRepository.php` — no syntax errors.
|
|
All explicit SQL SELECTs in this file now include `is_cod`.
|
|
</verify>
|
|
<done>AC-3 + AC-4 precondition: repository returns is_cod field</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Fix BUG-2: Add is_cod switch to admin payment method form</name>
|
|
<files>autoload/admin/Controllers/ShopPaymentMethodController.php</files>
|
|
<action>
|
|
In `buildFormViewModel()`:
|
|
|
|
1. Add `'is_cod' => (int)($paymentMethod['is_cod'] ?? 0)` to the `$data` array.
|
|
|
|
2. Add a switch field after the `status` field:
|
|
```php
|
|
FormField::switch('is_cod', [
|
|
'label' => 'Platnosc przy odbiorze',
|
|
'tab' => 'settings',
|
|
]),
|
|
```
|
|
|
|
In the `save()` / `update()` method of this controller: ensure `is_cod` is read from POST and included in the DB update data. Find where the other fields (description, status, apilo_payment_type_id, etc.) are read from request and add:
|
|
```php
|
|
'is_cod' => (int)(\Shared\Helpers\Helpers::get('is_cod') ? 1 : 0),
|
|
```
|
|
|
|
Check if there is a `FormRequestHandler` or similar save mechanism — if so, `is_cod` may need to be added to the allowed fields list. Read the save method to confirm.
|
|
</action>
|
|
<verify>
|
|
`php -l autoload/admin/Controllers/ShopPaymentMethodController.php` — no syntax errors.
|
|
Check that `is_cod` appears in both the form field list and the save data array.
|
|
</verify>
|
|
<done>AC-3 satisfied: admin can set is_cod flag on any payment method</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Fix BUG-2: Use is_cod flag instead of hardcoded payment_id == 3 in OrderRepository</name>
|
|
<files>autoload/Domain/Order/OrderRepository.php</files>
|
|
<action>
|
|
In `createFromBasket()`, at lines 817-820, replace the hardcoded check:
|
|
|
|
```php
|
|
// BEFORE:
|
|
if ($payment_id == 3) {
|
|
$this->updateOrderStatus($order_id, 4);
|
|
$this->insertStatusHistory($order_id, 4, 1);
|
|
}
|
|
```
|
|
|
|
With:
|
|
```php
|
|
// AFTER:
|
|
if (!empty($payment_method['is_cod'])) {
|
|
$this->updateOrderStatus($order_id, 4);
|
|
$this->insertStatusHistory($order_id, 4, 1);
|
|
}
|
|
```
|
|
|
|
`$payment_method` is already fetched at line 669:
|
|
```php
|
|
$payment_method = ( new \Domain\PaymentMethod\PaymentMethodRepository( $this->db ) )->findActiveById( (int)$payment_id );
|
|
```
|
|
So `$payment_method['is_cod']` is available without any additional DB query.
|
|
</action>
|
|
<verify>
|
|
`php -l autoload/Domain/Order/OrderRepository.php` — no syntax errors.
|
|
Confirm the old `$payment_id == 3` no longer exists in createFromBasket().
|
|
</verify>
|
|
<done>AC-4 satisfied: COD status promotion is driven by is_cod flag, not hardcoded ID</done>
|
|
</task>
|
|
|
|
<task type="checkpoint:human-action" gate="blocking">
|
|
<action>Run the database migration on production server</action>
|
|
<instructions>
|
|
Claude has prepared the migration file at `migrations/0.338.sql`.
|
|
The SQL is: ALTER TABLE pp_shop_payment_methods ADD COLUMN is_cod TINYINT(1) NOT NULL DEFAULT 0
|
|
|
|
You need to run this on the production database manually (via phpMyAdmin, SSH, or your DB client).
|
|
|
|
After running, go to /admin/shop_payment_method/list/ → edit the "Płatność przy odbiorze" payment method → enable the "Płatnosc przy odbiorze" switch → Save.
|
|
|
|
Also flush Redis cache (or wait for TTL expiry — payment methods cache is 24h).
|
|
</instructions>
|
|
<verification>
|
|
Claude will verify the code changes are in place. The DB migration must be confirmed by you.
|
|
</verification>
|
|
<resume-signal>Type "done" when migration and admin flag set</resume-signal>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<boundaries>
|
|
## DO NOT CHANGE
|
|
- The CSRF token mechanism (separate from order submit token)
|
|
- The basket session structure
|
|
- The order submission token logic (ORDER_SUBMIT_TOKEN_SESSION_KEY) — only guard summaryView, don't change how tokens are generated/consumed
|
|
- Email sending logic in createFromBasket
|
|
- Any other payment method fields or behavior
|
|
|
|
## SCOPE LIMITS
|
|
- Do NOT add database-level unique constraints or idempotency key columns to pp_shop_orders (over-engineering for now)
|
|
- Do NOT change the order status values or their meaning
|
|
- Do NOT modify test files unless directly testing the changed methods
|
|
- Do NOT change the frontend templates
|
|
</boundaries>
|
|
|
|
<verification>
|
|
Before declaring plan complete:
|
|
- [ ] `php -l` passes on all modified PHP files
|
|
- [ ] summaryView() guard redirects to existing order when ORDER_SUBMIT_LAST_ORDER_ID_SESSION_KEY is set
|
|
- [ ] createFromBasket call in basketSave() is wrapped in try-catch
|
|
- [ ] `is_cod` column exists in migration SQL
|
|
- [ ] normalizePaymentMethod() includes is_cod normalization
|
|
- [ ] admin form shows is_cod switch
|
|
- [ ] admin save includes is_cod in update data
|
|
- [ ] OrderRepository uses $payment_method['is_cod'] not $payment_id == 3
|
|
- [ ] DATABASE_STRUCTURE.md updated
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
- All PHP files lint-clean
|
|
- No more duplicate orders when customer navigates back to summary after successful order
|
|
- COD payment method (when is_cod=1) automatically promotes order to status 4
|
|
- Admin can configure which payment method is COD
|
|
</success_criteria>
|
|
|
|
<output>
|
|
After completion, create `.paul/phases/05-order-bugs-fix/05-01-FIX-SUMMARY.md` with:
|
|
- List of files changed
|
|
- Note that DB migration in `migrations/0.338.sql` must be run on production
|
|
- Note that admin must set is_cod=1 on the COD payment method after migration
|
|
|
|
Then run: `/koniec-pracy`
|
|
</output>
|