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`
+