From fdc4cac593a745bebf376e9cf66ec74e749c0fc4 Mon Sep 17 00:00:00 2001 From: Jacek Pyziak Date: Mon, 23 Feb 2026 10:50:34 +0100 Subject: [PATCH] =?UTF-8?q?ver.=200.311:=20fix=20race=20condition=20Apilo?= =?UTF-8?q?=20+=20persistence=20filtr=C3=B3w=20+=20poprawki=20cen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix: race condition callback płatności przed wysłaniem do Apilo - Fix: processApiloSyncQueue czeka na apilo_order_id zamiast usuwać task - Fix: drugie wywołanie processApiloSyncQueue po wysyłce zamówień w cronie - Fix: ceny w szczegółach zamówienia (effective price zamiast 0 zł) - New: persistence filtrów tabel admin (localStorage) - Testy: 760 tests, 2141 assertions Co-Authored-By: Claude Opus 4.6 --- admin/templates/components/table-list.php | 48 +++++++- admin/templates/shop-order/order-details.php | 7 +- autoload/Domain/Order/OrderAdminService.php | 31 ++++- cron.php | 3 + docs/CHANGELOG.md | 10 ++ templates/shop-order/order-details.php | 2 +- .../Domain/Order/OrderAdminServiceTest.php | 106 ++++++++++++++++++ updates/versions.php | 2 +- 8 files changed, 199 insertions(+), 10 deletions(-) diff --git a/admin/templates/components/table-list.php b/admin/templates/components/table-list.php index 4acf9a7..28e01a5 100644 --- a/admin/templates/components/table-list.php +++ b/admin/templates/components/table-list.php @@ -162,7 +162,7 @@ $isCompactColumn = function(array $column): bool {
- Wyczyść + Wyczyść
@@ -312,6 +312,40 @@ $isCompactColumn = function(array $column): bool { + + diff --git a/admin/templates/shop-order/order-details.php b/admin/templates/shop-order/order-details.php index 1561386..a8acd83 100644 --- a/admin/templates/shop-order/order-details.php +++ b/admin/templates/shop-order/order-details.php @@ -184,13 +184,14 @@ $orderId = (int)($this -> order['id'] ?? 0); Wiadomość: ' . $product['message'] : '';?>
- × = zł + 0 && (float)$product['price_brutto_promo'] < (float)$product['price_brutto']) ? (float)$product['price_brutto_promo'] : (float)$product['price_brutto'];?> + × =
zł - zł - zł + zł + diff --git a/autoload/Domain/Order/OrderAdminService.php b/autoload/Domain/Order/OrderAdminService.php index 003812b..7753095 100644 --- a/autoload/Domain/Order/OrderAdminService.php +++ b/autoload/Domain/Order/OrderAdminService.php @@ -533,9 +533,26 @@ class OrderAdminService $error = ''; $sync_failed = false; + $max_attempts = 50; // ~8h przy cronie co 10 min + + // Zamówienie jeszcze nie wysłane do Apilo — czekaj na crona + if (!(int)$order['apilo_order_id']) { + $attempts = (int)($task['attempts'] ?? 0) + 1; + if ($attempts >= $max_attempts) { + // Przekroczono limit prób — porzuć task + unset($queue[$key]); + } else { + $task['attempts'] = $attempts; + $task['last_error'] = 'awaiting_apilo_order'; + $task['updated_at'] = date('Y-m-d H:i:s'); + $queue[$key] = $task; + } + $processed++; + continue; + } $payment_pending = !empty($task['payment']) && (int)$order['paid'] === 1; - if ($payment_pending && (int)$order['apilo_order_id']) { + if ($payment_pending) { if (!$this->syncApiloPayment($order)) { $sync_failed = true; $error = 'payment_sync_failed'; @@ -543,7 +560,7 @@ class OrderAdminService } $status_pending = isset($task['status']) && $task['status'] !== null && $task['status'] !== ''; - if (!$sync_failed && $status_pending && (int)$order['apilo_order_id']) { + if (!$sync_failed && $status_pending) { if (!$this->syncApiloStatus($order, (int)$task['status'])) { $sync_failed = true; $error = 'status_sync_failed'; @@ -631,7 +648,10 @@ class OrderAdminService self::appendApiloLog("SET AS PAID\n" . print_r($order, true)); } - if ($order['apilo_order_id'] && !$this->syncApiloPayment($order)) { + if (!$order['apilo_order_id']) { + // Zamówienie jeszcze nie wysłane do Apilo — kolejkuj sync płatności na później + self::queueApiloSync((int)$order['id'], true, null, 'awaiting_apilo_order'); + } elseif (!$this->syncApiloPayment($order)) { self::queueApiloSync((int)$order['id'], true, null, 'payment_sync_failed'); } } @@ -652,7 +672,10 @@ class OrderAdminService self::appendApiloLog("UPDATE STATUS\n" . print_r($order, true)); } - if ($order['apilo_order_id'] && !$this->syncApiloStatus($order, $status)) { + if (!$order['apilo_order_id']) { + // Zamówienie jeszcze nie wysłane do Apilo — kolejkuj sync statusu na później + self::queueApiloSync((int)$order['id'], false, $status, 'awaiting_apilo_order'); + } elseif (!$this->syncApiloStatus($order, $status)) { self::queueApiloSync((int)$order['id'], false, $status, 'status_sync_failed'); } } diff --git a/cron.php b/cron.php index 0972db8..3b42f59 100644 --- a/cron.php +++ b/cron.php @@ -504,6 +504,9 @@ if ( $apilo_settings['enabled'] and $apilo_settings['sync_orders'] and $apilo_se echo '

Wysłałem zamówienie do apilo.com: ID: ' . $order['id'] . ' - ' . $response['id'] . '

'; } } + + // Po wysłaniu zamówień: przetwórz kolejkę sync (płatności/statusy oczekujące na apilo_order_id) + $orderAdminService->processApiloSyncQueue( 10 ); } // sprawdzanie statusów zamówień w apilo.com jeżeli zamówienie nie jest zrealizowane, anulowane lub nieodebrane diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 9791af4..2c0739f 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -4,6 +4,16 @@ Logi zmian z migracji na Domain-Driven Architecture. Najnowsze na gorze. --- +## ver. 0.311 (2026-02-23) - Fix race condition Apilo + persistence filtrów + poprawki cen + +- **FIX**: Race condition — callback płatności przed wysłaniem zamówienia do Apilo nie synchronizował płatności (task trafiał w pustkę). Teraz `syncApiloPaymentIfNeeded` i `syncApiloStatusIfNeeded` kolejkują sync do retry gdy `apilo_order_id` jeszcze nie istnieje +- **FIX**: `processApiloSyncQueue` — zamówienia bez `apilo_order_id` były usuwane z kolejki bez synchronizacji. Teraz czekają (max 50 prób ~8h) aż cron wyśle zamówienie do Apilo +- **FIX**: Drugie wywołanie `processApiloSyncQueue` w cronie po wysyłce zamówień — sync płatności/statusów w tym samym cyklu +- **FIX**: Ceny w szczegółach zamówienia (admin + frontend) — gdy `price_brutto_promo` = 0 lub >= ceny regularnej, wyświetla cenę regularną zamiast 0 zł +- **NEW**: Persistence filtrów tabel w panelu admin — localStorage zapamiętuje ostatni widok (filtry, sortowanie, paginacja) i przywraca go przy powrocie do listy. Przycisk "Wyczyść" resetuje zapisany stan + +--- + ## ver. 0.310 (2026-02-23) - Logi integracji w panelu admin - **NEW**: Zakładka "Logi" w sekcji Integracje — podgląd tabeli `pp_log` z paginacją, sortowaniem, filtrami (akcja, wiadomość, ID zamówienia) i rozwijalnym kontekstem JSON diff --git a/templates/shop-order/order-details.php b/templates/shop-order/order-details.php index e81af24..d32eef6 100644 --- a/templates/shop-order/order-details.php +++ b/templates/shop-order/order-details.php @@ -179,7 +179,7 @@ 'id': , 'name': '', 'quantity': , - 'price': + 'price': 0 && (float)$product['price_brutto_promo'] < (float)$product['price_brutto']) ? (float)$product['price_brutto_promo'] : (float)$product['price_brutto'];?> } order['products'] ) ) echo ',';?> ] diff --git a/tests/Unit/Domain/Order/OrderAdminServiceTest.php b/tests/Unit/Domain/Order/OrderAdminServiceTest.php index 569a0da..b74c78d 100644 --- a/tests/Unit/Domain/Order/OrderAdminServiceTest.php +++ b/tests/Unit/Domain/Order/OrderAdminServiceTest.php @@ -227,4 +227,110 @@ class OrderAdminServiceTest extends TestCase $service = $this->createService(null, null, $settingsRepo); $this->assertSame(150.0, $service->getFreeDeliveryThreshold()); } + + // ========================================================================= + // processApiloSyncQueue — awaiting apilo_order_id + // ========================================================================= + + private function getQueuePath(): string + { + // Musi odpowiadać ścieżce w OrderAdminService::apiloSyncQueuePath() + // dirname(autoload/Domain/Order/, 2) = autoload/ + return dirname(__DIR__, 4) . '/autoload/temp/apilo-sync-queue.json'; + } + + private function writeQueue(array $queue): void + { + $path = $this->getQueuePath(); + $dir = dirname($path); + if (!is_dir($dir)) { + mkdir($dir, 0777, true); + } + file_put_contents($path, json_encode($queue, JSON_PRETTY_PRINT)); + } + + private function readQueue(): array + { + $path = $this->getQueuePath(); + if (!file_exists($path)) return []; + $content = file_get_contents($path); + return $content ? json_decode($content, true) : []; + } + + protected function tearDown(): void + { + $path = $this->getQueuePath(); + if (file_exists($path)) { + unlink($path); + } + parent::tearDown(); + } + + public function testProcessApiloSyncQueueKeepsTaskWhenApiloOrderIdIsNull(): void + { + // Zamówienie bez apilo_order_id — task powinien zostać w kolejce + $this->writeQueue([ + '42' => [ + 'order_id' => 42, + 'payment' => 1, + 'status' => null, + 'attempts' => 0, + 'last_error' => 'awaiting_apilo_order', + 'updated_at' => '2026-01-01 00:00:00', + ], + ]); + + $orderRepo = $this->createMock(OrderRepository::class); + $orderRepo->method('findRawById') + ->with(42) + ->willReturn([ + 'id' => 42, + 'apilo_order_id' => null, + 'paid' => 1, + 'summary' => '100.00', + ]); + + $service = new OrderAdminService($orderRepo); + $processed = $service->processApiloSyncQueue(10); + + $this->assertSame(1, $processed); + + $queue = $this->readQueue(); + $this->assertArrayHasKey('42', $queue); + $this->assertSame('awaiting_apilo_order', $queue['42']['last_error']); + $this->assertSame(1, $queue['42']['attempts']); + } + + public function testProcessApiloSyncQueueRemovesTaskAfterMaxAttempts(): void + { + // Task z 49 próbami — limit to 50, więc powinien zostać usunięty + $this->writeQueue([ + '42' => [ + 'order_id' => 42, + 'payment' => 1, + 'status' => null, + 'attempts' => 49, + 'last_error' => 'awaiting_apilo_order', + 'updated_at' => '2026-01-01 00:00:00', + ], + ]); + + $orderRepo = $this->createMock(OrderRepository::class); + $orderRepo->method('findRawById') + ->with(42) + ->willReturn([ + 'id' => 42, + 'apilo_order_id' => null, + 'paid' => 1, + 'summary' => '100.00', + ]); + + $service = new OrderAdminService($orderRepo); + $processed = $service->processApiloSyncQueue(10); + + $this->assertSame(1, $processed); + + $queue = $this->readQueue(); + $this->assertArrayNotHasKey('42', $queue); + } } diff --git a/updates/versions.php b/updates/versions.php index 2c56836..d15c6c1 100644 --- a/updates/versions.php +++ b/updates/versions.php @@ -1,5 +1,5 @@