diff --git a/autoload/shop/class.Order.php b/autoload/shop/class.Order.php index 23b61a9..251f4be 100644 --- a/autoload/shop/class.Order.php +++ b/autoload/shop/class.Order.php @@ -3,6 +3,8 @@ namespace shop; class Order implements \ArrayAccess { + private const APILO_SYNC_QUEUE_FILE = '/temp/apilo-sync-queue.json'; + public $id; public $products; public $statuses; @@ -89,31 +91,9 @@ class Order implements \ArrayAccess file_put_contents( $_SERVER['DOCUMENT_ROOT'] . '/logs/apilo.txt', print_r( $this, true ) . "\n\n", FILE_APPEND ); } - if ( $this -> apilo_order_id ) + if ( $this -> apilo_order_id and !$this -> sync_apilo_payment() ) { - $payment_date = new \DateTime( $this -> date_order ); - $access_token = \admin\factory\Integrations::apilo_get_access_token(); - - $ch = curl_init(); - curl_setopt( $ch, CURLOPT_URL, "https://projectpro.apilo.com/rest/api/orders/" . $this -> apilo_order_id . '/payment/' ); - curl_setopt( $ch, CURLOPT_POST, 1 ); - curl_setopt( $ch, CURLOPT_POSTFIELDS, json_encode( [ - 'amount' => str_replace( ',', '.', $this -> summary ), - 'paymentDate' => $payment_date -> format('Y-m-d\TH:i:s\Z'), - 'type' => 1 - ] ) ); - curl_setopt( $ch, CURLOPT_HTTPHEADER, [ - "Authorization: Bearer " . $access_token, - "Accept: application/json" - ] ); - curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true); - $apilo_response = curl_exec( $ch ); - - // put response to log - if ( $config['debug']['apilo'] ) - file_put_contents( $_SERVER['DOCUMENT_ROOT'] . '/logs/apilo.txt', print_r( $apilo_response, true ) . "\n\n", FILE_APPEND ); - - curl_close( $ch ); + self::queue_apilo_sync( (int)$this -> id, true, null, 'payment_sync_failed' ); } } @@ -167,31 +147,9 @@ class Order implements \ArrayAccess file_put_contents( $_SERVER['DOCUMENT_ROOT'] . '/logs/apilo.txt', print_r( $this, true ) . "\n\n", FILE_APPEND ); } - if ( $this -> apilo_order_id ) + if ( $this -> apilo_order_id and !$this -> sync_apilo_status( (int)$status ) ) { - $access_token = \admin\factory\Integrations::apilo_get_access_token(); - - $ch = curl_init(); - curl_setopt( $ch, CURLOPT_URL, "https://projectpro.apilo.com/rest/api/orders/" . $this -> apilo_order_id . '/status/' ); - curl_setopt( $ch, CURLOPT_POST, 1 ); - curl_setopt( $ch, CURLOPT_CUSTOMREQUEST, "PUT"); - curl_setopt( $ch, CURLOPT_POSTFIELDS, json_encode( [ - 'id' => $this -> apilo_order_id, - 'status' => (int)\front\factory\ShopStatuses::get_apilo_status_id( $status ) - ] ) ); - curl_setopt( $ch, CURLOPT_HTTPHEADER, [ - "Authorization: Bearer " . $access_token, - "Accept: application/json", - "Content-Type: application/json" - ] ); - curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true); - $apilo_result = curl_exec( $ch ); - - // put response to log - if ( $config['debug']['apilo'] ) - file_put_contents( $_SERVER['DOCUMENT_ROOT'] . '/logs/apilo.txt', print_r( $apilo_result, true ) . "\n\n", FILE_APPEND ); - - curl_close( $ch ); + self::queue_apilo_sync( (int)$this -> id, false, (int)$status, 'status_sync_failed' ); } } @@ -361,4 +319,238 @@ class Order implements \ArrayAccess { unset( $this -> $offset ); } -} \ No newline at end of file + + private function sync_apilo_payment(): bool + { + global $config; + + if ( !(int)$this -> apilo_order_id ) + return true; + + $payment_type = (int)\front\factory\ShopPaymentMethod::get_apilo_payment_method_id( (int)$this -> payment_method_id ); + if ( $payment_type <= 0 ) + $payment_type = 1; + + $payment_date = new \DateTime( $this -> date_order ); + $access_token = \admin\factory\Integrations::apilo_get_access_token(); + + $ch = curl_init(); + curl_setopt( $ch, CURLOPT_URL, "https://projectpro.apilo.com/rest/api/orders/" . $this -> apilo_order_id . '/payment/' ); + curl_setopt( $ch, CURLOPT_POST, 1 ); + curl_setopt( $ch, CURLOPT_POSTFIELDS, json_encode( [ + 'amount' => str_replace( ',', '.', $this -> summary ), + 'paymentDate' => $payment_date -> format('Y-m-d\TH:i:s\Z'), + 'type' => $payment_type + ] ) ); + curl_setopt( $ch, CURLOPT_HTTPHEADER, [ + "Authorization: Bearer " . $access_token, + "Accept: application/json", + "Content-Type: application/json" + ] ); + curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true ); + curl_setopt( $ch, CURLOPT_CONNECTTIMEOUT, 5 ); + curl_setopt( $ch, CURLOPT_TIMEOUT, 15 ); + $apilo_response = curl_exec( $ch ); + $http_code = (int)curl_getinfo( $ch, CURLINFO_HTTP_CODE ); + $curl_error = curl_errno( $ch ) ? curl_error( $ch ) : ''; + curl_close( $ch ); + + if ( $config['debug']['apilo'] ) + { + self::append_apilo_log( "PAYMENT RESPONSE\nHTTP: " . $http_code . "\nCURL: " . $curl_error . "\n" . print_r( $apilo_response, true ) . "\n" ); + } + + if ( $curl_error !== '' ) + return false; + + if ( $http_code < 200 or $http_code >= 300 ) + return false; + + return true; + } + + private function sync_apilo_status( int $status ): bool + { + global $config; + + if ( !(int)$this -> apilo_order_id ) + return true; + + $access_token = \admin\factory\Integrations::apilo_get_access_token(); + + $ch = curl_init(); + curl_setopt( $ch, CURLOPT_URL, "https://projectpro.apilo.com/rest/api/orders/" . $this -> apilo_order_id . '/status/' ); + curl_setopt( $ch, CURLOPT_POST, 1 ); + curl_setopt( $ch, CURLOPT_CUSTOMREQUEST, "PUT"); + curl_setopt( $ch, CURLOPT_POSTFIELDS, json_encode( [ + 'id' => $this -> apilo_order_id, + 'status' => (int)\front\factory\ShopStatuses::get_apilo_status_id( $status ) + ] ) ); + curl_setopt( $ch, CURLOPT_HTTPHEADER, [ + "Authorization: Bearer " . $access_token, + "Accept: application/json", + "Content-Type: application/json" + ] ); + curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true ); + curl_setopt( $ch, CURLOPT_CONNECTTIMEOUT, 5 ); + curl_setopt( $ch, CURLOPT_TIMEOUT, 15 ); + $apilo_result = curl_exec( $ch ); + $http_code = (int)curl_getinfo( $ch, CURLINFO_HTTP_CODE ); + $curl_error = curl_errno( $ch ) ? curl_error( $ch ) : ''; + curl_close( $ch ); + + if ( $config['debug']['apilo'] ) + { + self::append_apilo_log( "STATUS RESPONSE\nHTTP: " . $http_code . "\nCURL: " . $curl_error . "\n" . print_r( $apilo_result, true ) . "\n" ); + } + + if ( $curl_error !== '' ) + return false; + + if ( $http_code < 200 or $http_code >= 300 ) + return false; + + return true; + } + + public static function process_apilo_sync_queue( int $limit = 10 ): int + { + $queue = self::load_apilo_sync_queue(); + if ( !\S::is_array_fix( $queue ) ) + return 0; + + $processed = 0; + + foreach ( $queue as $key => $task ) + { + if ( $processed >= $limit ) + break; + + $order_id = (int)( $task['order_id'] ?? 0 ); + if ( $order_id <= 0 ) + { + unset( $queue[$key] ); + continue; + } + + $order = new self( $order_id ); + if ( !(int)$order -> id ) + { + unset( $queue[$key] ); + continue; + } + + $error = ''; + $sync_failed = false; + + $payment_pending = !empty( $task['payment'] ) and (int)$order -> paid === 1; + if ( $payment_pending and (int)$order -> apilo_order_id ) + { + if ( !$order -> sync_apilo_payment() ) + { + $sync_failed = true; + $error = 'payment_sync_failed'; + } + } + + $status_pending = isset( $task['status'] ) and $task['status'] !== null and $task['status'] !== ''; + if ( !$sync_failed and $status_pending and (int)$order -> apilo_order_id ) + { + if ( !$order -> sync_apilo_status( (int)$task['status'] ) ) + { + $sync_failed = true; + $error = 'status_sync_failed'; + } + } + + if ( $sync_failed ) + { + $task['attempts'] = (int)( $task['attempts'] ?? 0 ) + 1; + $task['last_error'] = $error; + $task['updated_at'] = date( 'Y-m-d H:i:s' ); + $queue[$key] = $task; + } + else + { + unset( $queue[$key] ); + } + + $processed++; + } + + self::save_apilo_sync_queue( $queue ); + + return $processed; + } + + private static function queue_apilo_sync( int $order_id, bool $payment, ?int $status, string $error ): void + { + if ( $order_id <= 0 ) + return; + + $queue = self::load_apilo_sync_queue(); + $key = (string)$order_id; + $row = is_array( $queue[$key] ?? null ) ? $queue[$key] : []; + + $row['order_id'] = $order_id; + $row['payment'] = !empty( $row['payment'] ) || $payment ? 1 : 0; + if ( $status !== null ) + $row['status'] = $status; + + $row['attempts'] = (int)( $row['attempts'] ?? 0 ) + 1; + $row['last_error'] = $error; + $row['updated_at'] = date( 'Y-m-d H:i:s' ); + + $queue[$key] = $row; + self::save_apilo_sync_queue( $queue ); + } + + private static function apilo_sync_queue_path(): string + { + return dirname( __DIR__, 2 ) . self::APILO_SYNC_QUEUE_FILE; + } + + private static function load_apilo_sync_queue(): array + { + $path = self::apilo_sync_queue_path(); + if ( !file_exists( $path ) ) + return []; + + $content = file_get_contents( $path ); + if ( !$content ) + return []; + + $decoded = json_decode( $content, true ); + if ( !is_array( $decoded ) ) + return []; + + return $decoded; + } + + private static function save_apilo_sync_queue( array $queue ): void + { + $path = self::apilo_sync_queue_path(); + $dir = dirname( $path ); + if ( !is_dir( $dir ) ) + mkdir( $dir, 0777, true ); + + file_put_contents( $path, json_encode( $queue, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ), LOCK_EX ); + } + + private static function append_apilo_log( string $message ): void + { + $base = isset( $_SERVER['DOCUMENT_ROOT'] ) && $_SERVER['DOCUMENT_ROOT'] + ? rtrim( $_SERVER['DOCUMENT_ROOT'], '/\\' ) + : dirname( __DIR__, 2 ); + + $dir = $base . '/logs'; + if ( !is_dir( $dir ) ) + mkdir( $dir, 0777, true ); + + file_put_contents( + $dir . '/apilo.txt', + date( 'Y-m-d H:i:s' ) . ' --- ' . $message . "\n\n", + FILE_APPEND + ); + } +} diff --git a/cron.php b/cron.php index f8996a9..921d385 100644 --- a/cron.php +++ b/cron.php @@ -59,6 +59,7 @@ $apilo_settings = \admin\factory\Integrations::apilo_settings(); if ( (int)($apilo_settings['enabled'] ?? 0) === 1 ) { \admin\factory\Integrations::apilo_keepalive( 300 ); $apilo_settings = \admin\factory\Integrations::apilo_settings(); + Order::process_apilo_sync_queue( 10 ); } function parsePaczkomatAddress($input) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 657e55b..c31dc33 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -4,6 +4,18 @@ Logi zmian z migracji na Domain-Driven Architecture. Najnowsze na gorze. --- +## ver. 0.270 (2026-02-14) - Apilo payment/status sync hardening + +- **Shop/Order + Apilo** - utwardzenie synchronizacji platnosci i statusow zamowien + - FIX: `shop\Order::set_as_paid()` wysyla do Apilo mapowany typ platnosci (`payment_method_id` -> `apilo_payment_type_id`) zamiast stalego `type = 1` + - NOWE: retry queue dla chwilowej niedostepnosci Apilo (`temp/apilo-sync-queue.json`) dla sync platnosci i statusu + - NOWE: `shop\Order::process_apilo_sync_queue()` przetwarza zalegle syncy + - UPDATE: `cron.php` uruchamia przetwarzanie kolejki sync Apilo przy aktywnej integracji + - UPDATE: rozszerzone logowanie debug (`logs/apilo.txt`) o HTTP code i bledy cURL dla sync platnosci/statusu +- Testy: **OK (300 tests, 895 assertions)** + +--- + ## ver. 0.269 (2026-02-14) - ShopTransport - **ShopTransport** - migracja `/admin/shop_transport` na Domain + DI + nowe widoki diff --git a/docs/PROJECT_STRUCTURE.md b/docs/PROJECT_STRUCTURE.md index 016afee..accb166 100644 --- a/docs/PROJECT_STRUCTURE.md +++ b/docs/PROJECT_STRUCTURE.md @@ -62,6 +62,8 @@ shop\product:{product_id}:{lang_id}:{permutation_hash} - Czestotliwosc: Co 10 minut - **Synchronizacja cennika:** masowa aktualizacja cen z Apilo - Czestotliwosc: Co 1 godzine +- **Synchronizacja zaleglych syncow platnosci/statusow:** kolejka retry dla chwilowej niedostepnosci Apilo (`temp/apilo-sync-queue.json`) + - Przetwarzanie: przy kazdym uruchomieniu `cron.php` (limit wsadowy) **Uwaga:** Integracje Sellasist i Baselinker zostaly usuniete w ver. 0.263. @@ -242,6 +244,11 @@ autoload/ - Usunieto legacy: `autoload/admin/controls/class.ShopTransport.php`, `autoload/admin/view/class.ShopTransport.php`, `admin/templates/shop-transport/view-list.php`. - `admin\factory\ShopTransport` i `front\factory\ShopTransport` przepiete na repozytorium. +**Aktualizacja 2026-02-14 (ver. 0.270):** +- `shop\Order` zapisuje nieudane syncy Apilo (status/platnosc) do kolejki `temp/apilo-sync-queue.json`. +- `cron.php` automatycznie ponawia zalegle syncy (`Order::process_apilo_sync_queue()`). +- `shop\Order::set_as_paid()` wysyla mapowany typ platnosci Apilo (z mapowania metody platnosci), bez stalej wartosci `type`. + ### Routing admin (admin\Site::route()) 1. Sprawdź mapę `$newControllers` → utwórz instancję z DI → wywołaj 2. Jeśli nowy kontroler nie istnieje (`class_exists()` = false) → fallback na `admin\controls\` diff --git a/docs/TESTING.md b/docs/TESTING.md index 18f4481..e1e4026 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -375,3 +375,13 @@ OK (300 tests, 895 assertions) Nowe testy dodane 2026-02-14: - `tests/Unit/Domain/Transport/TransportRepositoryTest.php` (14 testow: find invalid/null/normalize/nullables, save insert/update/failure/default reset/switch normalization, listForAdmin whitelist, allActive, getApiloCarrierAccountId, getTransportCost, allForAdmin) - `tests/Unit/admin/Controllers/ShopTransportControllerTest.php` (5 testow: kontrakty metod, brak aliasow legacy, return types, DI konstruktora z 2 repo) + +## Aktualizacja suite (Apilo sync hardening, ver. 0.270) +Ostatnio zweryfikowano: 2026-02-14 + +```text +OK (300 tests, 895 assertions) +``` + +Zmiany testowe 2026-02-14: +- brak nowych testow; pelna regresja po zmianach sync Apilo (TPAY -> Apilo) przeszla bez bledow diff --git a/updates/0.20/ver_0.270.zip b/updates/0.20/ver_0.270.zip new file mode 100644 index 0000000..5480496 Binary files /dev/null and b/updates/0.20/ver_0.270.zip differ diff --git a/updates/0.20/ver_0.270_files.txt b/updates/0.20/ver_0.270_files.txt new file mode 100644 index 0000000..e69de29 diff --git a/updates/changelog.php b/updates/changelog.php index f272a35..60ef483 100644 --- a/updates/changelog.php +++ b/updates/changelog.php @@ -1,3 +1,11 @@ +ver. 0.270 - 14.02.2026
+- FIX - Apilo: `shop\Order::set_as_paid()` wysyla mapowany typ platnosci Apilo (z `payment_method_id`), zamiast stalego `type = 1` +- NEW - Apilo: dodana kolejka retry `temp/apilo-sync-queue.json` dla nieudanych syncow platnosci/statusu (chwilowa niedostepnosc API) +- UPDATE - `cron.php`: automatyczne ponawianie zaleglych syncow przez `Order::process_apilo_sync_queue(10)` +- UPDATE - debug Apilo: rozszerzone logi odpowiedzi o HTTP code i bledy cURL dla sync platnosci/statusu +- UPDATE - testy: `OK (300 tests, 895 assertions)` +- UPDATE - pliki aktualizacji: `updates/0.20/ver_0.270.zip`, `ver_0.270_files.txt` +
ver. 0.269 - 14.02.2026
- NEW - migracja modulu `ShopPaymentMethod` do architektury Domain + DI (`Domain\PaymentMethod\PaymentMethodRepository`, `admin\Controllers\ShopPaymentMethodController`) - UPDATE - modul `/admin/shop_payment_method/*` przepiety z legacy `grid/gridEdit` na `components/table-list` i `components/form-edit` (nowe widoki listy i edycji) @@ -490,4 +498,3 @@
ver. 0.142
- FIX - poprawa adresu strony głównej - diff --git a/updates/versions.php b/updates/versions.php index 7ff553d..c23a72b 100644 --- a/updates/versions.php +++ b/updates/versions.php @@ -1,5 +1,5 @@