- 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.
902 lines
36 KiB
PHP
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;
|
|
}
|
|
}
|
|
}
|