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:
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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)) {
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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`).
|
||||||
|
|
||||||
|
|||||||
@@ -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
2
migrations/0.304.sql
Normal 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;
|
||||||
@@ -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'];?>"
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user