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>
925 lines
39 KiB
PHP
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
|
|
}
|
|
}
|
|
}
|