feat: Add CronJob functionality and integrate with existing services
- Implemented CronJobProcessor for managing scheduled jobs and processing job queues. - Created CronJobRepository for database interactions related to cron jobs. - Defined CronJobType for job types, statuses, and backoff calculations. - Added ApiloLogger for logging actions related to API interactions. - Enhanced UpdateController to check for updates and display update logs. - Updated FormAction to include a preview action for forms. - Modified ApiRouter to handle new dependencies for OrderAdminService and ProductsApiController. - Extended DictionariesApiController to manage attributes and producers. - Enhanced ProductsApiController with variant management and image upload functionality. - Updated ShopBasketController and ShopProductController to sort attributes and handle custom fields. - Added configuration for cron jobs in config.php. - Initialized apilo-sync-queue.json for managing sync tasks.
This commit is contained in:
30
autoload/Domain/Integrations/ApiloLogger.php
Normal file
30
autoload/Domain/Integrations/ApiloLogger.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
namespace Domain\Integrations;
|
||||
|
||||
class ApiloLogger
|
||||
{
|
||||
/**
|
||||
* @param \medoo $db
|
||||
* @param string $action np. 'send_order', 'payment_sync', 'status_sync', 'status_poll'
|
||||
* @param int|null $orderId
|
||||
* @param string $message
|
||||
* @param mixed $context dane do zapisania jako JSON (request/response)
|
||||
*/
|
||||
public static function log($db, string $action, ?int $orderId, string $message, $context = null): void
|
||||
{
|
||||
$contextJson = null;
|
||||
if ($context !== null) {
|
||||
$contextJson = is_string($context)
|
||||
? $context
|
||||
: json_encode($context, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
|
||||
}
|
||||
|
||||
$db->insert('pp_log', [
|
||||
'action' => $action,
|
||||
'order_id' => $orderId,
|
||||
'message' => $message,
|
||||
'context' => $contextJson,
|
||||
'date' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -56,6 +56,63 @@ class IntegrationsRepository
|
||||
return true;
|
||||
}
|
||||
|
||||
// ── Logs ────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Pobiera logi z tabeli pp_log z paginacją, sortowaniem i filtrowaniem.
|
||||
*
|
||||
* @return array{items:array, total:int}
|
||||
*/
|
||||
public function getLogs( array $filters, string $sortColumn, string $sortDir, int $page, int $perPage ): array
|
||||
{
|
||||
$where = [];
|
||||
|
||||
if ( !empty( $filters['log_action'] ) ) {
|
||||
$where['action[~]'] = '%' . $filters['log_action'] . '%';
|
||||
}
|
||||
|
||||
if ( !empty( $filters['message'] ) ) {
|
||||
$where['message[~]'] = '%' . $filters['message'] . '%';
|
||||
}
|
||||
|
||||
if ( !empty( $filters['order_id'] ) ) {
|
||||
$where['order_id'] = (int) $filters['order_id'];
|
||||
}
|
||||
|
||||
$total = $this->db->count( 'pp_log', $where );
|
||||
|
||||
$where['ORDER'] = [ $sortColumn => $sortDir ];
|
||||
$where['LIMIT'] = [ ( $page - 1 ) * $perPage, $perPage ];
|
||||
|
||||
$items = $this->db->select( 'pp_log', '*', $where );
|
||||
if ( !is_array( $items ) ) {
|
||||
$items = [];
|
||||
}
|
||||
|
||||
return [
|
||||
'items' => $items,
|
||||
'total' => (int) $total,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Usuwa wpis logu po ID.
|
||||
*/
|
||||
public function deleteLog( int $id ): bool
|
||||
{
|
||||
$this->db->delete( 'pp_log', [ 'id' => $id ] );
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Czyści wszystkie logi z tabeli pp_log.
|
||||
*/
|
||||
public function clearLogs(): bool
|
||||
{
|
||||
$this->db->delete( 'pp_log', [] );
|
||||
return true;
|
||||
}
|
||||
|
||||
// ── Product linking (Apilo) ─────────────────────────────────
|
||||
|
||||
public function linkProduct( int $productId, $externalId, $externalName ): bool
|
||||
@@ -611,15 +668,12 @@ class IntegrationsRepository
|
||||
public function shopproImportProduct( int $productId ): array
|
||||
{
|
||||
$settings = $this->getSettings( 'shoppro' );
|
||||
$missingSetting = $this->missingShopproSetting( $settings, [ 'domain', 'db_name', 'db_host', 'db_user' ] );
|
||||
if ( $missingSetting !== null ) {
|
||||
return [ 'success' => false, 'message' => 'Brakuje konfiguracji shopPRO: ' . $missingSetting . '.' ];
|
||||
}
|
||||
|
||||
$mdb2 = new \medoo( [
|
||||
'database_type' => 'mysql',
|
||||
'database_name' => $settings['db_name'],
|
||||
'server' => $settings['db_host'],
|
||||
'username' => $settings['db_user'],
|
||||
'password' => $settings['db_password'],
|
||||
'charset' => 'utf8'
|
||||
] );
|
||||
$mdb2 = $this->shopproDb( $settings );
|
||||
|
||||
$product = $mdb2->get( 'pp_shop_products', '*', [ 'id' => $productId ] );
|
||||
if ( !$product )
|
||||
@@ -643,6 +697,7 @@ class IntegrationsRepository
|
||||
'additional_message_text' => $product['additional_message_text'],
|
||||
'additional_message_required'=> $product['additional_message_required'],
|
||||
'weight' => $product['weight'],
|
||||
'producer_id' => $product['producer_id'] ?? null,
|
||||
] );
|
||||
|
||||
$newProductId = $this->db->id();
|
||||
@@ -672,41 +727,149 @@ class IntegrationsRepository
|
||||
'warehouse_message_nonzero'=> $lang['warehouse_message_nonzero'],
|
||||
'canonical' => $lang['canonical'],
|
||||
'xml_name' => $lang['xml_name'],
|
||||
'security_information' => $lang['security_information'] ?? null,
|
||||
] );
|
||||
}
|
||||
}
|
||||
|
||||
// Import custom fields
|
||||
$customFields = $mdb2->select( 'pp_shop_products_custom_fields', '*', [ 'id_product' => $productId ] );
|
||||
if ( is_array( $customFields ) ) {
|
||||
foreach ( $customFields as $field ) {
|
||||
$this->db->insert( 'pp_shop_products_custom_fields', [
|
||||
'id_product' => $newProductId,
|
||||
'name' => (string)($field['name'] ?? ''),
|
||||
'type' => (string)($field['type'] ?? 'text'),
|
||||
'is_required' => !empty( $field['is_required'] ) ? 1 : 0,
|
||||
] );
|
||||
}
|
||||
}
|
||||
|
||||
// Import images
|
||||
$images = $mdb2->select( 'pp_shop_products_images', '*', [ 'product_id' => $productId ] );
|
||||
$importLog = [];
|
||||
$domainRaw = preg_replace( '#^https?://#', '', (string)($settings['domain'] ?? '') );
|
||||
if ( is_array( $images ) ) {
|
||||
foreach ( $images as $image ) {
|
||||
$imageUrl = 'https://' . $settings['domain'] . $image['src'];
|
||||
$srcPath = (string)($image['src'] ?? '');
|
||||
$imageUrl = 'https://' . rtrim( $domainRaw, '/' ) . '/' . ltrim( $srcPath, '/' );
|
||||
$imageName = basename( $srcPath );
|
||||
|
||||
if ( $imageName === '' ) {
|
||||
$importLog[] = '[SKIP] Pusta nazwa pliku dla src: ' . $srcPath;
|
||||
continue;
|
||||
}
|
||||
|
||||
$ch = curl_init( $imageUrl );
|
||||
curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true );
|
||||
curl_setopt( $ch, CURLOPT_FOLLOWLOCATION, true );
|
||||
curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, false );
|
||||
curl_setopt( $ch, CURLOPT_SSL_VERIFYHOST, false );
|
||||
$imageData = curl_exec( $ch );
|
||||
curl_setopt( $ch, CURLOPT_TIMEOUT, 30 );
|
||||
curl_setopt( $ch, CURLOPT_CONNECTTIMEOUT, 10 );
|
||||
$imageData = curl_exec( $ch );
|
||||
$httpCode = (int)curl_getinfo( $ch, CURLINFO_HTTP_CODE );
|
||||
$curlErrno = curl_errno( $ch );
|
||||
$curlError = curl_error( $ch );
|
||||
curl_close( $ch );
|
||||
|
||||
$imageName = basename( $imageUrl );
|
||||
$imageDir = '../upload/product_images/product_' . $newProductId;
|
||||
if ( $curlErrno !== 0 || $imageData === false ) {
|
||||
$importLog[] = '[ERROR] cURL: ' . $imageUrl . ' — błąd ' . $curlErrno . ': ' . $curlError;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( $httpCode !== 200 ) {
|
||||
$importLog[] = '[ERROR] HTTP ' . $httpCode . ': ' . $imageUrl;
|
||||
continue;
|
||||
}
|
||||
|
||||
$imageDir = dirname( __DIR__, 3 ) . '/upload/product_images/product_' . $newProductId;
|
||||
$imagePath = $imageDir . '/' . $imageName;
|
||||
|
||||
if ( !file_exists( $imageDir ) )
|
||||
mkdir( $imageDir, 0777, true );
|
||||
if ( !file_exists( $imageDir ) && !mkdir( $imageDir, 0777, true ) && !file_exists( $imageDir ) ) {
|
||||
$importLog[] = '[ERROR] Nie można utworzyć katalogu: ' . $imageDir;
|
||||
continue;
|
||||
}
|
||||
|
||||
file_put_contents( $imagePath, $imageData );
|
||||
$written = file_put_contents( $imagePath, $imageData );
|
||||
if ( $written === false ) {
|
||||
$importLog[] = '[ERROR] Zapis pliku nieudany: ' . $imagePath;
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->db->insert( 'pp_shop_products_images', [
|
||||
'product_id' => $newProductId,
|
||||
'src' => '/upload/product_images/product_' . $newProductId . '/' . $imageName,
|
||||
'alt' => $image['alt'] ?? '',
|
||||
'o' => $image['o'],
|
||||
] );
|
||||
$importLog[] = '[OK] ' . $imageUrl . ' → ' . $imagePath . ' (' . $written . ' B)';
|
||||
}
|
||||
}
|
||||
|
||||
return [ 'success' => true, 'message' => 'Produkt został zaimportowany.' ];
|
||||
// Zapisz log importu zdjęć (ścieżka absolutna — niezależna od cwd)
|
||||
$logDir = dirname( __DIR__, 3 ) . '/logs';
|
||||
$logFile = $logDir . '/shoppro-import-debug.log';
|
||||
$mkdirOk = file_exists( $logDir ) || mkdir( $logDir, 0755, true ) || file_exists( $logDir );
|
||||
$logEntry = '[' . date( 'Y-m-d H:i:s' ) . '] Import produktu #' . $productId . ' → #' . $newProductId . "\n"
|
||||
. ' Domain: ' . $domainRaw . "\n"
|
||||
. ' Obrazy źródłowe: ' . count( $images ?: [] ) . "\n";
|
||||
foreach ( $importLog as $line ) {
|
||||
$logEntry .= ' ' . $line . "\n";
|
||||
}
|
||||
// Zawsze loguj do error_log (niezależnie od uprawnień do pliku)
|
||||
error_log( '[shopPRO shoppro-import] ' . str_replace( "\n", ' | ', $logEntry ) );
|
||||
|
||||
if ( $mkdirOk && file_put_contents( $logFile, $logEntry, FILE_APPEND ) === false ) {
|
||||
error_log( '[shopPRO shoppro-import] WARN: nie można zapisać logu do: ' . $logFile );
|
||||
} elseif ( !$mkdirOk ) {
|
||||
error_log( '[shopPRO shoppro-import] WARN: nie można utworzyć katalogu: ' . $logDir );
|
||||
}
|
||||
|
||||
// Zbuduj czytelny komunikat z wynikiem importu zdjęć
|
||||
$imgCount = count( $images ?: [] );
|
||||
if ( $imgCount === 0 ) {
|
||||
$imgSummary = 'Zdjęcia: brak w bazie źródłowej.';
|
||||
} else {
|
||||
$ok = 0;
|
||||
$errors = [];
|
||||
foreach ( $importLog as $line ) {
|
||||
if ( strncmp( $line, '[OK]', 4 ) === 0 ) {
|
||||
$ok++;
|
||||
} else {
|
||||
$errors[] = $line;
|
||||
}
|
||||
}
|
||||
$imgSummary = 'Zdjęcia: ' . $ok . '/' . $imgCount . ' zaimportowanych.';
|
||||
if ( !empty( $errors ) ) {
|
||||
$imgSummary .= ' Błędy: ' . implode( '; ', $errors );
|
||||
}
|
||||
}
|
||||
|
||||
return [ 'success' => true, 'message' => 'Produkt został zaimportowany. ' . $imgSummary ];
|
||||
}
|
||||
|
||||
private function missingShopproSetting( array $settings, array $requiredKeys ): ?string
|
||||
{
|
||||
foreach ( $requiredKeys as $requiredKey ) {
|
||||
if ( trim( (string)($settings[$requiredKey] ?? '') ) === '' ) {
|
||||
return $requiredKey;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function shopproDb( array $settings ): \medoo
|
||||
{
|
||||
return new \medoo( [
|
||||
'database_type' => 'mysql',
|
||||
'database_name' => $settings['db_name'],
|
||||
'server' => $settings['db_host'],
|
||||
'username' => $settings['db_user'],
|
||||
'password' => $settings['db_password'] ?? '',
|
||||
'charset' => 'utf8'
|
||||
] );
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user