feat: database-backed cron job queue replacing JSON file system
Replace file-based JSON cron queue with DB-backed job queue (pp_cron_jobs, pp_cron_schedules). New Domain\CronJob module: CronJobType (constants), CronJobRepository (CRUD, atomic fetch, retry/backoff), CronJobProcessor (orchestration with handler registration). Priority ordering guarantees apilo_send_order (40) runs before sync tasks (50). Includes cron.php auth protection, race condition fix in fetchNext, API response validation, and DI wiring across all entry points. 41 new tests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
467
cron.php
467
cron.php
@@ -50,19 +50,26 @@ $mdb = new medoo( [
|
||||
'charset' => 'utf8'
|
||||
] );
|
||||
|
||||
$settings = ( new \Domain\Settings\SettingsRepository( $mdb ) )->allSettings();
|
||||
$integrationsRepository = new \Domain\Integrations\IntegrationsRepository( $mdb );
|
||||
$apilo_settings = $integrationsRepository -> getSettings( 'apilo' );
|
||||
// =========================================================================
|
||||
// Auth: cron endpoint protection
|
||||
// =========================================================================
|
||||
|
||||
// Keepalive tokenu Apilo: odswiezaj token przed wygasnieciem, zeby integracja byla stale aktywna.
|
||||
if ( (int)($apilo_settings['enabled'] ?? 0) === 1 ) {
|
||||
$integrationsRepository -> apiloKeepalive( 300 );
|
||||
$apilo_settings = $integrationsRepository -> getSettings( 'apilo' );
|
||||
$orderRepo = new \Domain\Order\OrderRepository( $mdb );
|
||||
$orderAdminService = new \Domain\Order\OrderAdminService( $orderRepo );
|
||||
$orderAdminService->processApiloSyncQueue( 10 );
|
||||
if ( php_sapi_name() !== 'cli' )
|
||||
{
|
||||
$cron_key = isset( $config['cron_key'] ) ? $config['cron_key'] : '';
|
||||
$provided_key = isset( $_GET['key'] ) ? $_GET['key'] : '';
|
||||
|
||||
if ( $cron_key === '' || $provided_key !== $cron_key )
|
||||
{
|
||||
http_response_code( 403 );
|
||||
exit( 'Forbidden' );
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Helper functions (used by handlers)
|
||||
// =========================================================================
|
||||
|
||||
function parsePaczkomatAddress($input)
|
||||
{
|
||||
$pattern = '/^([\w-]+)\s+\|\s+([^,]+),\s+(\d{2}-\d{3})\s+(.+)$/';
|
||||
@@ -118,93 +125,90 @@ function getImageUrlById($id) {
|
||||
return isset($data['img']) ? $data['img'] : null;
|
||||
}
|
||||
|
||||
// pobieranie informacji o produkcie z apilo.com
|
||||
if ( $apilo_settings['enabled'] and $apilo_settings['sync_products'] and $apilo_settings['access-token'] )
|
||||
// =========================================================================
|
||||
// Shared dependencies
|
||||
// =========================================================================
|
||||
|
||||
$settings = ( new \Domain\Settings\SettingsRepository( $mdb ) )->allSettings();
|
||||
$integrationsRepository = new \Domain\Integrations\IntegrationsRepository( $mdb );
|
||||
$orderRepo = new \Domain\Order\OrderRepository( $mdb );
|
||||
$cronRepo = new \Domain\CronJob\CronJobRepository( $mdb );
|
||||
$orderAdminService = new \Domain\Order\OrderAdminService( $orderRepo, null, null, null, $cronRepo );
|
||||
|
||||
$processor = new \Domain\CronJob\CronJobProcessor( $cronRepo );
|
||||
|
||||
// =========================================================================
|
||||
// One-time migration: JSON queue → DB
|
||||
// =========================================================================
|
||||
|
||||
$json_queue_path = __DIR__ . '/temp/apilo-sync-queue.json';
|
||||
if ( file_exists( $json_queue_path ) )
|
||||
{
|
||||
if ( $result = $mdb -> query( 'SELECT id, apilo_product_id, apilo_get_data_date, apilo_product_name FROM pp_shop_products WHERE apilo_product_id IS NOT NULL AND apilo_product_id != 0 AND ( apilo_get_data_date IS NULL OR apilo_get_data_date <= \'' . date( 'Y-m-d H:i:s', strtotime( '-10 minutes', time() ) ) . '\' ) ORDER BY apilo_get_data_date ASC LIMIT 1' ) -> fetch( \PDO::FETCH_ASSOC ) )
|
||||
$json_content = file_get_contents( $json_queue_path );
|
||||
$json_queue = $json_content ? json_decode( $json_content, true ) : [];
|
||||
|
||||
if ( is_array( $json_queue ) )
|
||||
{
|
||||
$access_token = $integrationsRepository -> apiloGetAccessToken();
|
||||
$url = 'https://projectpro.apilo.com/rest/api/warehouse/product/' . $result['apilo_product_id'] . '/';
|
||||
$curl = curl_init( $url );
|
||||
curl_setopt( $curl, CURLOPT_RETURNTRANSFER, true );
|
||||
curl_setopt( $curl, CURLOPT_HTTPHEADER, [
|
||||
"Authorization: Bearer " . $access_token,
|
||||
"Accept: application/json"
|
||||
] );
|
||||
|
||||
$response = curl_exec( $curl );
|
||||
$responseData = json_decode( $response, true );
|
||||
|
||||
// aktualizowanie stanu magazynowego
|
||||
$mdb -> update( 'pp_shop_products', [ 'quantity' => $responseData['quantity'] ], [ 'apilo_product_id' => $result['apilo_product_id'] ] );
|
||||
// aktualizowanie ceny
|
||||
$mdb -> update( 'pp_shop_products', [ 'price_netto' => \Shared\Helpers\Helpers::normalize_decimal( $responseData['priceWithoutTax'], 2 ), 'price_brutto' => \Shared\Helpers\Helpers::normalize_decimal( $responseData['priceWithTax'], 2 ) ], [ 'apilo_product_id' => $result['apilo_product_id'] ] );
|
||||
|
||||
$mdb -> update( 'pp_shop_products', [ 'apilo_get_data_date' => date( 'Y-m-d H:i:s' ) ], [ 'apilo_product_id' => $result['apilo_product_id'] ] );
|
||||
|
||||
// Czyszczenie cache produktu
|
||||
\Shared\Helpers\Helpers::clear_product_cache( (int)$result['id'] );
|
||||
|
||||
echo '<p>Zaktualizowałem dane produktu (APILO) <b>' . $result['apilo_product_name'] . ' #' . $result['id'] . '</b></p>';
|
||||
}
|
||||
}
|
||||
|
||||
// synchronizacja cen apilo.com
|
||||
if ( $apilo_settings['enabled'] and $apilo_settings['access-token'] and ( !$apilo_settings['pricelist_update_date'] or $apilo_settings['pricelist_update_date'] <= date( 'Y-m-d H:i:s', strtotime( '-1 hour', time() ) ) ) )
|
||||
{
|
||||
$access_token = $integrationsRepository -> apiloGetAccessToken();
|
||||
|
||||
$url = 'https://projectpro.apilo.com/rest/api/warehouse/price-calculated/?price=' . $apilo_settings['pricelist_id'];
|
||||
|
||||
$curl = curl_init( $url );
|
||||
curl_setopt( $curl, CURLOPT_RETURNTRANSFER, true );
|
||||
curl_setopt( $curl, CURLOPT_CUSTOMREQUEST, "GET" );
|
||||
curl_setopt( $curl, CURLOPT_HTTPHEADER, [
|
||||
"Authorization: Bearer " . $access_token,
|
||||
"Accept: application/json",
|
||||
"Content-Type: application/json"
|
||||
] );
|
||||
|
||||
$response = curl_exec( $curl );
|
||||
$responseData = json_decode( $response, true );
|
||||
|
||||
if ( $responseData['list'] )
|
||||
{
|
||||
foreach ( $responseData['list'] as $product_price )
|
||||
foreach ( $json_queue as $task )
|
||||
{
|
||||
//aktualizowanie ceny
|
||||
if ( $product_price['customPriceWithTax'] )
|
||||
$order_id = (int)($task['order_id'] ?? 0);
|
||||
if ( $order_id <= 0 ) continue;
|
||||
|
||||
if ( !empty($task['payment']) )
|
||||
{
|
||||
$price_brutto = $product_price['customPriceWithTax'];
|
||||
$vat = $vat = $mdb -> get( 'pp_shop_products', 'vat', [ 'apilo_product_id' => $result['apilo_product_id'] ] );
|
||||
$price_netto = $price_brutto / ( ( 100 + $vat ) / 100 );
|
||||
|
||||
$mdb -> update( 'pp_shop_products', [ 'price_netto' => \Shared\Helpers\Helpers::normalize_decimal( $price_netto, 2 ), 'price_brutto' => \Shared\Helpers\Helpers::normalize_decimal( $price_brutto, 2 ) ], [ 'apilo_product_id' => $product_price['product'] ] );
|
||||
$product_id = $mdb -> get( 'pp_shop_products', 'id', [ 'apilo_product_id' => $product_price['product'] ] );
|
||||
|
||||
( new \Domain\Product\ProductRepository( $mdb ) )->updateCombinationPricesFromBase( (int)$product_id, $price_brutto, $vat, null );
|
||||
|
||||
// Czyszczenie cache produktu
|
||||
\Shared\Helpers\Helpers::clear_product_cache( (int)$product_id );
|
||||
$cronRepo->enqueue(
|
||||
\Domain\CronJob\CronJobType::APILO_SYNC_PAYMENT,
|
||||
['order_id' => $order_id],
|
||||
\Domain\CronJob\CronJobType::PRIORITY_HIGH,
|
||||
50
|
||||
);
|
||||
}
|
||||
if ( isset($task['status']) && $task['status'] !== null && $task['status'] !== '' )
|
||||
{
|
||||
$cronRepo->enqueue(
|
||||
\Domain\CronJob\CronJobType::APILO_SYNC_STATUS,
|
||||
['order_id' => $order_id, 'status' => (int)$task['status']],
|
||||
\Domain\CronJob\CronJobType::PRIORITY_HIGH,
|
||||
50
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
$integrationsRepository -> saveSetting( 'apilo', 'pricelist_update_date', date( 'Y-m-d H:i:s' ) );
|
||||
echo '<p>Zaktualizowałem ceny produktów (APILO)</p>';
|
||||
|
||||
unlink( $json_queue_path );
|
||||
echo '<p>Migracja kolejki JSON → DB zakończona</p>';
|
||||
}
|
||||
|
||||
// wysyłanie zamówień do apilo
|
||||
if ( $apilo_settings['enabled'] and $apilo_settings['sync_orders'] and $apilo_settings['access-token'] and $apilo_settings['sync_orders_date_start'] <= date( 'Y-m-d H:i:s' ) )
|
||||
{
|
||||
$orders = $mdb -> select( 'pp_shop_orders', '*', [ 'AND' => [ 'apilo_order_id' => null, 'date_order[>=]' => $apilo_settings['sync_orders_date_start'] ], 'ORDER' => [ 'date_order' => 'ASC' ], 'LIMIT' => 1 ] );
|
||||
// =========================================================================
|
||||
// Handler registration
|
||||
// =========================================================================
|
||||
|
||||
// 1. Apilo token keepalive (priorytet: krytyczny)
|
||||
$processor->registerHandler( \Domain\CronJob\CronJobType::APILO_TOKEN_KEEPALIVE, function($payload) use ($integrationsRepository) {
|
||||
$apilo_settings = $integrationsRepository->getSettings('apilo');
|
||||
if ( !(int)($apilo_settings['enabled'] ?? 0) ) return true; // skip if disabled
|
||||
|
||||
$integrationsRepository->apiloKeepalive( 300 );
|
||||
echo '<p>Apilo token keepalive</p>';
|
||||
return true;
|
||||
});
|
||||
|
||||
// 2. Apilo send order (priorytet: wysoki)
|
||||
$processor->registerHandler( \Domain\CronJob\CronJobType::APILO_SEND_ORDER, function($payload) use ($mdb, $integrationsRepository, $orderAdminService, $config) {
|
||||
$apilo_settings = $integrationsRepository->getSettings('apilo');
|
||||
if ( !$apilo_settings['enabled'] || !$apilo_settings['sync_orders'] || !$apilo_settings['access-token'] || $apilo_settings['sync_orders_date_start'] > date('Y-m-d H:i:s') ) return true;
|
||||
|
||||
$orders = $mdb->select( 'pp_shop_orders', '*', [ 'AND' => [ 'apilo_order_id' => null, 'date_order[>=]' => $apilo_settings['sync_orders_date_start'] ], 'ORDER' => [ 'date_order' => 'ASC' ], 'LIMIT' => 1 ] );
|
||||
if ( empty($orders) ) return true;
|
||||
|
||||
foreach ( $orders as $order )
|
||||
{
|
||||
$products = $mdb -> select( 'pp_shop_order_products', '*', [ 'order_id' => $order['id'] ] );
|
||||
$products = $mdb->select( 'pp_shop_order_products', '*', [ 'order_id' => $order['id'] ] );
|
||||
$productRepo = new \Domain\Product\ProductRepository( $mdb );
|
||||
$products_array = [];
|
||||
$order_message = '';
|
||||
foreach ( $products as $product )
|
||||
{
|
||||
$productRepo = new \Domain\Product\ProductRepository( $mdb );
|
||||
$sku = $productRepo->getSkuWithFallback( (int)$product['product_id'], true );
|
||||
|
||||
$products_array[] = [
|
||||
@@ -237,11 +241,9 @@ if ( $apilo_settings['enabled'] and $apilo_settings['sync_orders'] and $apilo_se
|
||||
$order_message .= '<hr>';
|
||||
}
|
||||
|
||||
//TODO: ostatnio był problem kiedy wiadomość miała mniej 1024 znaki ale zawierała przeniesienie tekstu '<br>' i do tego jeszcze miała emoji. Wtedy APILO tego nie przepuszczał.
|
||||
if ( strlen( $order_message ) > 850 )
|
||||
$order_message = '<p><strong>Wiadomość do zamówienia była zbyt długa. Sprawdź szczegóły w panelu sklepu</strong></p>';
|
||||
|
||||
// add transport as product
|
||||
$products_array[] = [
|
||||
'idExternal' => '',
|
||||
'ean' => null,
|
||||
@@ -256,7 +258,6 @@ if ( $apilo_settings['enabled'] and $apilo_settings['sync_orders'] and $apilo_se
|
||||
'media' => null
|
||||
];
|
||||
|
||||
// Walidacja: sprawdź czy zamówienie ma produkty z cenami > 0
|
||||
$has_priced_products = false;
|
||||
foreach ( $products_array as $pa )
|
||||
{
|
||||
@@ -270,15 +271,13 @@ if ( $apilo_settings['enabled'] and $apilo_settings['sync_orders'] and $apilo_se
|
||||
{
|
||||
\Domain\Integrations\ApiloLogger::log( $mdb, 'send_order', (int)$order['id'], 'Pominięto zamówienie - wszystkie produkty mają cenę 0.00', [ 'products' => $products_array ] );
|
||||
\Shared\Helpers\Helpers::send_email( 'biuro@project-pro.pl', 'Apilo: zamówienie #' . $order['id'] . ' ma zerowe ceny produktów', 'Zamówienie #' . $order['id'] . ' nie zostało wysłane do Apilo, ponieważ wszystkie produkty mają cenę 0.00 PLN. Sprawdź zamówienie w panelu sklepu.' );
|
||||
$mdb -> update( 'pp_shop_orders', [ 'apilo_order_id' => -2 ], [ 'id' => $order['id'] ] );
|
||||
$mdb->update( 'pp_shop_orders', [ 'apilo_order_id' => -2 ], [ 'id' => $order['id'] ] );
|
||||
echo '<p>Pominięto zamówienie #' . $order['id'] . ' - zerowe ceny produktów</p>';
|
||||
continue;
|
||||
}
|
||||
|
||||
$access_token = $integrationsRepository -> apiloGetAccessToken();
|
||||
|
||||
$access_token = $integrationsRepository->apiloGetAccessToken();
|
||||
$order_date = new DateTime( $order['date_order'] );
|
||||
|
||||
$paczkomatData = parsePaczkomatAddress( $order['inpost_paczkomat'] );
|
||||
$orlenPointData = parseOrlenAddress( $order['orlen_point'] );
|
||||
|
||||
@@ -326,7 +325,7 @@ if ( $apilo_settings['enabled'] and $apilo_settings['sync_orders'] and $apilo_se
|
||||
'originalCurrency' => 'PLN',
|
||||
'originalAmountTotalWithTax' => str_replace( ',', '.', $order['summary'] ),
|
||||
'orderItems' => $products_array,
|
||||
'orderedAt' => $order_date -> format('Y-m-d\TH:i:s\Z'),
|
||||
'orderedAt' => $order_date->format('Y-m-d\TH:i:s\Z'),
|
||||
'addressCustomer' => [
|
||||
'name' => $order['client_name'] . ' ' . $order['client_surname'],
|
||||
'phone' => $order['client_phone'],
|
||||
@@ -361,7 +360,6 @@ if ( $apilo_settings['enabled'] and $apilo_settings['sync_orders'] and $apilo_se
|
||||
$postData['addressInvoice']['companyTaxNumber'] = $order['firm_nip'];
|
||||
}
|
||||
|
||||
// jeżeli paczkomat
|
||||
if ( $order['inpost_paczkomat'] )
|
||||
{
|
||||
$postData['addressDelivery']['parcelName'] = $order['inpost_paczkomat'] ? 'Paczkomat: ' . $order['inpost_paczkomat'] : null;
|
||||
@@ -381,7 +379,6 @@ if ( $apilo_settings['enabled'] and $apilo_settings['sync_orders'] and $apilo_se
|
||||
}
|
||||
}
|
||||
|
||||
// jeżeli orlen paczka
|
||||
if ( $order['orlen_point'] )
|
||||
{
|
||||
$postData['addressDelivery']['parcelName'] = $order['orlen_point'] ? 'Automat ORLEN ' . $order['orlen_point'] : null;
|
||||
@@ -399,16 +396,14 @@ if ( $apilo_settings['enabled'] and $apilo_settings['sync_orders'] and $apilo_se
|
||||
$postData['addressDelivery']['zipCode'] = $postalCode;
|
||||
$postData['addressDelivery']['city'] = $city;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if ( $order['paid'] )
|
||||
{
|
||||
$payment_date = new DateTime( $order['date_order'] );
|
||||
|
||||
$postData['orderPayments'][] = [
|
||||
'amount' => str_replace( ',', '.', $order['summary'] ),
|
||||
'paymentDate' => $payment_date -> format('Y-m-d\TH:i:s\Z'),
|
||||
'paymentDate' => $payment_date->format('Y-m-d\TH:i:s\Z'),
|
||||
'type' => ( new \Domain\PaymentMethod\PaymentMethodRepository( $mdb ) )->getApiloPaymentTypeId( (int)$order['payment_method_id'] )
|
||||
];
|
||||
}
|
||||
@@ -435,30 +430,29 @@ if ( $apilo_settings['enabled'] and $apilo_settings['sync_orders'] and $apilo_se
|
||||
|
||||
$response = json_decode( $response, true );
|
||||
|
||||
if ( $config['debug']['apilo'] )
|
||||
if ( isset($config['debug']['apilo']) && $config['debug']['apilo'] )
|
||||
{
|
||||
file_put_contents( $_SERVER['DOCUMENT_ROOT'] . '/logs/apilo.txt', date( 'Y-m-d H:i:s' ) . " --- SEND ORDER TO APILO\n\n", FILE_APPEND );
|
||||
file_put_contents( $_SERVER['DOCUMENT_ROOT'] . '/logs/apilo.txt', print_r( $postData, true ) . "\n\n", FILE_APPEND );
|
||||
file_put_contents( $_SERVER['DOCUMENT_ROOT'] . '/logs/apilo.txt', print_r( $response, true ) . "\n\n", FILE_APPEND );
|
||||
}
|
||||
|
||||
if ( $response['message'] == 'Order already exists' )
|
||||
if ( isset($response['message']) && $response['message'] == 'Order already exists' )
|
||||
{
|
||||
$apilo_order_id = str_replace( 'Order id: ', '', $response['description'] );
|
||||
$mdb -> update( 'pp_shop_orders', [ 'apilo_order_id' => $apilo_order_id ], [ 'id' => $order['id'] ] );
|
||||
$mdb->update( 'pp_shop_orders', [ 'apilo_order_id' => $apilo_order_id ], [ 'id' => $order['id'] ] );
|
||||
\Domain\Integrations\ApiloLogger::log( $mdb, 'send_order', (int)$order['id'], 'Zamówienie już istnieje w Apilo (apilo_order_id: ' . $apilo_order_id . ')', [ 'http_code' => $http_code_send, 'response' => $response ] );
|
||||
echo '<p>Zaktualizowałem id zamówienia na podstawie zamówienia apilo.com</p>';
|
||||
}
|
||||
elseif ( $response['message'] == 'Validation error' )
|
||||
elseif ( isset($response['message']) && $response['message'] == 'Validation error' )
|
||||
{
|
||||
// sprawdzanie czy błąd dotyczy duplikatu idExternal
|
||||
$is_duplicate_idexternal = false;
|
||||
if ( isset( $response['errors'] ) and is_array( $response['errors'] ) )
|
||||
if ( isset( $response['errors'] ) && is_array( $response['errors'] ) )
|
||||
{
|
||||
foreach ( $response['errors'] as $error )
|
||||
{
|
||||
if ( isset( $error['field'] ) and $error['field'] == 'idExternal' and
|
||||
( strpos( $error['message'], 'już wykorzystywana' ) !== false or
|
||||
if ( isset( $error['field'] ) && $error['field'] == 'idExternal' &&
|
||||
( strpos( $error['message'], 'już wykorzystywana' ) !== false ||
|
||||
strpos( $error['message'], 'already' ) !== false ) )
|
||||
{
|
||||
$is_duplicate_idexternal = true;
|
||||
@@ -469,7 +463,6 @@ if ( $apilo_settings['enabled'] and $apilo_settings['sync_orders'] and $apilo_se
|
||||
|
||||
if ( $is_duplicate_idexternal )
|
||||
{
|
||||
// próba pobrania zamówienia z Apilo na podstawie idExternal
|
||||
$ch_get = curl_init();
|
||||
curl_setopt( $ch_get, CURLOPT_URL, "https://projectpro.apilo.com/rest/api/orders/?idExternal=" . $order['id'] );
|
||||
curl_setopt( $ch_get, CURLOPT_RETURNTRANSFER, true );
|
||||
@@ -482,22 +475,16 @@ if ( $apilo_settings['enabled'] and $apilo_settings['sync_orders'] and $apilo_se
|
||||
|
||||
$get_response_data = json_decode( $get_response, true );
|
||||
|
||||
if ( isset( $get_response_data['list'] ) and count( $get_response_data['list'] ) > 0 )
|
||||
if ( isset( $get_response_data['list'] ) && count( $get_response_data['list'] ) > 0 )
|
||||
{
|
||||
$apilo_order_id = $get_response_data['list'][0]['id'];
|
||||
$mdb -> update( 'pp_shop_orders', [ 'apilo_order_id' => $apilo_order_id ], [ 'id' => $order['id'] ] );
|
||||
$mdb->update( 'pp_shop_orders', [ 'apilo_order_id' => $apilo_order_id ], [ 'id' => $order['id'] ] );
|
||||
\Domain\Integrations\ApiloLogger::log( $mdb, 'send_order', (int)$order['id'], 'Duplikat idExternal - pobrano apilo_order_id: ' . $apilo_order_id, [ 'http_code' => $http_code_send, 'response' => $response, 'get_response' => $get_response_data ] );
|
||||
echo '<p>Zamówienie już istnieje w Apilo. Zaktualizowano ID zamówienia: ' . $apilo_order_id . '</p>';
|
||||
}
|
||||
else
|
||||
{
|
||||
echo '<pre>';
|
||||
echo print_r( $response, true );
|
||||
echo print_r( $postData, true );
|
||||
echo '</pre>';
|
||||
|
||||
\Domain\Integrations\ApiloLogger::log( $mdb, 'send_order', (int)$order['id'], 'Błąd: duplikat idExternal, ale nie znaleziono zamówienia w Apilo', [ 'http_code' => $http_code_send, 'response' => $response, 'get_response' => $get_response_data ] );
|
||||
|
||||
$email_data = print_r( $response, true );
|
||||
$email_data .= print_r( $postData, true );
|
||||
\Shared\Helpers\Helpers::send_email( 'biuro@project-pro.pl', 'Błąd wysyłania zamówienia do apilo.com - nie znaleziono zamówienia', $email_data );
|
||||
@@ -505,13 +492,7 @@ if ( $apilo_settings['enabled'] and $apilo_settings['sync_orders'] and $apilo_se
|
||||
}
|
||||
else
|
||||
{
|
||||
echo '<pre>';
|
||||
echo print_r( $response, true );
|
||||
echo print_r( $postData, true );
|
||||
echo '</pre>';
|
||||
|
||||
\Domain\Integrations\ApiloLogger::log( $mdb, 'send_order', (int)$order['id'], 'Błąd walidacji wysyłania zamówienia do Apilo', [ 'http_code' => $http_code_send, 'response' => $response ] );
|
||||
|
||||
$email_data = print_r( $response, true );
|
||||
$email_data .= print_r( $postData, true );
|
||||
\Shared\Helpers\Helpers::send_email( 'biuro@project-pro.pl', 'Błąd wysyłania zamówienia do apilo.com', $email_data );
|
||||
@@ -519,39 +500,146 @@ if ( $apilo_settings['enabled'] and $apilo_settings['sync_orders'] and $apilo_se
|
||||
}
|
||||
elseif ( $http_code_send >= 400 || !isset( $response['id'] ) )
|
||||
{
|
||||
// Błąd serwera lub brak ID w odpowiedzi — logujemy i pomijamy, NIE ustawiamy apilo_order_id
|
||||
// żeby zamówienie nie wpadło w nieskończoną pętlę, ustawiamy apilo_order_id na -1 (błąd)
|
||||
$mdb -> update( 'pp_shop_orders', [ 'apilo_order_id' => -1 ], [ 'id' => $order['id'] ] );
|
||||
$mdb->update( 'pp_shop_orders', [ 'apilo_order_id' => -1 ], [ 'id' => $order['id'] ] );
|
||||
\Domain\Integrations\ApiloLogger::log( $mdb, 'send_order', (int)$order['id'], 'Błąd wysyłania zamówienia do Apilo (HTTP ' . $http_code_send . ')', [ 'http_code' => $http_code_send, 'response' => $response ] );
|
||||
|
||||
$email_data = 'HTTP Code: ' . $http_code_send . "\n\n";
|
||||
$email_data .= print_r( $response, true );
|
||||
$email_data .= print_r( $postData, true );
|
||||
\Shared\Helpers\Helpers::send_email( 'biuro@project-pro.pl', 'Błąd wysyłania zamówienia #' . $order['id'] . ' do apilo.com (HTTP ' . $http_code_send . ')', $email_data );
|
||||
|
||||
echo '<p>Błąd wysyłania zamówienia do apilo.com: ID: ' . $order['id'] . ' (HTTP ' . $http_code_send . ')</p>';
|
||||
}
|
||||
else
|
||||
{
|
||||
$mdb -> update( 'pp_shop_orders', [ 'apilo_order_id' => $response['id'] ], [ 'id' => $order['id'] ] );
|
||||
$mdb->update( 'pp_shop_orders', [ 'apilo_order_id' => $response['id'] ], [ 'id' => $order['id'] ] );
|
||||
\Domain\Integrations\ApiloLogger::log( $mdb, 'send_order', (int)$order['id'], 'Zamówienie wysłane do Apilo (apilo_order_id: ' . $response['id'] . ')', [ 'http_code' => $http_code_send, 'response' => $response ] );
|
||||
echo '<p>Wysłałem zamówienie do apilo.com: ID: ' . $order['id'] . ' - ' . $response['id'] . '</p>';
|
||||
}
|
||||
}
|
||||
|
||||
// Po wysłaniu zamówień: przetwórz kolejkę sync (płatności/statusy oczekujące na apilo_order_id)
|
||||
$orderAdminService->processApiloSyncQueue( 10 );
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
// 3. Apilo sync payment (event-driven — enqueued by OrderAdminService)
|
||||
$processor->registerHandler( \Domain\CronJob\CronJobType::APILO_SYNC_PAYMENT, function($payload) use ($mdb, $orderRepo, $orderAdminService) {
|
||||
$order_id = (int)($payload['order_id'] ?? 0);
|
||||
if ( $order_id <= 0 ) return true;
|
||||
|
||||
$order = $orderRepo->findRawById( $order_id );
|
||||
if ( !$order ) return true;
|
||||
|
||||
if ( empty($order['apilo_order_id']) ) return false; // retry — awaiting apilo_order_id
|
||||
|
||||
if ( (int)$order['paid'] !== 1 ) return true; // not paid — nothing to sync
|
||||
|
||||
return $orderAdminService->syncApiloPayment( $order );
|
||||
});
|
||||
|
||||
// 4. Apilo sync status (event-driven — enqueued by OrderAdminService)
|
||||
$processor->registerHandler( \Domain\CronJob\CronJobType::APILO_SYNC_STATUS, function($payload) use ($mdb, $orderRepo, $orderAdminService) {
|
||||
$order_id = (int)($payload['order_id'] ?? 0);
|
||||
$status = isset($payload['status']) ? (int)$payload['status'] : null;
|
||||
if ( $order_id <= 0 || $status === null ) return true;
|
||||
|
||||
$order = $orderRepo->findRawById( $order_id );
|
||||
if ( !$order ) return true;
|
||||
|
||||
if ( empty($order['apilo_order_id']) ) return false; // retry — awaiting apilo_order_id
|
||||
|
||||
return $orderAdminService->syncApiloStatus( $order, $status );
|
||||
});
|
||||
|
||||
// 5. Apilo product sync
|
||||
$processor->registerHandler( \Domain\CronJob\CronJobType::APILO_PRODUCT_SYNC, function($payload) use ($mdb, $integrationsRepository) {
|
||||
$apilo_settings = $integrationsRepository->getSettings('apilo');
|
||||
if ( !$apilo_settings['enabled'] || !$apilo_settings['sync_products'] || !$apilo_settings['access-token'] ) return true;
|
||||
|
||||
$stmt = $mdb->query( 'SELECT id, apilo_product_id, apilo_get_data_date, apilo_product_name FROM pp_shop_products WHERE apilo_product_id IS NOT NULL AND apilo_product_id != 0 AND ( apilo_get_data_date IS NULL OR apilo_get_data_date <= \'' . date( 'Y-m-d H:i:s', strtotime( '-10 minutes', time() ) ) . '\' ) ORDER BY apilo_get_data_date ASC LIMIT 1' );
|
||||
$result = $stmt ? $stmt->fetch( \PDO::FETCH_ASSOC ) : null;
|
||||
if ( !$result ) return true;
|
||||
|
||||
$access_token = $integrationsRepository->apiloGetAccessToken();
|
||||
$url = 'https://projectpro.apilo.com/rest/api/warehouse/product/' . $result['apilo_product_id'] . '/';
|
||||
$curl = curl_init( $url );
|
||||
curl_setopt( $curl, CURLOPT_RETURNTRANSFER, true );
|
||||
curl_setopt( $curl, CURLOPT_HTTPHEADER, [
|
||||
"Authorization: Bearer " . $access_token,
|
||||
"Accept: application/json"
|
||||
] );
|
||||
|
||||
$response = curl_exec( $curl );
|
||||
if ( $response === false ) return false;
|
||||
|
||||
$responseData = json_decode( $response, true );
|
||||
if ( !is_array( $responseData ) || !isset( $responseData['quantity'] ) ) return false;
|
||||
|
||||
$mdb->update( 'pp_shop_products', [ 'quantity' => $responseData['quantity'] ], [ 'apilo_product_id' => $result['apilo_product_id'] ] );
|
||||
$mdb->update( 'pp_shop_products', [ 'price_netto' => \Shared\Helpers\Helpers::normalize_decimal( $responseData['priceWithoutTax'], 2 ), 'price_brutto' => \Shared\Helpers\Helpers::normalize_decimal( $responseData['priceWithTax'], 2 ) ], [ 'apilo_product_id' => $result['apilo_product_id'] ] );
|
||||
$mdb->update( 'pp_shop_products', [ 'apilo_get_data_date' => date( 'Y-m-d H:i:s' ) ], [ 'apilo_product_id' => $result['apilo_product_id'] ] );
|
||||
\Shared\Helpers\Helpers::clear_product_cache( (int)$result['id'] );
|
||||
|
||||
echo '<p>Zaktualizowałem dane produktu (APILO) <b>' . $result['apilo_product_name'] . ' #' . $result['id'] . '</b></p>';
|
||||
return true;
|
||||
});
|
||||
|
||||
// 6. Apilo pricelist sync
|
||||
$processor->registerHandler( \Domain\CronJob\CronJobType::APILO_PRICELIST_SYNC, function($payload) use ($mdb, $integrationsRepository) {
|
||||
$apilo_settings = $integrationsRepository->getSettings('apilo');
|
||||
if ( !$apilo_settings['enabled'] || !$apilo_settings['access-token'] ) return true;
|
||||
|
||||
$access_token = $integrationsRepository->apiloGetAccessToken();
|
||||
$url = 'https://projectpro.apilo.com/rest/api/warehouse/price-calculated/?price=' . $apilo_settings['pricelist_id'];
|
||||
|
||||
$curl = curl_init( $url );
|
||||
curl_setopt( $curl, CURLOPT_RETURNTRANSFER, true );
|
||||
curl_setopt( $curl, CURLOPT_CUSTOMREQUEST, "GET" );
|
||||
curl_setopt( $curl, CURLOPT_HTTPHEADER, [
|
||||
"Authorization: Bearer " . $access_token,
|
||||
"Accept: application/json",
|
||||
"Content-Type: application/json"
|
||||
] );
|
||||
|
||||
$response = curl_exec( $curl );
|
||||
if ( $response === false ) return false;
|
||||
|
||||
$responseData = json_decode( $response, true );
|
||||
if ( !is_array( $responseData ) ) return false;
|
||||
|
||||
if ( isset($responseData['list']) && $responseData['list'] )
|
||||
{
|
||||
foreach ( $responseData['list'] as $product_price )
|
||||
{
|
||||
if ( $product_price['customPriceWithTax'] )
|
||||
{
|
||||
$price_brutto = $product_price['customPriceWithTax'];
|
||||
$vat = $mdb->get( 'pp_shop_products', 'vat', [ 'apilo_product_id' => $product_price['product'] ] );
|
||||
$price_netto = $price_brutto / ( ( 100 + $vat ) / 100 );
|
||||
|
||||
$mdb->update( 'pp_shop_products', [ 'price_netto' => \Shared\Helpers\Helpers::normalize_decimal( $price_netto, 2 ), 'price_brutto' => \Shared\Helpers\Helpers::normalize_decimal( $price_brutto, 2 ) ], [ 'apilo_product_id' => $product_price['product'] ] );
|
||||
$product_id = $mdb->get( 'pp_shop_products', 'id', [ 'apilo_product_id' => $product_price['product'] ] );
|
||||
|
||||
( new \Domain\Product\ProductRepository( $mdb ) )->updateCombinationPricesFromBase( (int)$product_id, $price_brutto, $vat, null );
|
||||
\Shared\Helpers\Helpers::clear_product_cache( (int)$product_id );
|
||||
}
|
||||
}
|
||||
}
|
||||
$integrationsRepository->saveSetting( 'apilo', 'pricelist_update_date', date( 'Y-m-d H:i:s' ) );
|
||||
echo '<p>Zaktualizowałem ceny produktów (APILO)</p>';
|
||||
return true;
|
||||
});
|
||||
|
||||
// 7. Apilo status poll
|
||||
$processor->registerHandler( \Domain\CronJob\CronJobType::APILO_STATUS_POLL, function($payload) use ($mdb, $integrationsRepository, $orderRepo, $orderAdminService) {
|
||||
$apilo_settings = $integrationsRepository->getSettings('apilo');
|
||||
if ( !$apilo_settings['enabled'] || !$apilo_settings['sync_orders'] || !$apilo_settings['access-token'] ) return true;
|
||||
|
||||
$stmt = $mdb->query( 'SELECT id, apilo_order_id, apilo_order_status_date, number FROM pp_shop_orders WHERE apilo_order_id IS NOT NULL AND ( status != 6 AND status != 8 AND status != 9 ) AND ( apilo_order_status_date IS NULL OR apilo_order_status_date <= \'' . date( 'Y-m-d H:i:s', strtotime( '-10 minutes', time() ) ) . '\' ) ORDER BY apilo_order_status_date ASC LIMIT 5' );
|
||||
$orders = $stmt ? $stmt->fetchAll( \PDO::FETCH_ASSOC ) : [];
|
||||
|
||||
// sprawdzanie statusów zamówień w apilo.com jeżeli zamówienie nie jest zrealizowane, anulowane lub nieodebrane
|
||||
if ( $apilo_settings['enabled'] and $apilo_settings['sync_orders'] and $apilo_settings['access-token'] and $apilo_settings['sync_orders_date_start'] <= date( 'Y-m-d H:i:s' ) )
|
||||
{
|
||||
$orders = $mdb -> query( 'SELECT id, apilo_order_id, apilo_order_status_date, number FROM pp_shop_orders WHERE apilo_order_id IS NOT NULL AND ( status != 6 AND status != 8 AND status != 9 ) AND ( apilo_order_status_date IS NULL OR apilo_order_status_date <= \'' . date( 'Y-m-d H:i:s', strtotime( '-10 minutes', time() ) ) . '\' ) ORDER BY apilo_order_status_date ASC LIMIT 5' ) -> fetchAll( \PDO::FETCH_ASSOC );
|
||||
foreach ( $orders as $order )
|
||||
{
|
||||
if ( $order['apilo_order_id'] )
|
||||
{
|
||||
$access_token = $integrationsRepository -> apiloGetAccessToken();
|
||||
$access_token = $integrationsRepository->apiloGetAccessToken();
|
||||
$url = 'https://projectpro.apilo.com/rest/api/orders/' . $order['apilo_order_id'] . '/';
|
||||
|
||||
$ch = curl_init( $url );
|
||||
@@ -565,70 +653,103 @@ if ( $apilo_settings['enabled'] and $apilo_settings['sync_orders'] and $apilo_se
|
||||
$http_code_poll = (int)curl_getinfo( $ch, CURLINFO_HTTP_CODE );
|
||||
$responseData = json_decode( $response, true );
|
||||
|
||||
if ( $responseData['id'] and $responseData['status'] )
|
||||
if ( isset($responseData['id']) && $responseData['id'] && isset($responseData['status']) && $responseData['status'] )
|
||||
{
|
||||
$shop_status_id = ( new \Domain\ShopStatus\ShopStatusRepository( $mdb ) )->getByIntegrationStatusId( 'apilo', (int)$responseData['status'] );
|
||||
|
||||
if ( $shop_status_id )
|
||||
$orderAdminService->changeStatus( (int)$order['id'], $shop_status_id, false );
|
||||
|
||||
\Domain\Integrations\ApiloLogger::log( $mdb, 'status_poll', (int)$order['id'], 'Status pobrany z Apilo (apilo_status: ' . $responseData['status'] . ', shop_status: ' . ($shop_status_id ?: 'brak mapowania') . ')', [ 'apilo_order_id' => $order['apilo_order_id'], 'http_code' => $http_code_poll, 'response' => $responseData ] );
|
||||
\Domain\Integrations\ApiloLogger::log( $mdb, 'status_poll', (int)$order['id'], 'Status pobrany z Apilo (apilo_status: ' . $responseData['status'] . ', shop_status: ' . ($shop_status_id ? $shop_status_id : 'brak mapowania') . ')', [ 'apilo_order_id' => $order['apilo_order_id'], 'http_code' => $http_code_poll, 'response' => $responseData ] );
|
||||
|
||||
$orderRepo->updateApiloStatusDate( (int)$order['id'], date( 'Y-m-d H:i:s' ) );
|
||||
echo '<p>Zaktualizowałem status zamówienia <b>' . $order['number'] . '</b></p>';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
/* zapisywanie historii cen produktów */
|
||||
$results = $mdb -> select( 'pp_shop_products', [ 'id', 'price_brutto', 'price_brutto_promo' ], [ 'OR' => [ 'price_history_date[!]' => date( 'Y-m-d' ), 'price_history_date' => null ], 'ORDER' => [ 'price_history_date' => 'ASC' ], 'LIMIT' => 100 ] );
|
||||
foreach ( $results as $row )
|
||||
{
|
||||
if ( $price )
|
||||
// 8. Price history
|
||||
$processor->registerHandler( \Domain\CronJob\CronJobType::PRICE_HISTORY, function($payload) use ($mdb) {
|
||||
$results = $mdb->select( 'pp_shop_products', [ 'id', 'price_brutto', 'price_brutto_promo' ], [ 'OR' => [ 'price_history_date[!]' => date( 'Y-m-d' ), 'price_history_date' => null ], 'ORDER' => [ 'price_history_date' => 'ASC' ], 'LIMIT' => 100 ] );
|
||||
|
||||
foreach ( $results as $row )
|
||||
{
|
||||
$mdb -> insert( 'pp_shop_product_price_history', [
|
||||
'id_product' => $row['id'],
|
||||
'price' => $row['price_brutto_promo'] > 0 ? $row['price_brutto_promo'] : $row['price_brutto'],
|
||||
'date' => date( 'Y-m-d' )
|
||||
] );
|
||||
$price = $row['price_brutto_promo'] > 0 ? $row['price_brutto_promo'] : $row['price_brutto'];
|
||||
if ( $price )
|
||||
{
|
||||
$mdb->insert( 'pp_shop_product_price_history', [
|
||||
'id_product' => $row['id'],
|
||||
'price' => $price,
|
||||
'date' => date( 'Y-m-d' )
|
||||
] );
|
||||
}
|
||||
|
||||
$mdb->update( 'pp_shop_products', [ 'price_history_date' => date( 'Y-m-d' ) ], [ 'id' => $row['id'] ] );
|
||||
$mdb->delete( 'pp_shop_product_price_history', [ 'date[<=]' => date( 'Y-m-d', strtotime( '-31 days', time() ) ) ] );
|
||||
echo '<p>Zapisuję historyczną cenę dla produktu <b>#' . $row['id'] . '</b></p>';
|
||||
}
|
||||
|
||||
$mdb -> update( 'pp_shop_products', [ 'price_history_date' => date( 'Y-m-d' ) ], [ 'id' => $row['id'] ] );
|
||||
return true;
|
||||
});
|
||||
|
||||
$mdb -> delete( 'pp_shop_product_price_history', [ 'date[<=]' => date( 'Y-m-d', strtotime( '-31 days', time() ) ) ] );
|
||||
echo '<p>Zapisuję historyczną cenę dla produktu <b>#' . $row['id'] . '</b></p>';
|
||||
}
|
||||
|
||||
/* parsowanie zamówień m.in. pod kątem najczęściej sprzedawanych razem produktów */
|
||||
$orders = $mdb -> select( 'pp_shop_orders', 'id', [ 'parsed' => 0, 'LIMIT' => 1 ] );
|
||||
foreach ( $orders as $order )
|
||||
{
|
||||
$products = $mdb -> select( 'pp_shop_order_products', 'product_id', [ 'order_id' => $order ] );
|
||||
foreach ( $products as $product1 )
|
||||
// 9. Order analysis
|
||||
$processor->registerHandler( \Domain\CronJob\CronJobType::ORDER_ANALYSIS, function($payload) use ($mdb) {
|
||||
$orders = $mdb->select( 'pp_shop_orders', 'id', [ 'parsed' => 0, 'LIMIT' => 1 ] );
|
||||
foreach ( $orders as $order )
|
||||
{
|
||||
if ( $parent_id = $mdb -> get( 'pp_shop_products', 'parent_id', [ 'id' => $product1 ] ) )
|
||||
$product1 = $parent_id;
|
||||
|
||||
foreach ( $products as $product2 )
|
||||
$products = $mdb->select( 'pp_shop_order_products', 'product_id', [ 'order_id' => $order ] );
|
||||
foreach ( $products as $product1 )
|
||||
{
|
||||
if ( $parent_id = $mdb -> get( 'pp_shop_products', 'parent_id', [ 'id' => $product2 ] ) )
|
||||
$product2 = $parent_id;
|
||||
if ( $parent_id = $mdb->get( 'pp_shop_products', 'parent_id', [ 'id' => $product1 ] ) )
|
||||
$product1 = $parent_id;
|
||||
|
||||
if ( $product1 != $product2 )
|
||||
foreach ( $products as $product2 )
|
||||
{
|
||||
$intersection_id = $mdb -> query( 'SELECT * FROM pp_shop_orders_products_intersection WHERE product_1_id = :product_1_id AND product_2_id = :product_2_id OR product_1_id = :product_2_id AND product_2_id = :product_1_id', [ 'product_1_id' => (int)$product1, 'product_2_id' => (int)$product2 ] ) -> fetch( \PDO::FETCH_ASSOC );
|
||||
if ( $intersection_id )
|
||||
if ( $parent_id = $mdb->get( 'pp_shop_products', 'parent_id', [ 'id' => $product2 ] ) )
|
||||
$product2 = $parent_id;
|
||||
|
||||
if ( $product1 != $product2 )
|
||||
{
|
||||
$mdb -> update( 'pp_shop_orders_products_intersection', [ 'count' => $intersection_id['count'] + 1 ], [ 'id' => $intersection_id['id'] ] );
|
||||
}
|
||||
else
|
||||
{
|
||||
$mdb -> insert( 'pp_shop_orders_products_intersection', [ 'product_1_id' => (int)$product1, 'product_2_id' => (int)$product2, 'count' => 1 ] );
|
||||
$stmt = $mdb->query( 'SELECT * FROM pp_shop_orders_products_intersection WHERE product_1_id = :product_1_id AND product_2_id = :product_2_id OR product_1_id = :product_2_id AND product_2_id = :product_1_id', [ 'product_1_id' => (int)$product1, 'product_2_id' => (int)$product2 ] );
|
||||
$intersection_id = $stmt ? $stmt->fetch( \PDO::FETCH_ASSOC ) : null;
|
||||
if ( $intersection_id )
|
||||
{
|
||||
$mdb->update( 'pp_shop_orders_products_intersection', [ 'count' => $intersection_id['count'] + 1 ], [ 'id' => $intersection_id['id'] ] );
|
||||
}
|
||||
else
|
||||
{
|
||||
$mdb->insert( 'pp_shop_orders_products_intersection', [ 'product_1_id' => (int)$product1, 'product_2_id' => (int)$product2, 'count' => 1 ] );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$mdb->update( 'pp_shop_orders', [ 'parsed' => 1 ], [ 'id' => $order ] );
|
||||
echo '<p>Parsuję zamówienie <b>#' . $order . '</b></p>';
|
||||
}
|
||||
$mdb -> update( 'pp_shop_orders', [ 'parsed' => 1 ], [ 'id' => $order ] );
|
||||
echo '<p>Parsuję zamówienie <b>#' . $order . '</b></p>';
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
// 10. Google XML feed
|
||||
$processor->registerHandler( \Domain\CronJob\CronJobType::GOOGLE_XML_FEED, function($payload) use ($mdb) {
|
||||
( new \Domain\Product\ProductRepository( $mdb ) )->generateGoogleFeedXml();
|
||||
echo '<p>Wygenerowano Google XML Feed</p>';
|
||||
return true;
|
||||
});
|
||||
|
||||
// 11. TrustMate invitation — handled by separate cron-turstmate.php (requires browser context)
|
||||
$processor->registerHandler( \Domain\CronJob\CronJobType::TRUSTMATE_INVITATION, function($payload) use ($config) {
|
||||
if ( !isset($config['trustmate']['enabled']) || !$config['trustmate']['enabled'] ) return true;
|
||||
// TrustMate requires browser context (JavaScript). Handled by cron-turstmate.php.
|
||||
return true;
|
||||
});
|
||||
|
||||
// =========================================================================
|
||||
// Run processor
|
||||
// =========================================================================
|
||||
|
||||
$result = $processor->run( 20 );
|
||||
|
||||
echo '<hr>';
|
||||
echo '<p><small>CronJob stats: scheduled=' . $result['scheduled'] . ', processed=' . $result['processed'] . ', succeeded=' . $result['succeeded'] . ', failed=' . $result['failed'] . ', skipped=' . $result['skipped'] . '</small></p>';
|
||||
|
||||
Reference in New Issue
Block a user