Files
orderPRO/src/Modules/Settings/AllegroIntegrationController.php
Jacek Pyziak 3c27c4e54a feat(06-sonarqube-quality): introduce typed exception hierarchy (S112 fix)
Replace 86+ generic RuntimeException throws with domain-specific exception
classes: AllegroApiException, AllegroOAuthException, ApaczkaApiException,
ShipmentException, IntegrationConfigException — all extending OrderProException
extends RuntimeException. Existing catch(RuntimeException) blocks unaffected.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-13 11:04:52 +01:00

925 lines
39 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 AppCoreExceptionsIntegrationConfigException;
use Throwable;
final class AllegroIntegrationController
{
private const OAUTH_STATE_SESSION_KEY = 'allegro_oauth_state';
private const ORDERS_IMPORT_JOB_TYPE = 'allegro_orders_import';
private const STATUS_SYNC_JOB_TYPE = 'allegro_status_sync';
private const ORDERS_IMPORT_DEFAULT_INTERVAL_SECONDS = 300;
private const ORDERS_IMPORT_DEFAULT_PRIORITY = 20;
private const ORDERS_IMPORT_DEFAULT_MAX_ATTEMPTS = 3;
private const STATUS_SYNC_DIRECTION_ALLEGRO_TO_ORDERPRO = 'allegro_to_orderpro';
private const STATUS_SYNC_DIRECTION_ORDERPRO_TO_ALLEGRO = 'orderpro_to_allegro';
private const STATUS_SYNC_DEFAULT_INTERVAL_MINUTES = 15;
private const ORDERS_IMPORT_DEFAULT_PAYLOAD = [
'max_pages' => 5,
'page_limit' => 50,
'max_orders' => 200,
];
private const OAUTH_SCOPES = [
AllegroOAuthClient::ORDERS_READ_SCOPE,
AllegroOAuthClient::SALE_OFFERS_READ_SCOPE,
AllegroOAuthClient::SHIPMENTS_READ_SCOPE,
AllegroOAuthClient::SHIPMENTS_WRITE_SCOPE,
];
public function __construct(
private readonly Template $template,
private readonly Translator $translator,
private readonly AuthService $auth,
private readonly AllegroIntegrationRepository $repository,
private readonly AllegroStatusMappingRepository $statusMappings,
private readonly OrderStatusRepository $orderStatuses,
private readonly CronRepository $cronRepository,
private readonly AllegroOAuthClient $oauthClient,
private readonly AllegroOrderImportService $orderImportService,
private readonly AllegroStatusDiscoveryService $statusDiscoveryService,
private readonly string $appUrl,
private readonly ?CarrierDeliveryMethodMappingRepository $deliveryMappings = null,
private readonly ?AllegroApiClient $apiClient = null,
private readonly ?ApaczkaIntegrationRepository $apaczkaRepository = null,
private readonly ?ApaczkaApiClient $apaczkaApiClient = null
) {
}
public function index(Request $request): Response
{
$envParam = trim((string) $request->input('env', ''));
$activeEnv = in_array($envParam, ['sandbox', 'production'], true)
? $envParam
: $this->repository->getActiveEnvironment();
$settings = $this->repository->getSettings($activeEnv);
$tab = trim((string) $request->input('tab', 'integration'));
if (!in_array($tab, ['integration', 'statuses', 'settings', 'delivery'], true)) {
$tab = 'integration';
}
$defaultRedirectUri = $this->defaultRedirectUri();
if (trim((string) ($settings['redirect_uri'] ?? '')) === '') {
$settings['redirect_uri'] = $defaultRedirectUri;
}
$this->ensureDefaultSchedulesExist();
$importIntervalSeconds = $this->currentImportIntervalSeconds();
$statusSyncDirection = $this->currentStatusSyncDirection();
$statusSyncIntervalMinutes = $this->currentStatusSyncIntervalMinutes();
$deliveryServicesData = $tab === 'delivery' ? $this->loadDeliveryServices($settings) : [[], [], ''];
$html = $this->template->render('settings/allegro', [
'title' => $this->translator->get('settings.allegro.title'),
'activeMenu' => 'settings',
'activeSettings' => 'allegro',
'user' => $this->auth->user(),
'csrfToken' => Csrf::token(),
'settings' => $settings,
'activeTab' => $tab,
'importIntervalSeconds' => $importIntervalSeconds,
'statusSyncDirection' => $statusSyncDirection,
'statusSyncIntervalMinutes' => $statusSyncIntervalMinutes,
'statusMappings' => $this->statusMappings->listMappings(),
'orderproStatuses' => $this->orderStatuses->listStatuses(),
'defaultRedirectUri' => $defaultRedirectUri,
'errorMessage' => (string) Flash::get('settings_error', ''),
'successMessage' => (string) Flash::get('settings_success', ''),
'warningMessage' => (string) Flash::get('settings_warning', ''),
'deliveryMappings' => $this->deliveryMappings !== null ? $this->deliveryMappings->listMappings('allegro', 0) : [],
'orderDeliveryMethods' => $this->deliveryMappings !== null ? $this->deliveryMappings->getDistinctOrderDeliveryMethods('allegro', 0) : [],
'allegroDeliveryServices' => $deliveryServicesData[0],
'apaczkaDeliveryServices' => $deliveryServicesData[1],
'allegroDeliveryServicesError' => $deliveryServicesData[2],
'inpostDeliveryServices' => array_values(array_filter(
$deliveryServicesData[0],
static fn(array $svc) => stripos((string) ($svc['carrierId'] ?? ''), 'inpost') !== false
)),
], 'layouts/app');
return Response::html($html);
}
public function save(Request $request): Response
{
$redirectTo = $this->resolveRedirectPath((string) $request->input('return_to', '/settings/integrations/allegro'));
if (!Csrf::validate((string) $request->input('_token', ''))) {
Flash::set('settings_error', $this->translator->get('auth.errors.csrf_expired'));
return Response::redirect($redirectTo);
}
$environment = trim((string) $request->input('environment', 'sandbox'));
if (!in_array($environment, ['sandbox', 'production'], true)) {
Flash::set('settings_error', $this->translator->get('settings.allegro.validation.environment_invalid'));
return Response::redirect($redirectTo);
}
$clientId = trim((string) $request->input('client_id', ''));
if ($clientId !== '' && mb_strlen($clientId) > 128) {
Flash::set('settings_error', $this->translator->get('settings.allegro.validation.client_id_too_long'));
return Response::redirect($redirectTo);
}
$redirectUriInput = trim((string) $request->input('redirect_uri', ''));
$redirectUri = $redirectUriInput !== '' ? $redirectUriInput : $this->defaultRedirectUri();
if (!$this->isValidHttpUrl($redirectUri)) {
Flash::set('settings_error', $this->translator->get('settings.allegro.validation.redirect_uri_invalid'));
return Response::redirect($redirectTo);
}
$ordersFetchStartDate = trim((string) $request->input('orders_fetch_start_date', ''));
if ($ordersFetchStartDate !== '' && !$this->isValidDate($ordersFetchStartDate)) {
Flash::set('settings_error', $this->translator->get('settings.allegro.validation.orders_fetch_start_date_invalid'));
return Response::redirect($redirectTo);
}
try {
$this->repository->saveSettings([
'environment' => $environment,
'client_id' => $clientId,
'client_secret' => trim((string) $request->input('client_secret', '')),
'redirect_uri' => $redirectUri,
'orders_fetch_enabled' => (string) $request->input('orders_fetch_enabled', '0') === '1',
'orders_fetch_start_date' => $ordersFetchStartDate,
]);
$this->ensureDefaultSchedulesExist();
Flash::set('settings_success', $this->translator->get('settings.allegro.flash.saved'));
} catch (Throwable $exception) {
Flash::set(
'settings_error',
$this->translator->get('settings.allegro.flash.save_failed') . ' ' . $exception->getMessage()
);
}
return Response::redirect($redirectTo);
}
public function saveImportSettings(Request $request): Response
{
$csrfError = $this->validateCsrf((string) $request->input('_token', ''));
if ($csrfError !== null) {
return $csrfError;
}
$intervalMinutesRaw = (int) $request->input('orders_import_interval_minutes', 5);
$intervalMinutes = max(1, min(1440, $intervalMinutesRaw));
if ($intervalMinutesRaw !== $intervalMinutes) {
Flash::set('settings_error', $this->translator->get('settings.allegro.validation.orders_import_interval_invalid'));
return Response::redirect('/settings/integrations/allegro?tab=settings');
}
$statusSyncDirection = trim((string) $request->input(
'status_sync_direction',
self::STATUS_SYNC_DIRECTION_ALLEGRO_TO_ORDERPRO
));
if (!in_array($statusSyncDirection, $this->allowedStatusSyncDirections(), true)) {
Flash::set('settings_error', $this->translator->get('settings.allegro.validation.status_sync_direction_invalid'));
return Response::redirect('/settings/integrations/allegro?tab=settings');
}
$statusSyncIntervalRaw = (int) $request->input(
'status_sync_interval_minutes',
self::STATUS_SYNC_DEFAULT_INTERVAL_MINUTES
);
$statusSyncInterval = max(1, min(1440, $statusSyncIntervalRaw));
if ($statusSyncIntervalRaw !== $statusSyncInterval) {
Flash::set('settings_error', $this->translator->get('settings.allegro.validation.status_sync_interval_invalid'));
return Response::redirect('/settings/integrations/allegro?tab=settings');
}
$existing = $this->findImportSchedule();
$priority = (int) ($existing['priority'] ?? self::ORDERS_IMPORT_DEFAULT_PRIORITY);
$maxAttempts = (int) ($existing['max_attempts'] ?? self::ORDERS_IMPORT_DEFAULT_MAX_ATTEMPTS);
$payload = is_array($existing['payload'] ?? null)
? (array) $existing['payload']
: self::ORDERS_IMPORT_DEFAULT_PAYLOAD;
$enabled = array_key_exists('enabled', $existing)
? (bool) $existing['enabled']
: true;
$statusSchedule = $this->findStatusSyncSchedule();
$statusPriority = (int) ($statusSchedule['priority'] ?? self::ORDERS_IMPORT_DEFAULT_PRIORITY);
$statusMaxAttempts = (int) ($statusSchedule['max_attempts'] ?? self::ORDERS_IMPORT_DEFAULT_MAX_ATTEMPTS);
$statusEnabled = array_key_exists('enabled', $statusSchedule)
? (bool) $statusSchedule['enabled']
: true;
try {
$this->cronRepository->upsertSchedule(
self::ORDERS_IMPORT_JOB_TYPE,
$intervalMinutes * 60,
$priority,
$maxAttempts,
$payload,
$enabled
);
$this->cronRepository->upsertSchedule(
self::STATUS_SYNC_JOB_TYPE,
$statusSyncInterval * 60,
$statusPriority,
$statusMaxAttempts,
null,
$statusEnabled
);
$this->cronRepository->upsertSetting('allegro_status_sync_direction', $statusSyncDirection);
$this->cronRepository->upsertSetting('allegro_status_sync_interval_minutes', (string) $statusSyncInterval);
Flash::set('settings_success', $this->translator->get('settings.allegro.flash.import_settings_saved'));
} catch (Throwable $exception) {
Flash::set(
'settings_error',
$this->translator->get('settings.allegro.flash.import_settings_save_failed') . ' ' . $exception->getMessage()
);
}
return Response::redirect('/settings/integrations/allegro?tab=settings');
}
public function saveStatusMapping(Request $request): Response
{
$csrfError = $this->validateCsrf((string) $request->input('_token', ''));
if ($csrfError !== null) {
return $csrfError;
}
$allegroStatusCode = strtolower(trim((string) $request->input('allegro_status_code', '')));
$orderproStatusCode = strtolower(trim((string) $request->input('orderpro_status_code', '')));
$allegroStatusName = trim((string) $request->input('allegro_status_name', ''));
if ($allegroStatusCode === '') {
Flash::set('settings_error', $this->translator->get('settings.allegro.statuses.flash.allegro_status_required'));
return Response::redirect('/settings/integrations/allegro?tab=statuses');
}
if ($orderproStatusCode === '') {
Flash::set('settings_error', $this->translator->get('settings.allegro.statuses.flash.orderpro_status_required'));
return Response::redirect('/settings/integrations/allegro?tab=statuses');
}
if (!$this->orderStatusCodeExists($orderproStatusCode)) {
Flash::set('settings_error', $this->translator->get('settings.allegro.statuses.flash.orderpro_status_not_found'));
return Response::redirect('/settings/integrations/allegro?tab=statuses');
}
try {
$this->statusMappings->upsertMapping($allegroStatusCode, $allegroStatusName !== '' ? $allegroStatusName : null, $orderproStatusCode);
Flash::set('settings_success', $this->translator->get('settings.allegro.statuses.flash.saved'));
} catch (Throwable $exception) {
Flash::set('settings_error', $this->translator->get('settings.allegro.statuses.flash.save_failed') . ' ' . $exception->getMessage());
}
return Response::redirect('/settings/integrations/allegro?tab=statuses');
}
public function saveStatusMappingsBulk(Request $request): Response
{
$csrfError = $this->validateCsrf((string) $request->input('_token', ''));
if ($csrfError !== null) {
return $csrfError;
}
$codes = $request->input('allegro_status_code', []);
$names = $request->input('allegro_status_name', []);
$selectedOrderproCodes = $request->input('orderpro_status_code', []);
if (!is_array($codes) || !is_array($names) || !is_array($selectedOrderproCodes)) {
Flash::set('settings_error', $this->translator->get('settings.allegro.statuses.flash.save_failed'));
return Response::redirect('/settings/integrations/allegro?tab=statuses');
}
try {
foreach ($codes as $index => $rawCode) {
$allegroStatusCode = strtolower(trim((string) $rawCode));
if ($allegroStatusCode === '') {
continue;
}
$allegroStatusName = trim((string) ($names[$index] ?? ''));
$orderproStatusCodeRaw = strtolower(trim((string) ($selectedOrderproCodes[$index] ?? '')));
$orderproStatusCode = $orderproStatusCodeRaw !== '' ? $orderproStatusCodeRaw : null;
if ($orderproStatusCode !== null && !$this->orderStatusCodeExists($orderproStatusCode)) {
Flash::set('settings_error', $this->translator->get('settings.allegro.statuses.flash.orderpro_status_not_found'));
return Response::redirect('/settings/integrations/allegro?tab=statuses');
}
$this->statusMappings->upsertMapping(
$allegroStatusCode,
$allegroStatusName !== '' ? $allegroStatusName : null,
$orderproStatusCode
);
}
Flash::set('settings_success', $this->translator->get('settings.allegro.statuses.flash.saved_bulk'));
} catch (Throwable $exception) {
Flash::set('settings_error', $this->translator->get('settings.allegro.statuses.flash.save_failed') . ' ' . $exception->getMessage());
}
return Response::redirect('/settings/integrations/allegro?tab=statuses');
}
public function deleteStatusMapping(Request $request): Response
{
$csrfError = $this->validateCsrf((string) $request->input('_token', ''));
if ($csrfError !== null) {
return $csrfError;
}
$mappingId = max(0, (int) $request->input('mapping_id', 0));
if ($mappingId <= 0) {
Flash::set('settings_error', $this->translator->get('settings.allegro.statuses.flash.mapping_not_found'));
return Response::redirect('/settings/integrations/allegro?tab=statuses');
}
try {
$this->statusMappings->deleteMappingById($mappingId);
Flash::set('settings_success', $this->translator->get('settings.allegro.statuses.flash.deleted'));
} catch (Throwable $exception) {
Flash::set('settings_error', $this->translator->get('settings.allegro.statuses.flash.delete_failed') . ' ' . $exception->getMessage());
}
return Response::redirect('/settings/integrations/allegro?tab=statuses');
}
public function syncStatusesFromAllegro(Request $request): Response
{
$csrfError = $this->validateCsrf((string) $request->input('_token', ''));
if ($csrfError !== null) {
return $csrfError;
}
try {
$result = $this->statusDiscoveryService->discoverAndStoreStatuses(5, 100);
Flash::set(
'settings_success',
$this->translator->get('settings.allegro.statuses.flash.sync_ok', [
'discovered' => (string) ((int) ($result['discovered'] ?? 0)),
'samples' => (string) ((int) ($result['samples'] ?? 0)),
])
);
} catch (Throwable $exception) {
Flash::set('settings_error', $this->translator->get('settings.allegro.statuses.flash.sync_failed') . ' ' . $exception->getMessage());
}
return Response::redirect('/settings/integrations/allegro?tab=statuses');
}
public function startOAuth(Request $request): Response
{
$csrfError = $this->validateCsrf((string) $request->input('_token', ''));
if ($csrfError !== null) {
return $csrfError;
}
try {
$credentials = $this->requireOAuthCredentials();
$state = bin2hex(random_bytes(24));
$_SESSION[self::OAUTH_STATE_SESSION_KEY] = $state;
$url = $this->oauthClient->buildAuthorizeUrl(
(string) $credentials['environment'],
(string) $credentials['client_id'],
(string) $credentials['redirect_uri'],
$state,
self::OAUTH_SCOPES
);
return Response::redirect($url);
} catch (Throwable $exception) {
Flash::set('settings_error', $exception->getMessage());
return Response::redirect('/settings/integrations/allegro');
}
}
public function oauthCallback(Request $request): Response
{
$error = trim((string) $request->input('error', ''));
if ($error !== '') {
$description = trim((string) $request->input('error_description', ''));
$message = $this->translator->get('settings.allegro.flash.oauth_failed');
if ($description !== '') {
$message .= ' ' . $description;
}
Flash::set('settings_error', $message);
return Response::redirect('/settings/integrations/allegro');
}
$state = trim((string) $request->input('state', ''));
$expectedState = trim((string) ($_SESSION[self::OAUTH_STATE_SESSION_KEY] ?? ''));
unset($_SESSION[self::OAUTH_STATE_SESSION_KEY]);
if ($state === '' || $expectedState === '' || !hash_equals($expectedState, $state)) {
Flash::set('settings_error', $this->translator->get('settings.allegro.flash.oauth_state_invalid'));
return Response::redirect('/settings/integrations/allegro');
}
$authorizationCode = trim((string) $request->input('code', ''));
if ($authorizationCode === '') {
Flash::set('settings_error', $this->translator->get('settings.allegro.flash.oauth_code_missing'));
return Response::redirect('/settings/integrations/allegro');
}
try {
$credentials = $this->requireOAuthCredentials();
$token = $this->oauthClient->exchangeAuthorizationCode(
(string) $credentials['environment'],
(string) $credentials['client_id'],
(string) $credentials['client_secret'],
(string) $credentials['redirect_uri'],
$authorizationCode
);
$expiresAt = null;
if ((int) ($token['expires_in'] ?? 0) > 0) {
$expiresAt = (new DateTimeImmutable('now'))
->add(new DateInterval('PT' . (int) $token['expires_in'] . 'S'))
->format('Y-m-d H:i:s');
}
$this->repository->saveTokens(
(string) ($token['access_token'] ?? ''),
(string) ($token['refresh_token'] ?? ''),
(string) ($token['token_type'] ?? ''),
(string) ($token['scope'] ?? ''),
$expiresAt
);
Flash::set('settings_success', $this->translator->get('settings.allegro.flash.oauth_connected'));
} catch (Throwable $exception) {
Flash::set('settings_error', $this->translator->get('settings.allegro.flash.oauth_failed') . ' ' . $exception->getMessage());
}
return Response::redirect('/settings/integrations/allegro');
}
public function importSingleOrder(Request $request): Response
{
$csrfError = $this->validateCsrf((string) $request->input('_token', ''));
if ($csrfError !== null) {
return $csrfError;
}
$checkoutFormId = trim((string) $request->input('checkout_form_id', ''));
if ($checkoutFormId === '') {
Flash::set('settings_error', $this->translator->get('settings.allegro.flash.checkout_form_id_required'));
return Response::redirect('/settings/integrations/allegro');
}
try {
$result = $this->orderImportService->importSingleOrder($checkoutFormId);
$imageDiagnostics = is_array($result['image_diagnostics'] ?? null) ? $result['image_diagnostics'] : [];
Flash::set(
'settings_success',
$this->translator->get('settings.allegro.flash.import_single_ok', [
'source_order_id' => (string) ($result['source_order_id'] ?? $checkoutFormId),
'local_id' => (string) ((int) ($result['order_id'] ?? 0)),
'action' => !empty($result['created'])
? $this->translator->get('settings.allegro.import_action.created')
: $this->translator->get('settings.allegro.import_action.updated'),
]) . ' '
. $this->translator->get('settings.allegro.flash.import_single_media_summary', [
'with_image' => (string) ((int) ($imageDiagnostics['with_image'] ?? 0)),
'total_items' => (string) ((int) ($imageDiagnostics['total_items'] ?? 0)),
'without_image' => (string) ((int) ($imageDiagnostics['without_image'] ?? 0)),
])
);
$warningDetails = $this->buildImportImageWarningMessage($imageDiagnostics);
if ($warningDetails !== '') {
Flash::set('settings_warning', $warningDetails);
}
} catch (Throwable $exception) {
Flash::set(
'settings_error',
$this->translator->get('settings.allegro.flash.import_single_failed') . ' ' . $exception->getMessage()
);
}
return Response::redirect('/settings/integrations/allegro');
}
public function saveDeliveryMappings(Request $request): Response
{
$csrfError = $this->validateCsrf((string) $request->input('_token', ''));
if ($csrfError !== null) {
return $csrfError;
}
if ($this->deliveryMappings === null) {
Flash::set('settings_error', 'Delivery mappings not configured.');
return Response::redirect('/settings/integrations/allegro?tab=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 $idx => $orderMethod) {
$orderMethod = trim((string) $orderMethod);
$carrier = trim((string) ($carriers[$idx] ?? 'allegro'));
$provider = $carrier === 'apaczka' ? 'apaczka' : 'allegro_wza';
$providerServiceId = $provider === 'apaczka'
? trim((string) ($apaczkaMethodIds[$idx] ?? ''))
: trim((string) ($allegroMethodIds[$idx] ?? ''));
if ($orderMethod === '' || $providerServiceId === '') {
continue;
}
$mappings[] = [
'order_delivery_method' => $orderMethod,
'provider' => $provider,
'provider_service_id' => $providerServiceId,
'provider_account_id' => $provider === 'allegro_wza' ? trim((string) ($credentialsIds[$idx] ?? '')) : '',
'provider_carrier_id' => $provider === 'allegro_wza' ? trim((string) ($carrierIds[$idx] ?? '')) : '',
'provider_service_name' => trim((string) ($serviceNames[$idx] ?? '')),
];
}
try {
$this->deliveryMappings->saveMappings('allegro', 0, $mappings);
Flash::set('settings_success', $this->translator->get('settings.allegro.delivery.flash.saved'));
} catch (Throwable $exception) {
Flash::set('settings_error', $this->translator->get('settings.allegro.delivery.flash.save_failed') . ' ' . $exception->getMessage());
}
return Response::redirect('/settings/integrations/allegro?tab=delivery');
}
/**
* @param array<string, mixed> $settings
* @return array{0: array<int, array<string, mixed>>, 1: array<int, array<string, mixed>>, 2: string}
*/
private function loadDeliveryServices(array $settings): array
{
$allegroServices = [];
$apaczkaServices = [];
$errorMessage = '';
if ($this->apiClient !== null) {
$isConnected = (bool) ($settings['is_connected'] ?? false);
if (!$isConnected) {
$errorMessage = $this->translator->get('settings.allegro.delivery.not_connected');
} else {
try {
$oauth = $this->repository->getTokenCredentials();
if ($oauth === null) {
$errorMessage = $this->translator->get('settings.allegro.delivery.not_connected');
} else {
$env = (string) ($oauth['environment'] ?? 'sandbox');
$accessToken = trim((string) ($oauth['access_token'] ?? ''));
if ($accessToken === '') {
$errorMessage = $this->translator->get('settings.allegro.delivery.not_connected');
} else {
try {
$response = $this->apiClient->getDeliveryServices($env, $accessToken);
} catch (RuntimeException $ex) {
if (trim($ex->getMessage()) === 'ALLEGRO_HTTP_401') {
$refreshed = $this->refreshOAuthToken($oauth);
if ($refreshed === null) {
$errorMessage = $this->translator->get('settings.allegro.delivery.not_connected');
$response = [];
} else {
$response = $this->apiClient->getDeliveryServices($env, $refreshed);
}
} else {
throw $ex;
}
}
if (is_array($response ?? null)) {
$allegroServices = is_array($response['services'] ?? null) ? $response['services'] : [];
}
}
}
} catch (Throwable $e) {
$errorMessage = $e->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 refreshOAuthToken(array $oauth): ?string
{
try {
$token = $this->oauthClient->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->repository->saveTokens(
(string) ($token['access_token'] ?? ''),
$refreshToken,
(string) ($token['token_type'] ?? ''),
(string) ($token['scope'] ?? ''),
$expiresAt
);
return trim((string) ($token['access_token'] ?? '')) ?: null;
} catch (Throwable) {
return null;
}
}
private function defaultRedirectUri(): string
{
$base = trim($this->appUrl);
if ($base === '') {
$base = 'http://localhost:8000';
}
return rtrim($base, '/') . '/settings/integrations/allegro/oauth/callback';
}
private function validateCsrf(string $token): ?Response
{
if (Csrf::validate($token)) {
return null;
}
Flash::set('settings_error', $this->translator->get('auth.errors.csrf_expired'));
return Response::redirect('/settings/integrations/allegro');
}
private function resolveRedirectPath(string $candidate): string
{
$value = trim($candidate);
if ($value === '') {
return '/settings/integrations/allegro';
}
if (!str_starts_with($value, '/settings/integrations')) {
return '/settings/integrations/allegro';
}
return $value;
}
/**
* @return array<string, string>
*/
private function requireOAuthCredentials(): array
{
$credentials = $this->repository->getOAuthCredentials();
if ($credentials === null) {
throw new IntegrationConfigException($this->translator->get('settings.allegro.flash.credentials_missing'));
}
return $credentials;
}
private function isValidHttpUrl(string $url): bool
{
$trimmed = trim($url);
if ($trimmed === '') {
return false;
}
if (filter_var($trimmed, FILTER_VALIDATE_URL) === false) {
return false;
}
$scheme = strtolower((string) parse_url($trimmed, PHP_URL_SCHEME));
return $scheme === 'http' || $scheme === 'https';
}
private function isValidDate(string $value): bool
{
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $value) !== 1) {
return false;
}
$date = DateTimeImmutable::createFromFormat('Y-m-d', $value);
return $date instanceof DateTimeImmutable && $date->format('Y-m-d') === $value;
}
private function orderStatusCodeExists(string $code): bool
{
$needle = strtolower(trim($code));
if ($needle === '') {
return false;
}
foreach ($this->orderStatuses->listStatuses() as $row) {
$statusCode = strtolower(trim((string) ($row['code'] ?? '')));
if ($statusCode === $needle) {
return true;
}
}
return false;
}
/**
* @param array<string, mixed> $imageDiagnostics
*/
private function buildImportImageWarningMessage(array $imageDiagnostics): string
{
$withoutImage = (int) ($imageDiagnostics['without_image'] ?? 0);
if ($withoutImage <= 0) {
return '';
}
$reasonCountsRaw = $imageDiagnostics['reason_counts'] ?? [];
if (!is_array($reasonCountsRaw) || $reasonCountsRaw === []) {
return $this->translator->get('settings.allegro.flash.import_single_media_warning_generic', [
'without_image' => (string) $withoutImage,
]);
}
$parts = [];
foreach ($reasonCountsRaw as $reason => $countRaw) {
$count = (int) $countRaw;
if ($count <= 0) {
continue;
}
$parts[] = $this->reasonLabel((string) $reason) . ': ' . $count;
}
if ($parts === []) {
return $this->translator->get('settings.allegro.flash.import_single_media_warning_generic', [
'without_image' => (string) $withoutImage,
]);
}
return $this->translator->get('settings.allegro.flash.import_single_media_warning', [
'without_image' => (string) $withoutImage,
'reasons' => implode(', ', $parts),
]);
}
private function reasonLabel(string $reasonCode): string
{
return match ($reasonCode) {
'missing_offer_id' => 'brak ID oferty',
'missing_in_checkout_form' => 'brak obrazka w checkout form',
'missing_in_offer_api' => 'brak obrazka w API oferty',
'offer_api_access_denied_403' => 'brak uprawnien API ofert (403)',
'offer_api_unauthorized_401' => 'token nieautoryzowany dla API ofert (401)',
'offer_api_not_found_404' => 'oferta nie znaleziona (404)',
'offer_api_request_failed' => 'blad zapytania do API oferty',
default => str_starts_with($reasonCode, 'offer_api_http_')
? 'blad API oferty (' . str_replace('offer_api_http_', '', $reasonCode) . ')'
: $reasonCode,
};
}
private function currentImportIntervalSeconds(): int
{
$schedule = $this->findImportSchedule();
$value = (int) ($schedule['interval_seconds'] ?? self::ORDERS_IMPORT_DEFAULT_INTERVAL_SECONDS);
return max(60, min(86400, $value));
}
/**
* @return array<string, mixed>
*/
private function findImportSchedule(): array
{
try {
$schedules = $this->cronRepository->listSchedules();
} catch (Throwable) {
return [];
}
foreach ($schedules as $schedule) {
if (!is_array($schedule)) {
continue;
}
if ((string) ($schedule['job_type'] ?? '') !== self::ORDERS_IMPORT_JOB_TYPE) {
continue;
}
return $schedule;
}
return [];
}
/**
* @return array<string, mixed>
*/
private function findStatusSyncSchedule(): array
{
try {
$schedules = $this->cronRepository->listSchedules();
} catch (Throwable) {
return [];
}
foreach ($schedules as $schedule) {
if (!is_array($schedule)) {
continue;
}
if ((string) ($schedule['job_type'] ?? '') !== self::STATUS_SYNC_JOB_TYPE) {
continue;
}
return $schedule;
}
return [];
}
private function currentStatusSyncDirection(): string
{
$value = trim($this->cronRepository->getStringSetting(
'allegro_status_sync_direction',
self::STATUS_SYNC_DIRECTION_ALLEGRO_TO_ORDERPRO
));
if (!in_array($value, $this->allowedStatusSyncDirections(), true)) {
return self::STATUS_SYNC_DIRECTION_ALLEGRO_TO_ORDERPRO;
}
return $value;
}
private function currentStatusSyncIntervalMinutes(): int
{
return $this->cronRepository->getIntSetting(
'allegro_status_sync_interval_minutes',
self::STATUS_SYNC_DEFAULT_INTERVAL_MINUTES,
1,
1440
);
}
/**
* @return array<int, string>
*/
private function allowedStatusSyncDirections(): array
{
return [
self::STATUS_SYNC_DIRECTION_ALLEGRO_TO_ORDERPRO,
self::STATUS_SYNC_DIRECTION_ORDERPRO_TO_ALLEGRO,
];
}
private function ensureDefaultSchedulesExist(): void
{
try {
if ($this->findImportSchedule() === []) {
$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,
self::ORDERS_IMPORT_DEFAULT_PAYLOAD,
true
);
}
if ($this->findStatusSyncSchedule() === []) {
$this->cronRepository->upsertSchedule(
self::STATUS_SYNC_JOB_TYPE,
self::STATUS_SYNC_DEFAULT_INTERVAL_MINUTES * 60,
self::ORDERS_IMPORT_DEFAULT_PRIORITY,
self::ORDERS_IMPORT_DEFAULT_MAX_ATTEMPTS,
null,
true
);
}
} catch (Throwable) {
// non-critical: schedules will be created when user explicitly saves import settings
}
}
}