ver. 0.304: Configurable payment method order amount limits

Replace hardcoded PayPo condition (id=6, 40-1000 PLN) with generic
min/max order amount columns on pp_shop_payment_methods. Admin form
fields added, frontend basket checkout filters dynamically. Cache
invalidation on save. 4 new tests (734 total, 2080 assertions).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-22 15:26:51 +01:00
parent 3a3c2adb47
commit 9de4afec9a
10 changed files with 171 additions and 7 deletions

View File

@@ -11,7 +11,10 @@ Gdy użytkownik napisze `KONIEC PRACY`, wykonaj kolejno:
- `docs/FORM_EDIT_SYSTEM.md` - `docs/FORM_EDIT_SYSTEM.md`
- `docs/CHANGELOG.md` - `docs/CHANGELOG.md`
- `docs/TESTING.md` - `docs/TESTING.md`
3. Przygotowanie aktualizacji zgodnie z plikiem docs/UPDATE_INSTRUCTIONS.md (ZIP, plik z usuwanymi plikami, plik SQL jeśli wymagany). 3. Migracje SQL (jeśli były zmiany w bazie danych):
- Plik: `migrations/{version}.sql` (np. `migrations/0.304.sql`)
- **NIE** w `updates/` — build script sam wczyta z `migrations/`
- Sprawdź czy plik istnieje i jest poprawnie nazwany przed commitem
4. Commit. 4. Commit.
5. Push. 5. Push.

View File

@@ -36,7 +36,7 @@ composer test
PHPUnit 9.6 via `phpunit.phar`. Bootstrap: `tests/bootstrap.php`. Config: `phpunit.xml`. PHPUnit 9.6 via `phpunit.phar`. Bootstrap: `tests/bootstrap.php`. Config: `phpunit.xml`.
Current suite: **730 tests, 2066 assertions**. Current suite: **734 tests, 2080 assertions**.
### Creating Updates ### Creating Updates
See `docs/UPDATE_INSTRUCTIONS.md` for the full procedure. Updates are ZIP packages in `updates/0.XX/`. Never include `*.md` files, `updates/changelog.php`, or root `.htaccess` in update ZIPs. See `docs/UPDATE_INSTRUCTIONS.md` for the full procedure. Updates are ZIP packages in `updates/0.XX/`. Never include `*.md` files, `updates/changelog.php`, or root `.htaccess` in update ZIPs.
@@ -208,7 +208,7 @@ $controller = new \admin\Controllers\ExampleController($repo);
When user says **"KONIEC PRACY"**, execute in order: When user says **"KONIEC PRACY"**, execute in order:
1. Run tests 1. Run tests
2. Update documentation if needed: `docs/DATABASE_STRUCTURE.md`, `docs/PROJECT_STRUCTURE.md`, `docs/FORM_EDIT_SYSTEM.md`, `docs/CHANGELOG.md`, `docs/TESTING.md` 2. Update documentation if needed: `docs/DATABASE_STRUCTURE.md`, `docs/PROJECT_STRUCTURE.md`, `docs/FORM_EDIT_SYSTEM.md`, `docs/CHANGELOG.md`, `docs/TESTING.md`
3. Prepare update package per `docs/UPDATE_INSTRUCTIONS.md` 3. SQL migrations (if DB changes): place in `migrations/{version}.sql` (e.g. `migrations/0.304.sql`). **NOT** in `updates/` — build script reads from `migrations/` automatically
4. Commit 4. Commit
5. Push 5. Push

View File

@@ -120,10 +120,16 @@ class PaymentMethodRepository
'description' => trim((string)($data['description'] ?? '')), 'description' => trim((string)($data['description'] ?? '')),
'status' => $this->toSwitchValue($data['status'] ?? 0), 'status' => $this->toSwitchValue($data['status'] ?? 0),
'apilo_payment_type_id' => $this->normalizeApiloPaymentTypeId($data['apilo_payment_type_id'] ?? null), 'apilo_payment_type_id' => $this->normalizeApiloPaymentTypeId($data['apilo_payment_type_id'] ?? null),
'min_order_amount' => $this->normalizeDecimalOrNull($data['min_order_amount'] ?? null),
'max_order_amount' => $this->normalizeDecimalOrNull($data['max_order_amount'] ?? null),
]; ];
$this->db->update('pp_shop_payment_methods', $row, ['id' => $paymentMethodId]); $this->db->update('pp_shop_payment_methods', $row, ['id' => $paymentMethodId]);
$cacheHandler = new \Shared\Cache\CacheHandler();
$cacheHandler->deletePattern('payment_method*');
$cacheHandler->deletePattern('payment_methods*');
return $paymentMethodId; return $paymentMethodId;
} }
@@ -232,7 +238,9 @@ class PaymentMethodRepository
spm.name, spm.name,
spm.description, spm.description,
spm.status, spm.status,
spm.apilo_payment_type_id spm.apilo_payment_type_id,
spm.min_order_amount,
spm.max_order_amount
FROM pp_shop_payment_methods AS spm FROM pp_shop_payment_methods AS spm
INNER JOIN pp_shop_transport_payment_methods AS stpm INNER JOIN pp_shop_transport_payment_methods AS stpm
ON stpm.id_payment_method = spm.id ON stpm.id_payment_method = spm.id
@@ -325,6 +333,8 @@ class PaymentMethodRepository
$row['description'] = (string)($row['description'] ?? ''); $row['description'] = (string)($row['description'] ?? '');
$row['status'] = $this->toSwitchValue($row['status'] ?? 0); $row['status'] = $this->toSwitchValue($row['status'] ?? 0);
$row['apilo_payment_type_id'] = $this->normalizeApiloPaymentTypeId($row['apilo_payment_type_id'] ?? null); $row['apilo_payment_type_id'] = $this->normalizeApiloPaymentTypeId($row['apilo_payment_type_id'] ?? null);
$row['min_order_amount'] = $this->normalizeDecimalOrNull($row['min_order_amount'] ?? null);
$row['max_order_amount'] = $this->normalizeDecimalOrNull($row['max_order_amount'] ?? null);
return $row; return $row;
} }
@@ -350,6 +360,23 @@ class PaymentMethodRepository
return $text; return $text;
} }
/**
* @return float|null
*/
private function normalizeDecimalOrNull($value)
{
if ($value === null || $value === false) {
return null;
}
$text = trim((string)$value);
if ($text === '') {
return null;
}
return (float)$text;
}
private function toSwitchValue($value): int private function toSwitchValue($value): int
{ {
if (is_bool($value)) { if (is_bool($value)) {

View File

@@ -182,6 +182,8 @@ class ShopPaymentMethodController
'description' => (string)($paymentMethod['description'] ?? ''), 'description' => (string)($paymentMethod['description'] ?? ''),
'status' => (int)($paymentMethod['status'] ?? 0), 'status' => (int)($paymentMethod['status'] ?? 0),
'apilo_payment_type_id' => $paymentMethod['apilo_payment_type_id'] ?? '', 'apilo_payment_type_id' => $paymentMethod['apilo_payment_type_id'] ?? '',
'min_order_amount' => $paymentMethod['min_order_amount'] ?? '',
'max_order_amount' => $paymentMethod['max_order_amount'] ?? '',
]; ];
$fields = [ $fields = [
@@ -203,6 +205,16 @@ class ShopPaymentMethodController
'tab' => 'settings', 'tab' => 'settings',
'rows' => 5, 'rows' => 5,
]), ]),
FormField::number('min_order_amount', [
'label' => 'Min. kwota zamowienia (PLN)',
'tab' => 'settings',
'step' => 0.01,
]),
FormField::number('max_order_amount', [
'label' => 'Maks. kwota zamowienia (PLN)',
'tab' => 'settings',
'step' => 0.01,
]),
FormField::select('apilo_payment_type_id', [ FormField::select('apilo_payment_type_id', [
'label' => 'Typ platnosci Apilo', 'label' => 'Typ platnosci Apilo',
'tab' => 'settings', 'tab' => 'settings',

View File

@@ -4,6 +4,17 @@ Logi zmian z migracji na Domain-Driven Architecture. Najnowsze na gorze.
--- ---
## ver. 0.304 (2026-02-22) - Konfigurowalne limity kwotowe metod platnosci
- **NEW**: Kolumny `min_order_amount` i `max_order_amount` w `pp_shop_payment_methods` — konfigurowalne limity kwotowe per metoda platnosci
- **NEW**: Pola min/max kwoty zamowienia w formularzu edycji metody platnosci (admin)
- **FIX**: Zastapiono hardcoded warunek PayPo (id=6, 40-1000 PLN) generycznym filtrowaniem na froncie (basket checkout)
- **NEW**: Cache invalidation po zapisie metody platnosci
- **NEW**: 4 nowe testy jednostkowe (734 total, 2080 assertions)
- **MIGRATION**: `migrations/0.304.sql` — ALTER TABLE pp_shop_payment_methods ADD min/max_order_amount
---
## ver. 0.303 (2026-02-22) - Fix: wyswietlanie atrybutow produktu na froncie + podglad produktu w adminie ## ver. 0.303 (2026-02-22) - Fix: wyswietlanie atrybutow produktu na froncie + podglad produktu w adminie
- **FIX**: Naprawiono wyswietlanie atrybutow produktu na froncie — gdy dwa atrybuty mialy te sama wartosc kolejnosci (`o`), jeden nadpisywal drugi (kolizja kluczy tablicy). Teraz atrybuty sortowane przez `usort()` z unikalnymi kluczami sekwencyjnymi. - **FIX**: Naprawiono wyswietlanie atrybutow produktu na froncie — gdy dwa atrybuty mialy te sama wartosc kolejnosci (`o`), jeden nadpisywal drugi (kolizja kluczy tablicy). Teraz atrybuty sortowane przez `usort()` z unikalnymi kluczami sekwencyjnymi.

View File

@@ -508,12 +508,16 @@ Metody platnosci sklepu (modul `/admin/shop_payment_method`).
| description | Opis metody platnosci (wyswietlany m.in. w checkout) | | description | Opis metody platnosci (wyswietlany m.in. w checkout) |
| status | Status: 1 = aktywna, 0 = nieaktywna | | status | Status: 1 = aktywna, 0 = nieaktywna |
| apilo_payment_type_id | ID typu platnosci Apilo (NULL gdy brak mapowania) | | apilo_payment_type_id | ID typu platnosci Apilo (NULL gdy brak mapowania) |
| min_order_amount | Minimalna kwota zamowienia (DECIMAL(10,2), NULL = brak limitu) |
| max_order_amount | Maksymalna kwota zamowienia (DECIMAL(10,2), NULL = brak limitu) |
| sellasist_payment_type_id | DEPRECATED (integracja Sellasist usunieta w ver. 0.263) | | sellasist_payment_type_id | DEPRECATED (integracja Sellasist usunieta w ver. 0.263) |
**Uzywane w:** `Domain\PaymentMethod\PaymentMethodRepository`, `admin\Controllers\ShopPaymentMethodController`, `front\factory\ShopPaymentMethod`, `shop\PaymentMethod`, `admin\controls\ShopTransport`, `cron.php` **Uzywane w:** `Domain\PaymentMethod\PaymentMethodRepository`, `admin\Controllers\ShopPaymentMethodController`, `front\factory\ShopPaymentMethod`, `shop\PaymentMethod`, `admin\controls\ShopTransport`, `cron.php`
**Aktualizacja 2026-02-14 (ver. 0.268):** modul `/admin/shop_payment_method` korzysta z `Domain\PaymentMethod\PaymentMethodRepository` przez `admin\Controllers\ShopPaymentMethodController`. Usunieto legacy klasy `admin\controls\ShopPaymentMethod`, `admin\factory\ShopPaymentMethod`, `admin\view\ShopPaymentMethod` oraz widok `admin/templates/shop-payment-method/view-list.php`. **Aktualizacja 2026-02-14 (ver. 0.268):** modul `/admin/shop_payment_method` korzysta z `Domain\PaymentMethod\PaymentMethodRepository` przez `admin\Controllers\ShopPaymentMethodController`. Usunieto legacy klasy `admin\controls\ShopPaymentMethod`, `admin\factory\ShopPaymentMethod`, `admin\view\ShopPaymentMethod` oraz widok `admin/templates/shop-payment-method/view-list.php`.
**Aktualizacja 2026-02-22 (ver. 0.304):** dodano kolumny `min_order_amount` i `max_order_amount` — konfigurowalne limity kwotowe metod platnosci. Zastapiono hardcoded warunek PayPo (id=6, 40-1000 PLN) generycznym filtrowaniem na froncie.
## pp_shop_transports ## pp_shop_transports
Rodzaje transportu sklepu (modul `/admin/shop_transport`). Rodzaje transportu sklepu (modul `/admin/shop_transport`).

View File

@@ -23,10 +23,10 @@ composer test # standard
## Aktualny stan ## Aktualny stan
```text ```text
OK (730 tests, 2066 assertions) OK (734 tests, 2080 assertions)
``` ```
Zweryfikowano: 2026-02-21 (ver. 0.300) Zweryfikowano: 2026-02-22 (ver. 0.304)
## Konfiguracja ## Konfiguracja

2
migrations/0.304.sql Normal file
View File

@@ -0,0 +1,2 @@
ALTER TABLE pp_shop_payment_methods ADD COLUMN min_order_amount DECIMAL(10,2) DEFAULT NULL;
ALTER TABLE pp_shop_payment_methods ADD COLUMN max_order_amount DECIMAL(10,2) DEFAULT NULL;

View File

@@ -13,7 +13,14 @@
$basket_summary = \Domain\Basket\BasketCalculator::summaryPrice( $basket, $coupon ) + $transport_cost; $basket_summary = \Domain\Basket\BasketCalculator::summaryPrice( $basket, $coupon ) + $transport_cost;
?> ?>
<? if ( is_array( $this -> payment_methods ) ): foreach ( $this -> payment_methods as $payment_method ):?> <? if ( is_array( $this -> payment_methods ) ): foreach ( $this -> payment_methods as $payment_method ):?>
<? if ( $payment_method['id'] != 6 or $payment_method['id'] == 6 and $basket_summary >= 40 and $basket_summary <= 1000 ):?> <?
$min = isset($payment_method['min_order_amount']) ? (float)$payment_method['min_order_amount'] : null;
$max = isset($payment_method['max_order_amount']) ? (float)$payment_method['max_order_amount'] : null;
$show = true;
if ($min !== null && $min > 0 && $basket_summary < $min) $show = false;
if ($max !== null && $max > 0 && $basket_summary > $max) $show = false;
?>
<? if ( $show ):?>
<div class="options"> <div class="options">
<div class="check"> <div class="check">
<input type="radio" class="icheck" name="payment_method" value="<?= $payment_method['id'];?>" <input type="radio" class="icheck" name="payment_method" value="<?= $payment_method['id'];?>"

View File

@@ -78,6 +78,8 @@ class PaymentMethodRepositoryTest extends TestCase
$this->assertSame('test', $updateRow['description']); $this->assertSame('test', $updateRow['description']);
$this->assertSame(1, $updateRow['status']); $this->assertSame(1, $updateRow['status']);
$this->assertSame(22, $updateRow['apilo_payment_type_id']); $this->assertSame(22, $updateRow['apilo_payment_type_id']);
$this->assertNull($updateRow['min_order_amount']);
$this->assertNull($updateRow['max_order_amount']);
$this->assertSame(['id' => 3], $updateWhere); $this->assertSame(['id' => 3], $updateWhere);
} }
@@ -113,6 +115,102 @@ class PaymentMethodRepositoryTest extends TestCase
$this->assertNull($repository->save(0, ['status' => 1])); $this->assertNull($repository->save(0, ['status' => 1]));
} }
public function testSavePersistsMinMaxOrderAmount(): void
{
$mockDb = $this->createMock(\medoo::class);
$updateRow = null;
$mockDb->expects($this->once())
->method('update')
->willReturnCallback(function ($table, $row) use (&$updateRow) {
$updateRow = $row;
return true;
});
$repository = new PaymentMethodRepository($mockDb);
$repository->save(5, [
'description' => 'test',
'status' => 1,
'apilo_payment_type_id' => '',
'min_order_amount' => '40.00',
'max_order_amount' => '1000.00',
]);
$this->assertSame(40.0, $updateRow['min_order_amount']);
$this->assertSame(1000.0, $updateRow['max_order_amount']);
}
public function testSaveConvertsEmptyMinMaxToNull(): void
{
$mockDb = $this->createMock(\medoo::class);
$updateRow = null;
$mockDb->expects($this->once())
->method('update')
->willReturnCallback(function ($table, $row) use (&$updateRow) {
$updateRow = $row;
return true;
});
$repository = new PaymentMethodRepository($mockDb);
$repository->save(6, [
'description' => 'test',
'status' => 1,
'apilo_payment_type_id' => '',
'min_order_amount' => '',
'max_order_amount' => '',
]);
$this->assertNull($updateRow['min_order_amount']);
$this->assertNull($updateRow['max_order_amount']);
}
public function testFindNormalizesMinMaxOrderAmount(): void
{
$mockDb = $this->createMock(\medoo::class);
$mockDb->expects($this->once())
->method('get')
->with('pp_shop_payment_methods', '*', ['id' => 6])
->willReturn([
'id' => '6',
'name' => 'PayPo',
'description' => '',
'status' => '1',
'apilo_payment_type_id' => null,
'min_order_amount' => '40.00',
'max_order_amount' => '1000.00',
]);
$repository = new PaymentMethodRepository($mockDb);
$result = $repository->find(6);
$this->assertSame(40.0, $result['min_order_amount']);
$this->assertSame(1000.0, $result['max_order_amount']);
}
public function testFindNormalizesNullMinMaxOrderAmount(): void
{
$mockDb = $this->createMock(\medoo::class);
$mockDb->expects($this->once())
->method('get')
->with('pp_shop_payment_methods', '*', ['id' => 7])
->willReturn([
'id' => '7',
'name' => 'Przelew',
'description' => '',
'status' => '1',
'apilo_payment_type_id' => null,
'min_order_amount' => null,
'max_order_amount' => null,
]);
$repository = new PaymentMethodRepository($mockDb);
$result = $repository->find(7);
$this->assertNull($result['min_order_amount']);
$this->assertNull($result['max_order_amount']);
}
public function testListForAdminWhitelistsSortAndDirection(): void public function testListForAdminWhitelistsSortAndDirection(): void
{ {
$mockDb = $this->createMock(\medoo::class); $mockDb = $this->createMock(\medoo::class);