Files
orderPRO/src/Modules/Settings/ShopproIntegrationsController.php
Jacek Pyziak 2b12fde248 feat(shipments): add ShipmentProviderInterface and ShipmentProviderRegistry
- Introduced ShipmentProviderInterface to define the contract for shipment providers.
- Implemented ShipmentProviderRegistry to manage and retrieve shipment providers.
- Added a new tool for probing Apaczka order_send payload variants, enhancing debugging capabilities.
2026-03-08 23:45:10 +01:00

902 lines
36 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Modules\Settings;
use App\Core\Http\Request;
use App\Core\Http\Response;
use App\Core\I18n\Translator;
use App\Core\Security\Csrf;
use App\Core\Support\Flash;
use App\Core\View\Template;
use App\Modules\Auth\AuthService;
use App\Modules\Cron\CronRepository;
use DateInterval;
use DateTimeImmutable;
use RuntimeException;
use Throwable;
final class ShopproIntegrationsController
{
private const ORDERS_IMPORT_JOB_TYPE = 'shoppro_orders_import';
private const ORDERS_IMPORT_DEFAULT_INTERVAL_SECONDS = 300;
private const ORDERS_IMPORT_DEFAULT_PRIORITY = 90;
private const ORDERS_IMPORT_DEFAULT_MAX_ATTEMPTS = 3;
private const ORDER_STATUS_SYNC_JOB_TYPE = 'shoppro_order_status_sync';
private const ORDER_STATUS_SYNC_DEFAULT_INTERVAL_SECONDS = 900;
private const ORDER_STATUS_SYNC_DEFAULT_PRIORITY = 100;
private const ORDER_STATUS_SYNC_DEFAULT_MAX_ATTEMPTS = 3;
private const PAYMENT_SYNC_JOB_TYPE = 'shoppro_payment_status_sync';
private const PAYMENT_SYNC_DEFAULT_INTERVAL_SECONDS = 600;
private const PAYMENT_SYNC_DEFAULT_PRIORITY = 105;
private const PAYMENT_SYNC_DEFAULT_MAX_ATTEMPTS = 3;
public function __construct(
private readonly Template $template,
private readonly Translator $translator,
private readonly AuthService $auth,
private readonly ShopproIntegrationsRepository $repository,
private readonly ShopproStatusMappingRepository $statusMappings,
private readonly OrderStatusRepository $orderStatuses,
private readonly CronRepository $cronRepository,
private readonly CarrierDeliveryMethodMappingRepository $deliveryMappings,
private readonly AllegroIntegrationRepository $allegroIntegrationRepository,
private readonly AllegroOAuthClient $allegroOAuthClient,
private readonly AllegroApiClient $allegroApiClient,
private readonly ?ApaczkaIntegrationRepository $apaczkaRepository = null,
private readonly ?ApaczkaApiClient $apaczkaApiClient = null
) {
}
public function index(Request $request): Response
{
$integrations = $this->repository->listIntegrations();
$forceNewMode = trim((string) $request->input('new', '')) === '1';
$selectedId = max(0, (int) $request->input('id', 0));
$selectedIntegration = $selectedId > 0 ? $this->repository->findIntegration($selectedId) : null;
if (!$forceNewMode && $selectedIntegration === null && $integrations !== []) {
$firstId = (int) ($integrations[0]['id'] ?? 0);
if ($firstId > 0) {
$selectedIntegration = $this->repository->findIntegration($firstId);
}
}
$this->ensureImportScheduleExists();
$this->ensureStatusSyncScheduleExists();
$this->ensurePaymentSyncScheduleExists();
$activeTab = $this->resolveTab((string) $request->input('tab', 'integration'));
$discoveredStatuses = $this->readDiscoveredStatuses();
$statusRows = $selectedIntegration !== null
? $this->buildStatusRows((int) ($selectedIntegration['id'] ?? 0), $discoveredStatuses)
: [];
$deliveryServicesData = $activeTab === 'delivery'
? $this->loadDeliveryServices()
: [[], [], ''];
$deliveryMappings = $selectedIntegration !== null
? $this->deliveryMappings->listMappings('shoppro', (int) ($selectedIntegration['id'] ?? 0))
: [];
$orderDeliveryMethods = $selectedIntegration !== null
? $this->deliveryMappings->getDistinctOrderDeliveryMethods('shoppro', (int) ($selectedIntegration['id'] ?? 0))
: [];
$html = $this->template->render('settings/shoppro', [
'title' => $this->translator->get('settings.integrations.title'),
'activeMenu' => 'settings',
'activeSettings' => 'shoppro',
'user' => $this->auth->user(),
'csrfToken' => Csrf::token(),
'activeTab' => $activeTab,
'rows' => $integrations,
'selectedIntegration' => $selectedIntegration,
'form' => $this->buildFormValues($selectedIntegration),
'ordersImportIntervalMinutes' => $this->currentImportIntervalMinutes(),
'statusSyncIntervalMinutes' => $this->currentStatusSyncIntervalMinutes(),
'paymentSyncIntervalMinutes' => $this->currentPaymentSyncIntervalMinutes(),
'statusRows' => $statusRows,
'orderproStatuses' => $this->orderStatuses->listStatuses(),
'deliveryMappings' => $deliveryMappings,
'orderDeliveryMethods' => $orderDeliveryMethods,
'allegroDeliveryServices' => $deliveryServicesData[0],
'apaczkaDeliveryServices' => $deliveryServicesData[1],
'allegroDeliveryServicesError' => $deliveryServicesData[2],
'inpostDeliveryServices' => array_values(array_filter(
$deliveryServicesData[0],
static fn (array $svc): bool => stripos((string) ($svc['carrierId'] ?? ''), 'inpost') !== false
)),
'errorMessage' => (string) Flash::get('settings_error', ''),
'successMessage' => (string) Flash::get('settings_success', ''),
], 'layouts/app');
return Response::html($html);
}
public function save(Request $request): Response
{
$integrationId = max(0, (int) $request->input('integration_id', 0));
$tab = $this->resolveTab((string) $request->input('tab', 'integration'));
$redirectBase = '/settings/integrations/shoppro';
$redirectTo = $this->buildRedirectUrl($integrationId, $tab);
if (!Csrf::validate((string) $request->input('_token', ''))) {
Flash::set('settings_error', $this->translator->get('auth.errors.csrf_expired'));
return Response::redirect($redirectTo);
}
$existing = $integrationId > 0 ? $this->repository->findIntegration($integrationId) : null;
if ($integrationId > 0 && $existing === null) {
Flash::set('settings_error', $this->translator->get('settings.integrations.flash.not_found'));
return Response::redirect($this->buildRedirectUrl(0, $tab));
}
$name = trim((string) $request->input('name', ''));
if (mb_strlen($name) < 2) {
Flash::set('settings_error', $this->translator->get('settings.integrations.validation.name_min'));
return Response::redirect($redirectTo);
}
$baseUrl = rtrim(trim((string) $request->input('base_url', '')), '/');
if (!$this->isValidHttpUrl($baseUrl)) {
Flash::set('settings_error', $this->translator->get('settings.integrations.validation.base_url_invalid'));
return Response::redirect($redirectTo);
}
$apiKey = trim((string) $request->input('api_key', ''));
$hasExistingApiKey = (bool) ($existing['has_api_key'] ?? false);
if ($tab === 'integration' && $apiKey === '' && !$hasExistingApiKey) {
Flash::set('settings_error', $this->translator->get('settings.integrations.validation.api_key_required'));
return Response::redirect($redirectTo);
}
$ordersFetchStartDate = trim((string) $request->input('orders_fetch_start_date', ''));
if ($ordersFetchStartDate !== '' && !$this->isValidYmdDate($ordersFetchStartDate)) {
Flash::set('settings_error', $this->translator->get('settings.integrations.validation.orders_fetch_start_date_invalid'));
return Response::redirect($redirectTo);
}
if ($this->isDuplicateName($integrationId, $name)) {
Flash::set('settings_error', $this->translator->get('settings.integrations.validation.name_taken'));
return Response::redirect($redirectTo);
}
try {
$statusSyncDirectionInput = $request->input('order_status_sync_direction', null);
$statusSyncDirection = $statusSyncDirectionInput !== null
? trim((string) $statusSyncDirectionInput)
: (string) ($existing['order_status_sync_direction'] ?? 'shoppro_to_orderpro');
$paymentSyncStatusCodesInput = $request->input('payment_sync_status_codes', null);
if (is_array($paymentSyncStatusCodesInput)) {
$paymentSyncStatusCodes = $paymentSyncStatusCodesInput;
} elseif ($tab === 'settings') {
$paymentSyncStatusCodes = [];
} else {
$paymentSyncStatusCodes = $existing['payment_sync_status_codes'] ?? [];
}
$savedId = $this->repository->saveIntegration([
'integration_id' => $integrationId,
'name' => $name,
'base_url' => $baseUrl,
'api_key' => $apiKey,
'timeout_seconds' => max(1, min(120, (int) $request->input('timeout_seconds', 10))),
'is_active' => $request->input('is_active', ''),
'orders_fetch_enabled' => $request->input('orders_fetch_enabled', ''),
'orders_fetch_start_date' => $ordersFetchStartDate,
'order_status_sync_direction' => $statusSyncDirection,
'payment_sync_status_codes' => $paymentSyncStatusCodes,
]);
$this->saveImportIntervalIfRequested($request);
$this->saveStatusSyncIntervalIfRequested($request);
$this->savePaymentSyncIntervalIfRequested($request);
$flashKey = $integrationId > 0
? 'settings.integrations.flash.updated'
: 'settings.integrations.flash.created';
Flash::set('settings_success', $this->translator->get($flashKey));
return Response::redirect($this->buildRedirectUrl($savedId, $tab));
} catch (Throwable) {
Flash::set('settings_error', $this->translator->get('settings.integrations.flash.failed'));
return Response::redirect($redirectTo);
}
}
public function test(Request $request): Response
{
$integrationId = max(0, (int) $request->input('integration_id', 0));
$tab = $this->resolveTab((string) $request->input('tab', 'integration'));
$redirectBase = '/settings/integrations/shoppro';
$redirectTo = $this->buildRedirectUrl($integrationId, $tab);
if (!Csrf::validate((string) $request->input('_token', ''))) {
Flash::set('settings_error', $this->translator->get('auth.errors.csrf_expired'));
return Response::redirect($redirectTo);
}
if ($integrationId <= 0) {
Flash::set('settings_error', $this->translator->get('settings.integrations.flash.not_found'));
return Response::redirect($this->buildRedirectUrl(0, $tab));
}
$result = $this->repository->testConnection($integrationId);
$isOk = (string) ($result['status'] ?? 'error') === 'ok';
$message = trim((string) ($result['message'] ?? ''));
$httpCode = $result['http_code'] ?? null;
if ($isOk) {
Flash::set('settings_success', $this->translator->get('settings.integrations.flash.test_ok'));
} else {
$suffix = $message !== '' ? ' ' . $message : '';
$httpPart = $httpCode !== null ? ' (HTTP ' . (string) $httpCode . ')' : '';
Flash::set(
'settings_error',
$this->translator->get('settings.integrations.flash.test_failed') . $httpPart . $suffix
);
}
return Response::redirect($redirectTo);
}
public function saveStatusMappings(Request $request): Response
{
$integrationId = max(0, (int) $request->input('integration_id', 0));
$redirectTo = $this->buildRedirectUrl($integrationId, 'statuses');
if (!Csrf::validate((string) $request->input('_token', ''))) {
Flash::set('settings_error', $this->translator->get('auth.errors.csrf_expired'));
return Response::redirect($redirectTo);
}
if ($integrationId <= 0 || $this->repository->findIntegration($integrationId) === null) {
Flash::set('settings_error', $this->translator->get('settings.integrations.flash.not_found'));
return Response::redirect($this->buildRedirectUrl(0, 'statuses'));
}
$shopCodes = $request->input('shoppro_status_code', []);
$shopNames = $request->input('shoppro_status_name', []);
$orderCodes = $request->input('orderpro_status_code', []);
if (!is_array($shopCodes) || !is_array($shopNames) || !is_array($orderCodes)) {
Flash::set('settings_error', $this->translator->get('settings.integrations.statuses.flash.invalid_payload'));
return Response::redirect($redirectTo);
}
$allowedOrderpro = $this->resolveAllowedOrderproStatusCodes();
$rowsCount = min(count($shopCodes), count($shopNames), count($orderCodes));
$mappings = [];
for ($index = 0; $index < $rowsCount; $index++) {
$shopCode = trim((string) ($shopCodes[$index] ?? ''));
$shopName = trim((string) ($shopNames[$index] ?? ''));
$orderCode = strtolower(trim((string) ($orderCodes[$index] ?? '')));
if ($shopCode === '') {
continue;
}
if ($orderCode === '') {
continue;
}
if (!isset($allowedOrderpro[$orderCode])) {
continue;
}
$mappings[] = [
'shoppro_status_code' => $shopCode,
'shoppro_status_name' => $shopName,
'orderpro_status_code' => $orderCode,
];
}
try {
$this->statusMappings->replaceForIntegration($integrationId, $mappings);
Flash::set('settings_success', $this->translator->get('settings.integrations.statuses.flash.saved'));
} catch (Throwable $exception) {
Flash::set(
'settings_error',
$this->translator->get('settings.integrations.statuses.flash.save_failed') . ' ' . $exception->getMessage()
);
}
return Response::redirect($redirectTo);
}
public function syncStatuses(Request $request): Response
{
$integrationId = max(0, (int) $request->input('integration_id', 0));
$redirectTo = $this->buildRedirectUrl($integrationId, 'statuses');
if (!Csrf::validate((string) $request->input('_token', ''))) {
Flash::set('settings_error', $this->translator->get('auth.errors.csrf_expired'));
return Response::redirect($redirectTo);
}
if ($integrationId <= 0 || $this->repository->findIntegration($integrationId) === null) {
Flash::set('settings_error', $this->translator->get('settings.integrations.flash.not_found'));
return Response::redirect($this->buildRedirectUrl(0, 'statuses'));
}
$result = $this->repository->fetchOrderStatuses($integrationId);
if (($result['ok'] ?? false) !== true) {
$message = trim((string) ($result['message'] ?? ''));
Flash::set(
'settings_error',
$this->translator->get('settings.integrations.statuses.flash.sync_failed') . ($message !== '' ? ' ' . $message : '')
);
return Response::redirect($redirectTo);
}
$statuses = $result['statuses'] ?? [];
Flash::set('shoppro_discovered_statuses', is_array($statuses) ? $statuses : []);
Flash::set(
'settings_success',
$this->translator->get('settings.integrations.statuses.flash.sync_ok', [
'count' => (string) (is_array($statuses) ? count($statuses) : 0),
])
);
return Response::redirect($redirectTo);
}
public function saveDeliveryMappings(Request $request): Response
{
$integrationId = max(0, (int) $request->input('integration_id', 0));
$redirectTo = $this->buildRedirectUrl($integrationId, 'delivery');
if (!Csrf::validate((string) $request->input('_token', ''))) {
Flash::set('settings_error', $this->translator->get('auth.errors.csrf_expired'));
return Response::redirect($redirectTo);
}
if ($integrationId <= 0 || $this->repository->findIntegration($integrationId) === null) {
Flash::set('settings_error', $this->translator->get('settings.integrations.flash.not_found'));
return Response::redirect($this->buildRedirectUrl(0, 'delivery'));
}
$orderMethods = (array) $request->input('order_delivery_method', []);
$carriers = (array) $request->input('carrier', []);
$allegroMethodIds = (array) $request->input('allegro_delivery_method_id', []);
$apaczkaMethodIds = (array) $request->input('apaczka_delivery_method_id', []);
$credentialsIds = (array) $request->input('allegro_credentials_id', []);
$carrierIds = (array) $request->input('allegro_carrier_id', []);
$serviceNames = (array) $request->input('allegro_service_name', []);
$mappings = [];
foreach ($orderMethods as $index => $orderMethod) {
$orderMethodValue = trim((string) $orderMethod);
$carrier = trim((string) ($carriers[$index] ?? 'allegro'));
$provider = $carrier === 'apaczka' ? 'apaczka' : 'allegro_wza';
$providerServiceId = $provider === 'apaczka'
? trim((string) ($apaczkaMethodIds[$index] ?? ''))
: trim((string) ($allegroMethodIds[$index] ?? ''));
if ($orderMethodValue === '' || $providerServiceId === '') {
continue;
}
$mappings[] = [
'order_delivery_method' => $orderMethodValue,
'provider' => $provider,
'provider_service_id' => $providerServiceId,
'provider_account_id' => $provider === 'allegro_wza' ? trim((string) ($credentialsIds[$index] ?? '')) : '',
'provider_carrier_id' => $provider === 'allegro_wza' ? trim((string) ($carrierIds[$index] ?? '')) : '',
'provider_service_name' => trim((string) ($serviceNames[$index] ?? '')),
];
}
try {
$this->deliveryMappings->saveMappings('shoppro', $integrationId, $mappings);
Flash::set('settings_success', $this->translator->get('settings.integrations.delivery.flash.saved'));
} catch (Throwable $exception) {
Flash::set(
'settings_error',
$this->translator->get('settings.integrations.delivery.flash.save_failed') . ' ' . $exception->getMessage()
);
}
return Response::redirect($redirectTo);
}
/**
* @param array<string, mixed>|null $integration
* @return array<string, mixed>
*/
private function buildFormValues(?array $integration): array
{
if ($integration === null) {
return [
'integration_id' => 0,
'name' => '',
'base_url' => '',
'timeout_seconds' => 10,
'is_active' => 1,
'orders_fetch_enabled' => 0,
'orders_fetch_start_date' => '',
'order_status_sync_direction' => 'shoppro_to_orderpro',
'payment_sync_status_codes' => [],
];
}
return [
'integration_id' => (int) ($integration['id'] ?? 0),
'name' => (string) ($integration['name'] ?? ''),
'base_url' => (string) ($integration['base_url'] ?? ''),
'timeout_seconds' => (int) ($integration['timeout_seconds'] ?? 10),
'is_active' => !empty($integration['is_active']) ? 1 : 0,
'orders_fetch_enabled' => !empty($integration['orders_fetch_enabled']) ? 1 : 0,
'orders_fetch_start_date' => (string) ($integration['orders_fetch_start_date'] ?? ''),
'order_status_sync_direction' => (string) ($integration['order_status_sync_direction'] ?? 'shoppro_to_orderpro'),
'payment_sync_status_codes' => is_array($integration['payment_sync_status_codes'] ?? null)
? $integration['payment_sync_status_codes']
: [],
];
}
private function isDuplicateName(int $currentId, string $name): bool
{
$needle = mb_strtolower(trim($name));
if ($needle === '') {
return false;
}
$rows = $this->repository->listIntegrations();
foreach ($rows as $row) {
$rowId = (int) ($row['id'] ?? 0);
if ($rowId === $currentId) {
continue;
}
$rowName = mb_strtolower(trim((string) ($row['name'] ?? '')));
if ($rowName === $needle) {
return true;
}
}
return false;
}
private function isValidHttpUrl(string $value): bool
{
if (filter_var($value, FILTER_VALIDATE_URL) === false) {
return false;
}
$scheme = strtolower((string) parse_url($value, PHP_URL_SCHEME));
return in_array($scheme, ['http', 'https'], true);
}
private function isValidYmdDate(string $value): bool
{
$date = DateTimeImmutable::createFromFormat('Y-m-d', $value);
return $date !== false && $date->format('Y-m-d') === $value;
}
/**
* @return array<string, array{shoppro_status_code:string,shoppro_status_name:string,orderpro_status_code:string}>
*/
private function buildStatusRows(int $integrationId, array $discoveredStatuses): array
{
$mappedRows = $this->statusMappings->listByIntegration($integrationId);
$result = [];
foreach ($mappedRows as $row) {
$code = trim((string) ($row['shoppro_status_code'] ?? ''));
if ($code === '') {
continue;
}
$key = mb_strtolower($code);
$result[$key] = [
'shoppro_status_code' => $code,
'shoppro_status_name' => trim((string) ($row['shoppro_status_name'] ?? '')),
'orderpro_status_code' => strtolower(trim((string) ($row['orderpro_status_code'] ?? ''))),
];
}
foreach ($discoveredStatuses as $status) {
if (!is_array($status)) {
continue;
}
$code = trim((string) ($status['code'] ?? ''));
if ($code === '') {
continue;
}
$key = mb_strtolower($code);
if (!isset($result[$key])) {
$result[$key] = [
'shoppro_status_code' => $code,
'shoppro_status_name' => trim((string) ($status['name'] ?? '')),
'orderpro_status_code' => '',
];
}
}
uasort($result, static function (array $left, array $right): int {
return strcmp(
strtolower((string) ($left['shoppro_status_code'] ?? '')),
strtolower((string) ($right['shoppro_status_code'] ?? ''))
);
});
return array_values($result);
}
/**
* @return array<string, true>
*/
private function resolveAllowedOrderproStatusCodes(): array
{
$allowed = [];
foreach ($this->orderStatuses->listStatuses() as $status) {
if (!is_array($status)) {
continue;
}
$code = strtolower(trim((string) ($status['code'] ?? '')));
if ($code === '') {
continue;
}
$allowed[$code] = true;
}
return $allowed;
}
/**
* @return array<int, array{code:string,name:string}>
*/
private function readDiscoveredStatuses(): array
{
$raw = Flash::get('shoppro_discovered_statuses', []);
if (!is_array($raw)) {
return [];
}
$result = [];
foreach ($raw as $item) {
if (!is_array($item)) {
continue;
}
$code = trim((string) ($item['code'] ?? ''));
if ($code === '') {
continue;
}
$result[] = [
'code' => $code,
'name' => trim((string) ($item['name'] ?? $code)),
];
}
return $result;
}
private function resolveTab(string $candidate): string
{
$value = trim($candidate);
$allowed = ['integration', 'statuses', 'settings', 'delivery'];
if (!in_array($value, $allowed, true)) {
return 'integration';
}
return $value;
}
private function buildRedirectUrl(int $integrationId, string $tab): string
{
$url = '/settings/integrations/shoppro';
$query = [];
if ($integrationId > 0) {
$query['id'] = (string) $integrationId;
}
if ($tab !== 'integration') {
$query['tab'] = $tab;
}
if ($query === []) {
return $url;
}
return $url . '?' . http_build_query($query);
}
private function currentImportIntervalMinutes(): int
{
$schedule = $this->findImportSchedule();
$seconds = (int) ($schedule['interval_seconds'] ?? self::ORDERS_IMPORT_DEFAULT_INTERVAL_SECONDS);
return max(1, min(1440, (int) floor(max(60, $seconds) / 60)));
}
/**
* @return array<string, mixed>
*/
private function findImportSchedule(): array
{
foreach ($this->cronRepository->listSchedules() as $schedule) {
if ((string) ($schedule['job_type'] ?? '') !== self::ORDERS_IMPORT_JOB_TYPE) {
continue;
}
return $schedule;
}
return [];
}
/**
* @return array<string, mixed>
*/
private function findStatusSyncSchedule(): array
{
foreach ($this->cronRepository->listSchedules() as $schedule) {
if ((string) ($schedule['job_type'] ?? '') !== self::ORDER_STATUS_SYNC_JOB_TYPE) {
continue;
}
return $schedule;
}
return [];
}
/**
* @return array<string, mixed>
*/
private function findPaymentSyncSchedule(): array
{
foreach ($this->cronRepository->listSchedules() as $schedule) {
if ((string) ($schedule['job_type'] ?? '') !== self::PAYMENT_SYNC_JOB_TYPE) {
continue;
}
return $schedule;
}
return [];
}
private function ensureImportScheduleExists(): void
{
try {
if ($this->findImportSchedule() !== []) {
return;
}
$this->cronRepository->upsertSchedule(
self::ORDERS_IMPORT_JOB_TYPE,
self::ORDERS_IMPORT_DEFAULT_INTERVAL_SECONDS,
self::ORDERS_IMPORT_DEFAULT_PRIORITY,
self::ORDERS_IMPORT_DEFAULT_MAX_ATTEMPTS,
null,
true
);
} catch (Throwable) {
return;
}
}
private function ensureStatusSyncScheduleExists(): void
{
try {
if ($this->findStatusSyncSchedule() !== []) {
return;
}
$this->cronRepository->upsertSchedule(
self::ORDER_STATUS_SYNC_JOB_TYPE,
self::ORDER_STATUS_SYNC_DEFAULT_INTERVAL_SECONDS,
self::ORDER_STATUS_SYNC_DEFAULT_PRIORITY,
self::ORDER_STATUS_SYNC_DEFAULT_MAX_ATTEMPTS,
null,
true
);
} catch (Throwable) {
return;
}
}
private function ensurePaymentSyncScheduleExists(): void
{
try {
if ($this->findPaymentSyncSchedule() !== []) {
return;
}
$this->cronRepository->upsertSchedule(
self::PAYMENT_SYNC_JOB_TYPE,
self::PAYMENT_SYNC_DEFAULT_INTERVAL_SECONDS,
self::PAYMENT_SYNC_DEFAULT_PRIORITY,
self::PAYMENT_SYNC_DEFAULT_MAX_ATTEMPTS,
null,
true
);
} catch (Throwable) {
return;
}
}
private function saveImportIntervalIfRequested(Request $request): void
{
if ($request->input('orders_import_interval_minutes', null) === null) {
return;
}
$this->ensureImportScheduleExists();
$minutes = max(1, min(1440, (int) $request->input('orders_import_interval_minutes', 5)));
$schedule = $this->findImportSchedule();
$priority = max(1, min(255, (int) ($schedule['priority'] ?? self::ORDERS_IMPORT_DEFAULT_PRIORITY)));
$maxAttempts = max(1, min(20, (int) ($schedule['max_attempts'] ?? self::ORDERS_IMPORT_DEFAULT_MAX_ATTEMPTS)));
$payload = is_array($schedule['payload'] ?? null) ? $schedule['payload'] : null;
$enabled = array_key_exists('enabled', $schedule) ? !empty($schedule['enabled']) : true;
$this->cronRepository->upsertSchedule(
self::ORDERS_IMPORT_JOB_TYPE,
$minutes * 60,
$priority,
$maxAttempts,
$payload,
$enabled
);
}
private function currentStatusSyncIntervalMinutes(): int
{
$schedule = $this->findStatusSyncSchedule();
$seconds = (int) ($schedule['interval_seconds'] ?? self::ORDER_STATUS_SYNC_DEFAULT_INTERVAL_SECONDS);
return max(1, min(1440, (int) floor(max(60, $seconds) / 60)));
}
private function saveStatusSyncIntervalIfRequested(Request $request): void
{
if ($request->input('status_sync_interval_minutes', null) === null) {
return;
}
$this->ensureStatusSyncScheduleExists();
$minutes = max(1, min(1440, (int) $request->input('status_sync_interval_minutes', 15)));
$schedule = $this->findStatusSyncSchedule();
$priority = max(1, min(255, (int) ($schedule['priority'] ?? self::ORDER_STATUS_SYNC_DEFAULT_PRIORITY)));
$maxAttempts = max(1, min(20, (int) ($schedule['max_attempts'] ?? self::ORDER_STATUS_SYNC_DEFAULT_MAX_ATTEMPTS)));
$payload = is_array($schedule['payload'] ?? null) ? $schedule['payload'] : null;
$enabled = array_key_exists('enabled', $schedule) ? !empty($schedule['enabled']) : true;
$this->cronRepository->upsertSchedule(
self::ORDER_STATUS_SYNC_JOB_TYPE,
$minutes * 60,
$priority,
$maxAttempts,
$payload,
$enabled
);
}
private function currentPaymentSyncIntervalMinutes(): int
{
$schedule = $this->findPaymentSyncSchedule();
$seconds = (int) ($schedule['interval_seconds'] ?? self::PAYMENT_SYNC_DEFAULT_INTERVAL_SECONDS);
return max(1, min(1440, (int) floor(max(60, $seconds) / 60)));
}
private function savePaymentSyncIntervalIfRequested(Request $request): void
{
if ($request->input('payment_sync_interval_minutes', null) === null) {
return;
}
$this->ensurePaymentSyncScheduleExists();
$minutes = max(1, min(1440, (int) $request->input('payment_sync_interval_minutes', 10)));
$schedule = $this->findPaymentSyncSchedule();
$priority = max(1, min(255, (int) ($schedule['priority'] ?? self::PAYMENT_SYNC_DEFAULT_PRIORITY)));
$maxAttempts = max(1, min(20, (int) ($schedule['max_attempts'] ?? self::PAYMENT_SYNC_DEFAULT_MAX_ATTEMPTS)));
$payload = is_array($schedule['payload'] ?? null) ? $schedule['payload'] : null;
$enabled = array_key_exists('enabled', $schedule) ? !empty($schedule['enabled']) : true;
$this->cronRepository->upsertSchedule(
self::PAYMENT_SYNC_JOB_TYPE,
$minutes * 60,
$priority,
$maxAttempts,
$payload,
$enabled
);
}
/**
* @return array{0: array<int, array<string, mixed>>, 1: array<int, array<string, mixed>>, 2: string}
*/
private function loadDeliveryServices(): array
{
$allegroServices = [];
$apaczkaServices = [];
$errorMessage = '';
try {
$oauth = $this->allegroIntegrationRepository->getTokenCredentials();
if (!is_array($oauth)) {
$errorMessage = $this->translator->get('settings.integrations.delivery.not_connected');
} else {
$env = (string) ($oauth['environment'] ?? 'sandbox');
$accessToken = trim((string) ($oauth['access_token'] ?? ''));
if ($accessToken === '') {
$errorMessage = $this->translator->get('settings.integrations.delivery.not_connected');
} else {
try {
$response = $this->allegroApiClient->getDeliveryServices($env, $accessToken);
} catch (RuntimeException $exception) {
if (trim($exception->getMessage()) !== 'ALLEGRO_HTTP_401') {
throw $exception;
}
$refreshedToken = $this->refreshAllegroAccessToken($oauth);
if ($refreshedToken === null) {
$errorMessage = $this->translator->get('settings.integrations.delivery.not_connected');
$response = [];
} else {
$response = $this->allegroApiClient->getDeliveryServices($env, $refreshedToken);
}
}
if (is_array($response ?? null)) {
$allegroServices = is_array($response['services'] ?? null) ? $response['services'] : [];
}
}
}
} catch (Throwable $exception) {
$errorMessage = $exception->getMessage();
}
if ($this->apaczkaRepository !== null && $this->apaczkaApiClient !== null) {
try {
$credentials = $this->apaczkaRepository->getApiCredentials();
if (is_array($credentials)) {
$apaczkaServices = $this->apaczkaApiClient->getServiceStructure(
(string) ($credentials['app_id'] ?? ''),
(string) ($credentials['app_secret'] ?? '')
);
}
} catch (Throwable $exception) {
if ($errorMessage === '') {
$errorMessage = $exception->getMessage();
}
}
}
return [$allegroServices, $apaczkaServices, $errorMessage];
}
/**
* @param array<string, mixed> $oauth
*/
private function refreshAllegroAccessToken(array $oauth): ?string
{
try {
$token = $this->allegroOAuthClient->refreshAccessToken(
(string) ($oauth['environment'] ?? 'sandbox'),
(string) ($oauth['client_id'] ?? ''),
(string) ($oauth['client_secret'] ?? ''),
(string) ($oauth['refresh_token'] ?? '')
);
$expiresIn = max(0, (int) ($token['expires_in'] ?? 0));
$expiresAt = $expiresIn > 0
? (new DateTimeImmutable('now'))->add(new DateInterval('PT' . $expiresIn . 'S'))->format('Y-m-d H:i:s')
: null;
$refreshToken = trim((string) ($token['refresh_token'] ?? ''));
if ($refreshToken === '') {
$refreshToken = (string) ($oauth['refresh_token'] ?? '');
}
$this->allegroIntegrationRepository->saveTokens(
(string) ($token['access_token'] ?? ''),
$refreshToken,
(string) ($token['token_type'] ?? ''),
(string) ($token['scope'] ?? ''),
$expiresAt
);
$accessToken = trim((string) ($token['access_token'] ?? ''));
return $accessToken !== '' ? $accessToken : null;
} catch (Throwable) {
return null;
}
}
}