feat(06-sonarqube-quality): split god classes — ShopproOrdersSyncService + AllegroIntegrationController (06-05)

ShopproOrdersSyncService: 39→9 methods via ShopproOrderMapper + ShopproProductImageResolver.
AllegroIntegrationController: 35→25 methods via AllegroStatusMappingController + AllegroDeliveryMappingController.
S1448 violations: 6x→2x. CronHandlerFactory and routes/web.php updated.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-13 17:25:59 +01:00
parent ddf767926e
commit eb5c9bf345
8 changed files with 1421 additions and 1271 deletions

View File

@@ -11,13 +11,16 @@ use App\Modules\Orders\OrdersController;
use App\Modules\Orders\OrderImportRepository;
use App\Modules\Orders\OrdersRepository;
use App\Modules\Settings\AllegroApiClient;
use App\Modules\Settings\AllegroDeliveryMappingController;
use App\Modules\Settings\AllegroIntegrationController;
use App\Modules\Settings\AllegroIntegrationRepository;
use App\Modules\Settings\AllegroOAuthClient;
use App\Modules\Settings\AllegroOrderImportService;
use App\Modules\Settings\AllegroStatusDiscoveryService;
use App\Modules\Settings\AllegroStatusMappingController;
use App\Modules\Settings\AllegroTokenManager;
use App\Modules\Settings\AllegroStatusMappingRepository;
use App\Modules\Settings\OrderStatusRepository;
use App\Modules\Settings\ApaczkaApiClient;
use App\Modules\Settings\ApaczkaIntegrationController;
use App\Modules\Settings\ApaczkaIntegrationRepository;
@@ -65,6 +68,26 @@ return static function (Application $app): void {
$app->db(),
(string) $app->config('app.integrations.secret', '')
);
$allegroStatusDiscoveryService = new AllegroStatusDiscoveryService(
$allegroTokenManager,
new AllegroApiClient(),
$allegroStatusMappingRepository
);
$allegroStatusMappingController = new AllegroStatusMappingController(
$translator,
$allegroStatusMappingRepository,
$app->orderStatuses(),
$allegroStatusDiscoveryService
);
$allegroDeliveryMappingController = new AllegroDeliveryMappingController(
$translator,
$allegroIntegrationRepository,
$allegroOAuthClient,
new AllegroApiClient(),
$carrierDeliveryMappings,
$apaczkaIntegrationRepository,
$apaczkaApiClient
);
$allegroIntegrationController = new AllegroIntegrationController(
$template,
$translator,
@@ -82,16 +105,9 @@ return static function (Application $app): void {
$allegroStatusMappingRepository,
new OrdersRepository($app->db())
),
new AllegroStatusDiscoveryService(
$allegroTokenManager,
new AllegroApiClient(),
$allegroStatusMappingRepository
),
$allegroStatusDiscoveryService,
(string) $app->config('app.url', ''),
$carrierDeliveryMappings,
new AllegroApiClient(),
$apaczkaIntegrationRepository,
$apaczkaApiClient
$allegroDeliveryMappingController
);
$apaczkaIntegrationController = new ApaczkaIntegrationController(
$template,
@@ -231,11 +247,11 @@ return static function (Application $app): void {
$router->post('/settings/integrations/allegro/settings/save', [$allegroIntegrationController, 'saveImportSettings'], [$authMiddleware]);
$router->post('/settings/integrations/allegro/oauth/start', [$allegroIntegrationController, 'startOAuth'], [$authMiddleware]);
$router->post('/settings/integrations/allegro/import-single', [$allegroIntegrationController, 'importSingleOrder'], [$authMiddleware]);
$router->post('/settings/integrations/allegro/statuses/save', [$allegroIntegrationController, 'saveStatusMapping'], [$authMiddleware]);
$router->post('/settings/integrations/allegro/statuses/save-bulk', [$allegroIntegrationController, 'saveStatusMappingsBulk'], [$authMiddleware]);
$router->post('/settings/integrations/allegro/statuses/delete', [$allegroIntegrationController, 'deleteStatusMapping'], [$authMiddleware]);
$router->post('/settings/integrations/allegro/statuses/sync', [$allegroIntegrationController, 'syncStatusesFromAllegro'], [$authMiddleware]);
$router->post('/settings/integrations/allegro/delivery/save', [$allegroIntegrationController, 'saveDeliveryMappings'], [$authMiddleware]);
$router->post('/settings/integrations/allegro/statuses/save', [$allegroStatusMappingController, 'saveStatusMapping'], [$authMiddleware]);
$router->post('/settings/integrations/allegro/statuses/save-bulk', [$allegroStatusMappingController, 'saveStatusMappingsBulk'], [$authMiddleware]);
$router->post('/settings/integrations/allegro/statuses/delete', [$allegroStatusMappingController, 'deleteStatusMapping'], [$authMiddleware]);
$router->post('/settings/integrations/allegro/statuses/sync', [$allegroStatusMappingController, 'syncStatusesFromAllegro'], [$authMiddleware]);
$router->post('/settings/integrations/allegro/delivery/save', [$allegroDeliveryMappingController, 'saveDeliveryMappings'], [$authMiddleware]);
$router->get('/settings/integrations/allegro/oauth/callback', [$allegroIntegrationController, 'oauthCallback']);
$router->get('/settings/integrations/apaczka', [$apaczkaIntegrationController, 'index'], [$authMiddleware]);
$router->post('/settings/integrations/apaczka/save', [$apaczkaIntegrationController, 'save'], [$authMiddleware]);

View File

@@ -17,8 +17,10 @@ use App\Modules\Settings\AllegroStatusSyncService;
use App\Modules\Settings\AllegroTokenManager;
use App\Modules\Settings\ShopproApiClient;
use App\Modules\Settings\ShopproIntegrationsRepository;
use App\Modules\Settings\ShopproOrderMapper;
use App\Modules\Settings\ShopproOrdersSyncService;
use App\Modules\Settings\ShopproOrderSyncStateRepository;
use App\Modules\Settings\ShopproProductImageResolver;
use App\Modules\Settings\ShopproPaymentStatusSyncService;
use App\Modules\Settings\ShopproStatusMappingRepository;
use App\Modules\Settings\ShopproStatusSyncService;
@@ -54,13 +56,16 @@ final class CronHandlerFactory
$orderImportService
);
$shopproIntegrationsRepo = new ShopproIntegrationsRepository($this->db, $this->integrationSecret);
$shopproApiClient = new ShopproApiClient();
$shopproSyncService = new ShopproOrdersSyncService(
$shopproIntegrationsRepo,
new ShopproOrderSyncStateRepository($this->db),
new ShopproApiClient(),
$shopproApiClient,
new OrderImportRepository($this->db),
new ShopproStatusMappingRepository($this->db),
new OrdersRepository($this->db)
new OrdersRepository($this->db),
new ShopproOrderMapper(),
new ShopproProductImageResolver($shopproApiClient)
);
$shopproStatusSyncService = new ShopproStatusSyncService($shopproIntegrationsRepo, $shopproSyncService);
$shopproPaymentSyncService = new ShopproPaymentStatusSyncService(

View File

@@ -0,0 +1,226 @@
<?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\Constants\IntegrationSources;
use App\Core\Constants\RedirectPaths;
use App\Core\Security\Csrf;
use App\Core\Support\Flash;
use DateInterval;
use DateTimeImmutable;
use RuntimeException;
use Throwable;
final class AllegroDeliveryMappingController
{
public function __construct(
private readonly Translator $translator,
private readonly AllegroIntegrationRepository $repository,
private readonly AllegroOAuthClient $oauthClient,
private readonly ?AllegroApiClient $apiClient = null,
private readonly ?CarrierDeliveryMethodMappingRepository $deliveryMappings = null,
private readonly ?ApaczkaIntegrationRepository $apaczkaRepository = null,
private readonly ?ApaczkaApiClient $apaczkaApiClient = null
) {
}
public function getDeliveryMappingsRepository(): ?CarrierDeliveryMethodMappingRepository
{
return $this->deliveryMappings;
}
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(RedirectPaths::ALLEGRO_DELIVERY_TAB);
}
$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(IntegrationSources::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(RedirectPaths::ALLEGRO_DELIVERY_TAB);
}
/**
* @param array<string, mixed> $settings
* @return array{0: array<int, array<string, mixed>>, 1: array<int, array<string, mixed>>, 2: string}
*/
public function loadDeliveryServices(array $settings): array
{
[$allegroServices, $errorMessage] = $this->loadAllegroDeliveryServices($settings);
[$apaczkaServices, $apaczkaError] = $this->loadApaczkaServices();
if ($errorMessage === '' && $apaczkaError !== '') {
$errorMessage = $apaczkaError;
}
return [$allegroServices, $apaczkaServices, $errorMessage];
}
/**
* @param array<string, mixed> $settings
* @return array{0: array<int, array<string, mixed>>, 1: string}
*/
private function loadAllegroDeliveryServices(array $settings): array
{
if ($this->apiClient === null) {
return [[], ''];
}
$isConnected = (bool) ($settings['is_connected'] ?? false);
if (!$isConnected) {
return [[], $this->translator->get('settings.allegro.delivery.not_connected')];
}
try {
$oauth = $this->repository->getTokenCredentials();
if ($oauth === null) {
return [[], $this->translator->get('settings.allegro.delivery.not_connected')];
}
$env = (string) ($oauth['environment'] ?? 'sandbox');
$accessToken = trim((string) ($oauth['access_token'] ?? ''));
if ($accessToken === '') {
return [[], $this->translator->get('settings.allegro.delivery.not_connected')];
}
[$response, $fetchError] = $this->fetchAllegroDeliveryResponse($env, $accessToken, $oauth);
$services = is_array($response) && is_array($response['services'] ?? null) ? $response['services'] : [];
return [$services, $fetchError];
} catch (Throwable $e) {
return [[], $e->getMessage()];
}
}
/**
* @param array<string, mixed> $oauth
* @return array{0: array<string, mixed>, 1: string}
*/
private function fetchAllegroDeliveryResponse(string $env, string $accessToken, array $oauth): array
{
try {
$response = $this->apiClient->getDeliveryServices($env, $accessToken);
return [is_array($response) ? $response : [], ''];
} catch (RuntimeException $ex) {
if (trim($ex->getMessage()) !== 'ALLEGRO_HTTP_401') {
throw $ex;
}
$refreshed = $this->refreshOAuthToken($oauth);
if ($refreshed === null) {
return [[], $this->translator->get('settings.allegro.delivery.not_connected')];
}
$response = $this->apiClient->getDeliveryServices($env, $refreshed);
return [is_array($response) ? $response : [], ''];
}
}
/**
* @return array{0: array<int, array<string, mixed>>, 1: string}
*/
private function loadApaczkaServices(): array
{
if ($this->apaczkaRepository === null || $this->apaczkaApiClient === null) {
return [[], ''];
}
try {
$credentials = $this->apaczkaRepository->getApiCredentials();
if (!is_array($credentials)) {
return [[], ''];
}
$services = $this->apaczkaApiClient->getServiceStructure(
(string) ($credentials['app_id'] ?? ''),
(string) ($credentials['app_secret'] ?? '')
);
return [$services, ''];
} catch (Throwable $exception) {
return [[], $exception->getMessage()];
}
}
/**
* @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 validateCsrf(string $token): ?Response
{
if (Csrf::validate($token)) {
return null;
}
Flash::set('settings_error', $this->translator->get('auth.errors.csrf_expired'));
return Response::redirect(RedirectPaths::ALLEGRO_INTEGRATION);
}
}

View File

@@ -16,7 +16,6 @@ use DateTimeImmutable;
use App\Core\Constants\IntegrationSources;
use App\Core\Constants\RedirectPaths;
use App\Core\Exceptions\IntegrationConfigException;
use RuntimeException;
use Throwable;
final class AllegroIntegrationController
@@ -54,10 +53,7 @@ final class AllegroIntegrationController
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
private readonly AllegroDeliveryMappingController $deliveryController
) {
}
@@ -82,7 +78,8 @@ final class AllegroIntegrationController
$importIntervalSeconds = $this->currentImportIntervalSeconds();
$statusSyncDirection = $this->currentStatusSyncDirection();
$statusSyncIntervalMinutes = $this->currentStatusSyncIntervalMinutes();
$deliveryServicesData = $tab === 'delivery' ? $this->loadDeliveryServices($settings) : [[], [], ''];
$deliveryServicesData = $tab === 'delivery' ? $this->deliveryController->loadDeliveryServices($settings) : [[], [], ''];
$deliveryMappings = $this->deliveryController->getDeliveryMappingsRepository();
$html = $this->template->render('settings/allegro', [
'title' => $this->translator->get('settings.allegro.title'),
@@ -101,8 +98,8 @@ final class AllegroIntegrationController
'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(IntegrationSources::ALLEGRO, 0) : [],
'orderDeliveryMethods' => $this->deliveryMappings !== null ? $this->deliveryMappings->getDistinctOrderDeliveryMethods(IntegrationSources::ALLEGRO, 0) : [],
'deliveryMappings' => $deliveryMappings !== null ? $deliveryMappings->listMappings(IntegrationSources::ALLEGRO, 0) : [],
'orderDeliveryMethods' => $deliveryMappings !== null ? $deliveryMappings->getDistinctOrderDeliveryMethods(IntegrationSources::ALLEGRO, 0) : [],
'allegroDeliveryServices' => $deliveryServicesData[0],
'apaczkaDeliveryServices' => $deliveryServicesData[1],
'allegroDeliveryServicesError' => $deliveryServicesData[2],
@@ -235,134 +232,6 @@ final class AllegroIntegrationController
return Response::redirect(RedirectPaths::ALLEGRO_SETTINGS_TAB);
}
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(RedirectPaths::ALLEGRO_STATUSES_TAB);
}
if ($orderproStatusCode === '') {
Flash::set('settings_error', $this->translator->get('settings.allegro.statuses.flash.orderpro_status_required'));
return Response::redirect(RedirectPaths::ALLEGRO_STATUSES_TAB);
}
if (!$this->orderStatusCodeExists($orderproStatusCode)) {
Flash::set('settings_error', $this->translator->get('settings.allegro.statuses.flash.orderpro_status_not_found'));
return Response::redirect(RedirectPaths::ALLEGRO_STATUSES_TAB);
}
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(RedirectPaths::ALLEGRO_STATUSES_TAB);
}
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(RedirectPaths::ALLEGRO_STATUSES_TAB);
}
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(RedirectPaths::ALLEGRO_STATUSES_TAB);
}
$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(RedirectPaths::ALLEGRO_STATUSES_TAB);
}
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(RedirectPaths::ALLEGRO_STATUSES_TAB);
}
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(RedirectPaths::ALLEGRO_STATUSES_TAB);
}
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(RedirectPaths::ALLEGRO_STATUSES_TAB);
}
public function startOAuth(Request $request): Response
{
$csrfError = $this->validateCsrf((string) $request->input('_token', ''));
@@ -493,187 +362,6 @@ final class AllegroIntegrationController
return Response::redirect(RedirectPaths::ALLEGRO_INTEGRATION);
}
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(RedirectPaths::ALLEGRO_DELIVERY_TAB);
}
$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(IntegrationSources::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(RedirectPaths::ALLEGRO_DELIVERY_TAB);
}
/**
* @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, $errorMessage] = $this->loadAllegroDeliveryServices($settings);
[$apaczkaServices, $apaczkaError] = $this->loadApaczkaServices();
if ($errorMessage === '' && $apaczkaError !== '') {
$errorMessage = $apaczkaError;
}
return [$allegroServices, $apaczkaServices, $errorMessage];
}
/**
* @param array<string, mixed> $settings
* @return array{0: array<int, array<string, mixed>>, 1: string}
*/
private function loadAllegroDeliveryServices(array $settings): array
{
if ($this->apiClient === null) {
return [[], ''];
}
$isConnected = (bool) ($settings['is_connected'] ?? false);
if (!$isConnected) {
return [[], $this->translator->get('settings.allegro.delivery.not_connected')];
}
try {
$oauth = $this->repository->getTokenCredentials();
if ($oauth === null) {
return [[], $this->translator->get('settings.allegro.delivery.not_connected')];
}
$env = (string) ($oauth['environment'] ?? 'sandbox');
$accessToken = trim((string) ($oauth['access_token'] ?? ''));
if ($accessToken === '') {
return [[], $this->translator->get('settings.allegro.delivery.not_connected')];
}
[$response, $fetchError] = $this->fetchAllegroDeliveryResponse($env, $accessToken, $oauth);
$services = is_array($response) && is_array($response['services'] ?? null) ? $response['services'] : [];
return [$services, $fetchError];
} catch (Throwable $e) {
return [[], $e->getMessage()];
}
}
/**
* @param array<string, mixed> $oauth
* @return array{0: array<string, mixed>, 1: string}
*/
private function fetchAllegroDeliveryResponse(string $env, string $accessToken, array $oauth): array
{
try {
$response = $this->apiClient->getDeliveryServices($env, $accessToken);
return [is_array($response) ? $response : [], ''];
} catch (RuntimeException $ex) {
if (trim($ex->getMessage()) !== 'ALLEGRO_HTTP_401') {
throw $ex;
}
$refreshed = $this->refreshOAuthToken($oauth);
if ($refreshed === null) {
return [[], $this->translator->get('settings.allegro.delivery.not_connected')];
}
$response = $this->apiClient->getDeliveryServices($env, $refreshed);
return [is_array($response) ? $response : [], ''];
}
}
/**
* @return array{0: array<int, array<string, mixed>>, 1: string}
*/
private function loadApaczkaServices(): array
{
if ($this->apaczkaRepository === null || $this->apaczkaApiClient === null) {
return [[], ''];
}
try {
$credentials = $this->apaczkaRepository->getApiCredentials();
if (!is_array($credentials)) {
return [[], ''];
}
$services = $this->apaczkaApiClient->getServiceStructure(
(string) ($credentials['app_id'] ?? ''),
(string) ($credentials['app_secret'] ?? '')
);
return [$services, ''];
} catch (Throwable $exception) {
return [[], $exception->getMessage()];
}
}
/**
* @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);
@@ -745,23 +433,6 @@ final class AllegroIntegrationController
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
*/

View File

@@ -0,0 +1,178 @@
<?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\Constants\RedirectPaths;
use App\Core\Security\Csrf;
use App\Core\Support\Flash;
use Throwable;
final class AllegroStatusMappingController
{
public function __construct(
private readonly Translator $translator,
private readonly AllegroStatusMappingRepository $statusMappings,
private readonly OrderStatusRepository $orderStatuses,
private readonly AllegroStatusDiscoveryService $statusDiscoveryService
) {
}
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(RedirectPaths::ALLEGRO_STATUSES_TAB);
}
if ($orderproStatusCode === '') {
Flash::set('settings_error', $this->translator->get('settings.allegro.statuses.flash.orderpro_status_required'));
return Response::redirect(RedirectPaths::ALLEGRO_STATUSES_TAB);
}
if (!$this->orderStatusCodeExists($orderproStatusCode)) {
Flash::set('settings_error', $this->translator->get('settings.allegro.statuses.flash.orderpro_status_not_found'));
return Response::redirect(RedirectPaths::ALLEGRO_STATUSES_TAB);
}
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(RedirectPaths::ALLEGRO_STATUSES_TAB);
}
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(RedirectPaths::ALLEGRO_STATUSES_TAB);
}
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(RedirectPaths::ALLEGRO_STATUSES_TAB);
}
$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(RedirectPaths::ALLEGRO_STATUSES_TAB);
}
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(RedirectPaths::ALLEGRO_STATUSES_TAB);
}
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(RedirectPaths::ALLEGRO_STATUSES_TAB);
}
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(RedirectPaths::ALLEGRO_STATUSES_TAB);
}
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;
}
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(RedirectPaths::ALLEGRO_INTEGRATION);
}
}

View File

@@ -0,0 +1,824 @@
<?php
declare(strict_types=1);
namespace App\Modules\Settings;
use App\Core\Constants\IntegrationSources;
use App\Core\Support\StringHelper;
final class ShopproOrderMapper
{
/**
* @param array<int, array<string, mixed>> $items
* @return array<int, array{source_order_id:string,source_updated_at:string,payload:array<string,mixed>}>
*/
public function buildCandidates(array $items, ?string $cursorUpdatedAt, ?string $cursorOrderId): array
{
$result = [];
foreach ($items as $row) {
$sourceOrderId = $this->normalizeOrderId($this->readPath($row, ['id', 'order_id', 'external_order_id']));
$sourceUpdatedAt = StringHelper::normalizeDateTime((string) $this->readPath($row, ['updated_at', 'date_updated', 'modified_at', 'date_modified', 'created_at', 'date_created']));
if ($sourceOrderId === '' || $sourceUpdatedAt === null) {
continue;
}
if (!$this->isAfterCursor($sourceUpdatedAt, $sourceOrderId, $cursorUpdatedAt, $cursorOrderId)) {
continue;
}
$result[] = [
'source_order_id' => $sourceOrderId,
'source_updated_at' => $sourceUpdatedAt,
'payload' => $row,
];
}
usort($result, static function (array $a, array $b): int {
$cmp = strcmp((string) ($a['source_updated_at'] ?? ''), (string) ($b['source_updated_at'] ?? ''));
if ($cmp !== 0) {
return $cmp;
}
return strcmp((string) ($a['source_order_id'] ?? ''), (string) ($b['source_order_id'] ?? ''));
});
return $result;
}
/**
* @param array<string, mixed> $payload
* @param array<string, string> $statusMap
* @param array<int, string> $productImagesById
* @return array{
* order:array<string,mixed>,
* addresses:array<int,array<string,mixed>>,
* items:array<int,array<string,mixed>>,
* payments:array<int,array<string,mixed>>,
* shipments:array<int,array<string,mixed>>,
* notes:array<int,array<string,mixed>>,
* status_history:array<int,array<string,mixed>>
* }
*/
public function mapOrderAggregate(
int $integrationId,
array $payload,
array $statusMap,
string $fallbackOrderId,
string $fallbackUpdatedAt,
array $productImagesById = []
): array {
$sourceOrderId = $this->normalizeOrderId($this->readPath($payload, ['id', 'order_id', 'external_order_id']));
if ($sourceOrderId === '') {
$sourceOrderId = $fallbackOrderId;
}
$sourceCreatedAt = StringHelper::normalizeDateTime((string) $this->readPath($payload, ['created_at', 'date_created', 'date_add']));
$sourceUpdatedAt = StringHelper::normalizeDateTime((string) $this->readPath($payload, ['updated_at', 'date_updated', 'modified_at', 'date_modified', 'created_at']));
if ($sourceUpdatedAt === null) {
$sourceUpdatedAt = $fallbackUpdatedAt !== '' ? $fallbackUpdatedAt : date('Y-m-d H:i:s');
}
$originalStatus = strtolower(trim((string) $this->readPath($payload, ['status', 'status_code', 'order_status'])));
$effectiveStatus = $statusMap[$originalStatus] ?? $originalStatus;
if ($effectiveStatus === '') {
$effectiveStatus = 'new';
}
$currency = trim((string) $this->readPath($payload, ['currency', 'totals.currency']));
if ($currency === '') {
$currency = 'PLN';
}
$totalGross = $this->toFloatOrNull($this->readPath($payload, [
'total_gross', 'total_with_tax', 'summary.total', 'totals.gross', 'summary', 'amount',
]));
$transportCost = $this->toFloatOrNull($this->readPath($payload, ['transport_cost', 'delivery_cost', 'shipping.cost']));
if ($totalGross === null && $transportCost !== null) {
$productsSum = 0.0;
$hasProducts = false;
$rawItemsForSummary = $this->readPath($payload, ['products', 'items', 'order_items']);
if (is_array($rawItemsForSummary)) {
foreach ($rawItemsForSummary as $rawItem) {
if (!is_array($rawItem)) {
continue;
}
$itemPrice = $this->toFloatOrNull($this->readPath($rawItem, [
'price_brutto', 'price_gross', 'gross_price', 'price',
]));
$itemQty = $this->toFloatOrDefault($this->readPath($rawItem, ['quantity', 'qty']), 1.0);
if ($itemPrice === null) {
continue;
}
$hasProducts = true;
$productsSum += ($itemPrice * $itemQty);
}
}
if ($hasProducts) {
$totalGross = $productsSum + $transportCost;
}
}
$totalNet = $this->toFloatOrNull($this->readPath($payload, ['total_net', 'total_without_tax', 'totals.net']));
$totalPaid = $this->toFloatOrNull($this->readPath($payload, ['total_paid', 'payments.total_paid', 'payment.total', 'paid_amount']));
$paidFlag = $this->readPath($payload, ['paid', 'is_paid']);
$isPaid = $this->normalizePaidFlag($paidFlag);
if ($totalPaid === null) {
if ($isPaid && $totalGross !== null) {
$totalPaid = $totalGross;
}
}
$deliveryLabel = $this->buildDeliveryMethodLabel($payload);
$order = [
'integration_id' => $integrationId,
'source' => IntegrationSources::SHOPPRO,
'source_order_id' => $sourceOrderId,
'external_order_id' => $sourceOrderId,
'external_platform_id' => IntegrationSources::SHOPPRO,
'external_platform_account_id' => null,
'external_status_id' => $effectiveStatus,
'external_payment_type_id' => StringHelper::nullableString((string) $this->readPath($payload, ['payment_method', 'payment.method', 'payments.method'])),
'payment_status' => $this->mapPaymentStatus($payload, $isPaid),
'external_carrier_id' => StringHelper::nullableString($deliveryLabel),
'external_carrier_account_id' => StringHelper::nullableString((string) $this->readPath($payload, [
'transport_id', 'shipping.method_id', 'delivery.method_id',
])),
'customer_login' => StringHelper::nullableString((string) $this->readPath($payload, [
'buyer_email', 'customer.email', 'buyer.email', 'client.email', 'email', 'customer.login', 'buyer.login',
])),
'is_invoice' => $this->resolveInvoiceRequested($payload),
'is_encrypted' => false,
'is_canceled_by_buyer' => false,
'currency' => $currency,
'total_without_tax' => $totalNet,
'total_with_tax' => $totalGross,
'total_paid' => $totalPaid,
'send_date_min' => null,
'send_date_max' => null,
'ordered_at' => $sourceCreatedAt,
'source_created_at' => $sourceCreatedAt,
'source_updated_at' => $sourceUpdatedAt,
'preferences_json' => null,
'payload_json' => $payload,
'fetched_at' => date('Y-m-d H:i:s'),
];
$addresses = $this->mapAddresses($payload);
$items = $this->mapItems($payload, $productImagesById);
$payments = $this->mapPayments($payload, $currency, $totalPaid);
$shipments = $this->mapShipments($payload);
$notes = $this->mapNotes($payload);
$statusHistory = [[
'from_status_id' => null,
'to_status_id' => $effectiveStatus,
'changed_at' => $sourceUpdatedAt,
'change_source' => 'import',
'comment' => $originalStatus !== '' ? 'shopPRO status: ' . $originalStatus : null,
'payload_json' => null,
]];
return [
'order' => $order,
'addresses' => $addresses,
'items' => $items,
'payments' => $payments,
'shipments' => $shipments,
'notes' => $notes,
'status_history' => $statusHistory,
];
}
/**
* @param array<string, mixed> $payload
* @return array<int, array<string, mixed>>
*/
private function mapAddresses(array $payload): array
{
$result = [];
$customerData = $this->buildCustomerAddress($payload);
$result[] = $customerData['address'];
$invoiceAddress = $this->buildInvoiceAddress(
$payload,
$customerData['name'],
$customerData['email'],
$customerData['phone']
);
if ($invoiceAddress !== null) {
$result[] = $invoiceAddress;
}
$deliveryAddress = $this->buildDeliveryAddress(
$payload,
$customerData['name'],
$customerData['email'],
$customerData['phone']
);
if ($deliveryAddress !== null) {
$result[] = $deliveryAddress;
}
return $result;
}
/**
* @param array<string, mixed> $payload
* @return array{address:array<string,mixed>,name:?string,email:?string,phone:?string}
*/
private function buildCustomerAddress(array $payload): array
{
$customerFirstName = StringHelper::nullableString((string) $this->readPath($payload, [
'buyer.first_name', 'buyer.firstname', 'customer.first_name', 'customer.firstname',
'client.first_name', 'client.firstname', 'billing_address.first_name', 'billing_address.firstname',
'first_name', 'firstname', 'client_name', 'imie',
]));
$customerLastName = StringHelper::nullableString((string) $this->readPath($payload, [
'buyer.last_name', 'buyer.lastname', 'customer.last_name', 'customer.lastname',
'client.last_name', 'client.lastname', 'billing_address.last_name', 'billing_address.lastname',
'last_name', 'lastname', 'client_surname', 'nazwisko',
]));
$customerName = StringHelper::nullableString((string) $this->readPath($payload, [
'buyer_name', 'buyer.name', 'customer.name', 'client.name', 'billing_address.name',
'receiver.name', 'client', 'customer_full_name', 'client_full_name',
])) ?? $this->composeName($customerFirstName, $customerLastName, 'Klient');
$customerEmail = StringHelper::nullableString((string) $this->readPath($payload, [
'buyer_email', 'buyer.email', 'customer.email', 'client.email', 'billing_address.email',
'shipping_address.email', 'delivery_address.email', 'email', 'client_email', 'mail',
]));
$customerPhone = StringHelper::nullableString((string) $this->readPath($payload, [
'buyer_phone', 'buyer.phone', 'customer.phone', 'client.phone', 'billing_address.phone',
'shipping_address.phone', 'delivery_address.phone', 'phone', 'telephone', 'client_phone',
'phone_number', 'client_phone_number',
]));
$address = [
'address_type' => 'customer',
'name' => $customerName ?? 'Klient',
'phone' => $customerPhone,
'email' => $customerEmail,
'street_name' => StringHelper::nullableString((string) $this->readPath($payload, [
'buyer_address.street', 'customer.address.street', 'billing_address.street', 'client.address.street',
'address.street', 'street', 'client_street', 'ulica',
])),
'street_number' => StringHelper::nullableString((string) $this->readPath($payload, [
'buyer_address.street_number', 'customer.address.street_number', 'billing_address.street_number',
'billing_address.house_number', 'client.address.street_number', 'address.street_number',
'house_number', 'street_no', 'street_number', 'nr_domu',
])),
'city' => StringHelper::nullableString((string) $this->readPath($payload, [
'buyer_address.city', 'customer.address.city', 'billing_address.city', 'client.address.city',
'address.city', 'city', 'client_city', 'miejscowosc',
])),
'zip_code' => StringHelper::nullableString((string) $this->readPath($payload, [
'buyer_address.zip', 'buyer_address.postcode', 'customer.address.zip', 'customer.address.postcode',
'billing_address.zip', 'billing_address.postcode', 'client.address.zip', 'address.zip',
'address.postcode', 'zip', 'postcode', 'client_postal_code', 'kod_pocztowy',
])),
'country' => StringHelper::nullableString((string) $this->readPath($payload, [
'buyer_address.country', 'customer.address.country', 'billing_address.country', 'client.address.country',
'address.country', 'country', 'kraj',
])),
'payload_json' => [
'buyer' => $this->readPath($payload, ['buyer']),
'customer' => $this->readPath($payload, ['customer']),
'billing_address' => $this->readPath($payload, ['billing_address']),
'buyer_address' => $this->readPath($payload, ['buyer_address']),
'address' => $this->readPath($payload, ['address']),
],
];
return ['address' => $address, 'name' => $customerName, 'email' => $customerEmail, 'phone' => $customerPhone];
}
/**
* @param array<string, mixed> $payload
* @return array<string, mixed>|null
*/
private function buildDeliveryAddress(array $payload, ?string $customerName, ?string $customerEmail, ?string $customerPhone): ?array
{
$deliveryFirstName = StringHelper::nullableString((string) $this->readPath($payload, [
'delivery.address.first_name', 'delivery.address.firstname', 'shipping.address.first_name', 'shipping.address.firstname',
'delivery_address.first_name', 'delivery_address.firstname', 'shipping_address.first_name', 'shipping_address.firstname',
'receiver.first_name', 'receiver.firstname', 'delivery_first_name', 'shipping_first_name',
]));
$deliveryLastName = StringHelper::nullableString((string) $this->readPath($payload, [
'delivery.address.last_name', 'delivery.address.lastname', 'shipping.address.last_name', 'shipping.address.lastname',
'delivery_address.last_name', 'delivery_address.lastname', 'shipping_address.last_name', 'shipping_address.lastname',
'receiver.last_name', 'receiver.lastname', 'delivery_last_name', 'shipping_last_name',
]));
$deliveryName = StringHelper::nullableString((string) $this->readPath($payload, [
'delivery.address.name', 'shipping.address.name', 'delivery_address.name', 'shipping_address.name',
'receiver.name', 'delivery_name', 'shipping_name',
])) ?? $this->composeName($deliveryFirstName, $deliveryLastName, null);
$pickupData = $this->parsePickupPoint((string) $this->readPath($payload, ['inpost_paczkomat', 'orlen_point', 'pickup_point']));
$fields = [
'name' => $deliveryName ?? StringHelper::nullableString($this->buildDeliveryMethodLabel($payload)),
'phone' => StringHelper::nullableString((string) $this->readPath($payload, [
'delivery.address.phone', 'shipping.address.phone', 'delivery_address.phone', 'shipping_address.phone',
'receiver.phone', 'delivery_phone', 'shipping_phone',
])) ?? $customerPhone,
'email' => StringHelper::nullableString((string) $this->readPath($payload, [
'delivery.address.email', 'shipping.address.email', 'delivery_address.email', 'shipping_address.email',
'receiver.email', 'delivery_email', 'shipping_email',
])) ?? $customerEmail,
'street_name' => StringHelper::nullableString((string) $this->readPath($payload, [
'delivery.address.street', 'shipping.address.street', 'delivery_address.street', 'shipping_address.street',
'receiver.address.street', 'delivery_street', 'shipping_street',
])) ?? StringHelper::nullableString($pickupData['street'] ?? ''),
'street_number' => StringHelper::nullableString((string) $this->readPath($payload, [
'delivery.address.street_number', 'shipping.address.street_number', 'delivery_address.street_number', 'shipping_address.street_number',
'delivery.address.house_number', 'shipping.address.house_number', 'receiver.address.street_number',
'receiver.address.house_number', 'delivery_street_number', 'shipping_street_number',
])),
'city' => StringHelper::nullableString((string) $this->readPath($payload, [
'delivery.address.city', 'shipping.address.city', 'delivery_address.city', 'shipping_address.city',
'receiver.address.city', 'delivery_city', 'shipping_city',
])) ?? StringHelper::nullableString($pickupData['city'] ?? ''),
'zip_code' => StringHelper::nullableString((string) $this->readPath($payload, [
'delivery.address.zip', 'delivery.address.postcode', 'shipping.address.zip', 'shipping.address.postcode',
'delivery_address.zip', 'delivery_address.postcode', 'shipping_address.zip', 'shipping_address.postcode',
'receiver.address.zip', 'receiver.address.postcode', 'delivery_zip', 'delivery_postcode',
'shipping_zip', 'shipping_postcode',
])) ?? StringHelper::nullableString($pickupData['zip_code'] ?? ''),
'country' => StringHelper::nullableString((string) $this->readPath($payload, [
'delivery.address.country', 'shipping.address.country', 'delivery_address.country', 'shipping_address.country',
'receiver.address.country', 'delivery_country', 'shipping_country',
])),
'parcel_external_id' => StringHelper::nullableString($pickupData['code'] ?? ''),
'parcel_name' => StringHelper::nullableString($pickupData['label'] ?? ''),
'payload_json' => [
'delivery' => $this->readPath($payload, ['delivery']),
'shipping' => $this->readPath($payload, ['shipping']),
'delivery_address' => $this->readPath($payload, ['delivery_address']),
'shipping_address' => $this->readPath($payload, ['shipping_address']),
'receiver' => $this->readPath($payload, ['receiver']),
'inpost_paczkomat' => $this->readPath($payload, ['inpost_paczkomat']),
'orlen_point' => $this->readPath($payload, ['orlen_point']),
],
];
if (!$this->hasAddressData($fields)) {
return null;
}
return [
'address_type' => 'delivery',
'name' => $fields['name'] ?? $customerName ?? 'Dostawa',
'phone' => $fields['phone'] ?? null,
'email' => $fields['email'] ?? $customerEmail,
'street_name' => $fields['street_name'] ?? null,
'street_number' => $fields['street_number'] ?? null,
'city' => $fields['city'] ?? null,
'zip_code' => $fields['zip_code'] ?? null,
'country' => $fields['country'] ?? null,
'parcel_external_id' => $fields['parcel_external_id'] ?? null,
'parcel_name' => $fields['parcel_name'] ?? null,
'payload_json' => is_array($fields['payload_json'] ?? null) ? $fields['payload_json'] : null,
];
}
/**
* @param array<string, mixed> $payload
*/
private function resolveInvoiceRequested(array $payload): bool
{
$explicitInvoice = $this->readPath($payload, ['is_invoice', 'invoice.required', 'invoice']);
if (!empty($explicitInvoice)) {
return true;
}
$companyName = StringHelper::nullableString((string) $this->readPath($payload, [
'invoice.company_name', 'invoice.company', 'billing.company_name', 'billing.company',
'firm_name', 'company_name', 'client_company', 'buyer_company',
]));
$taxNumber = StringHelper::nullableString((string) $this->readPath($payload, [
'invoice.tax_id', 'invoice.nip', 'billing.tax_id', 'billing.nip',
'firm_nip', 'company_nip', 'tax_id', 'nip',
]));
return $companyName !== null || $taxNumber !== null;
}
/**
* @param array<string, mixed> $payload
* @return array<string, mixed>|null
*/
private function buildInvoiceAddress(
array $payload,
?string $customerName,
?string $customerEmail,
?string $customerPhone
): ?array {
$companyName = StringHelper::nullableString((string) $this->readPath($payload, [
'invoice.company_name', 'invoice.company', 'billing.company_name', 'billing.company',
'firm_name', 'company_name', 'client_company', 'buyer_company',
]));
$companyTaxNumber = StringHelper::nullableString((string) $this->readPath($payload, [
'invoice.tax_id', 'invoice.nip', 'billing.tax_id', 'billing.nip',
'firm_nip', 'company_nip', 'tax_id', 'nip',
]));
$invoiceFirstName = StringHelper::nullableString((string) $this->readPath($payload, [
'invoice.first_name', 'invoice.firstname', 'billing_address.first_name', 'billing_address.firstname',
'buyer.first_name', 'customer.first_name', 'client_name',
]));
$invoiceLastName = StringHelper::nullableString((string) $this->readPath($payload, [
'invoice.last_name', 'invoice.lastname', 'billing_address.last_name', 'billing_address.lastname',
'buyer.last_name', 'customer.last_name', 'client_surname',
]));
$invoiceName = $companyName ?? $this->composeName($invoiceFirstName, $invoiceLastName, $customerName ?? 'Faktura');
$streetName = StringHelper::nullableString((string) $this->readPath($payload, [
'invoice.address.street', 'invoice.street', 'billing_address.street', 'billing.street',
'firm_street', 'company_street',
]));
$streetNumber = StringHelper::nullableString((string) $this->readPath($payload, [
'invoice.address.street_number', 'invoice.street_number', 'invoice.house_number',
'billing_address.street_number', 'billing_address.house_number',
'billing.street_number', 'house_number', 'street_number',
]));
$city = StringHelper::nullableString((string) $this->readPath($payload, [
'invoice.address.city', 'invoice.city', 'billing_address.city', 'billing.city',
'firm_city', 'company_city',
]));
$zipCode = StringHelper::nullableString((string) $this->readPath($payload, [
'invoice.address.zip', 'invoice.address.postcode', 'invoice.zip', 'invoice.postcode',
'billing_address.zip', 'billing_address.postcode', 'billing.zip', 'billing.postcode',
'firm_postal_code', 'company_postal_code',
]));
$country = StringHelper::nullableString((string) $this->readPath($payload, [
'invoice.address.country', 'invoice.country', 'billing_address.country', 'billing.country',
'firm_country', 'company_country',
]));
$email = StringHelper::nullableString((string) $this->readPath($payload, [
'invoice.email', 'billing_address.email', 'billing.email', 'client_email',
])) ?? $customerEmail;
$phone = StringHelper::nullableString((string) $this->readPath($payload, [
'invoice.phone', 'billing_address.phone', 'billing.phone', 'client_phone',
])) ?? $customerPhone;
$hasInvoiceData = $companyName !== null
|| $companyTaxNumber !== null
|| $streetName !== null
|| $city !== null
|| $zipCode !== null;
if (!$hasInvoiceData) {
return null;
}
return [
'address_type' => 'invoice',
'name' => $invoiceName ?? 'Faktura',
'phone' => $phone,
'email' => $email,
'street_name' => $streetName,
'street_number' => $streetNumber,
'city' => $city,
'zip_code' => $zipCode,
'country' => $country,
'company_tax_number' => $companyTaxNumber,
'company_name' => $companyName,
'payload_json' => [
'invoice' => $this->readPath($payload, ['invoice']),
'billing' => $this->readPath($payload, ['billing']),
'billing_address' => $this->readPath($payload, ['billing_address']),
'firm_name' => $this->readPath($payload, ['firm_name']),
'firm_nip' => $this->readPath($payload, ['firm_nip']),
'firm_street' => $this->readPath($payload, ['firm_street']),
'firm_postal_code' => $this->readPath($payload, ['firm_postal_code']),
'firm_city' => $this->readPath($payload, ['firm_city']),
],
];
}
private function composeName(?string $firstName, ?string $lastName, ?string $fallback): ?string
{
$name = trim(trim((string) $firstName) . ' ' . trim((string) $lastName));
if ($name !== '') {
return $name;
}
$fallbackValue = trim((string) $fallback);
return $fallbackValue !== '' ? $fallbackValue : null;
}
/**
* @param array<string, mixed> $address
*/
private function hasAddressData(array $address): bool
{
$fields = ['name', 'phone', 'email', 'street_name', 'street_number', 'city', 'zip_code', 'country'];
foreach ($fields as $field) {
$value = trim((string) ($address[$field] ?? ''));
if ($value !== '') {
return true;
}
}
return false;
}
/**
* @param array<string, mixed> $payload
* @param array<int, string> $productImagesById
* @return array<int, array<string, mixed>>
*/
private function mapItems(array $payload, array $productImagesById = []): array
{
$rawItems = $this->readPath($payload, ['items']);
if (!is_array($rawItems)) {
$rawItems = $this->readPath($payload, ['order_items']);
}
if (!is_array($rawItems)) {
$rawItems = $this->readPath($payload, ['products']);
}
if (!is_array($rawItems)) {
return [];
}
$result = [];
$sort = 0;
foreach ($rawItems as $row) {
if (!is_array($row)) {
continue;
}
$name = trim((string) $this->readPath($row, ['name', 'title']));
if ($name === '') {
continue;
}
$productId = (int) $this->readPath($row, ['product_id']);
$parentProductId = (int) $this->readPath($row, ['parent_product_id']);
$mediaUrl = StringHelper::nullableString((string) $this->readPath($row, ['image', 'image_url', 'img_url', 'img', 'photo', 'photo_url']));
if ($mediaUrl === null && $productId > 0 && isset($productImagesById[$productId])) {
$mediaUrl = StringHelper::nullableString((string) $productImagesById[$productId]);
}
if ($mediaUrl === null && $parentProductId > 0 && isset($productImagesById[$parentProductId])) {
$mediaUrl = StringHelper::nullableString((string) $productImagesById[$parentProductId]);
}
$result[] = [
'source_item_id' => StringHelper::nullableString((string) $this->readPath($row, ['id', 'item_id'])),
'external_item_id' => StringHelper::nullableString((string) $this->readPath($row, ['id', 'item_id'])),
'ean' => StringHelper::nullableString((string) $this->readPath($row, ['ean'])),
'sku' => StringHelper::nullableString((string) $this->readPath($row, ['sku', 'symbol', 'code'])),
'original_name' => $name,
'original_code' => StringHelper::nullableString((string) $this->readPath($row, ['code', 'symbol'])),
'original_price_with_tax' => $this->toFloatOrNull($this->readPath($row, ['price_gross', 'gross_price', 'price', 'price_brutto'])),
'original_price_without_tax' => $this->toFloatOrNull($this->readPath($row, ['price_net', 'net_price', 'price_netto'])),
'media_url' => $mediaUrl,
'quantity' => $this->toFloatOrDefault($this->readPath($row, ['quantity', 'qty']), 1.0),
'tax_rate' => $this->toFloatOrNull($this->readPath($row, ['vat', 'tax_rate'])),
'item_status' => null,
'unit' => StringHelper::nullableString((string) $this->readPath($row, ['unit'])),
'item_type' => 'product',
'source_product_id' => StringHelper::nullableString((string) ($productId > 0 ? $productId : $parentProductId)),
'source_product_set_id' => StringHelper::nullableString((string) ($parentProductId > 0 ? $parentProductId : '')),
'sort_order' => $sort++,
'payload_json' => $row,
];
}
return $result;
}
/**
* @param array<string, mixed> $payload
* @return array<int, array<string, mixed>>
*/
private function mapPayments(array $payload, string $currency, ?float $totalPaid): array
{
$paymentMethod = StringHelper::nullableString((string) $this->readPath($payload, ['payment_method', 'payment.method']));
if ($paymentMethod === null && $totalPaid === null) {
return [];
}
return [[
'source_payment_id' => null,
'external_payment_id' => null,
'payment_type_id' => $paymentMethod ?? 'unknown',
'payment_date' => StringHelper::nullableString((string) $this->readPath($payload, ['payment_date', 'payment.date'])),
'amount' => $totalPaid,
'currency' => $currency,
'comment' => StringHelper::nullableString((string) $this->readPath($payload, ['payment_status', 'payment.status'])),
'payload_json' => null,
]];
}
/**
* @param array<string, mixed> $payload
* @return array<int, array<string, mixed>>
*/
private function mapShipments(array $payload): array
{
$tracking = StringHelper::nullableString((string) $this->readPath($payload, ['delivery_tracking_number', 'delivery.tracking_number', 'shipping.tracking_number']));
if ($tracking === null) {
return [];
}
return [[
'source_shipment_id' => null,
'external_shipment_id' => null,
'tracking_number' => $tracking,
'carrier_provider_id' => $this->sanitizePlainText((string) ($this->readPath($payload, [
'delivery_method', 'shipping.method', 'transport', 'transport_description',
]) ?? 'unknown')),
'posted_at' => StringHelper::nullableString((string) $this->readPath($payload, ['delivery.posted_at', 'shipping.posted_at'])),
'media_uuid' => null,
'payload_json' => null,
]];
}
/**
* @param array<string, mixed> $payload
* @return array<int, array<string, mixed>>
*/
private function mapNotes(array $payload): array
{
$comment = StringHelper::nullableString((string) $this->readPath($payload, ['notes', 'comment', 'customer_comment']));
if ($comment === null) {
return [];
}
return [[
'source_note_id' => null,
'note_type' => 'message',
'created_at_external' => null,
'comment' => $comment,
'payload_json' => null,
]];
}
private function normalizeOrderId(mixed $value): string
{
return trim((string) $value);
}
private function normalizePaidFlag(mixed $value): bool
{
if ($value === true) {
return true;
}
if ($value === false || $value === null) {
return false;
}
$normalized = strtolower(trim((string) $value));
return in_array($normalized, ['1', 'true', 'yes', 'paid'], true);
}
private function mapPaymentStatus(array $payload, bool $isPaid): ?int
{
if ($isPaid) {
return 2;
}
$raw = strtolower(trim((string) $this->readPath($payload, ['payment_status', 'payment.status'])));
if ($raw === '') {
return 0;
}
return match ($raw) {
'paid', 'finished', 'completed' => 2,
'partially_paid', 'in_progress' => 1,
'cancelled', 'canceled', 'failed', 'unpaid', 'not_paid' => 0,
default => 0,
};
}
private function sanitizePlainText(string $value): string
{
$withoutTags = strip_tags($value);
$decoded = html_entity_decode($withoutTags, ENT_QUOTES | ENT_HTML5, 'UTF-8');
return trim(preg_replace('/\s+/', ' ', $decoded) ?? '');
}
private function buildDeliveryMethodLabel(array $payload): string
{
$label = $this->sanitizePlainText((string) $this->readPath($payload, [
'delivery_method', 'shipping.method', 'delivery.method', 'shipping_method', 'delivery_name', 'shipping_name',
'transport', 'transport_description',
]));
$cost = $this->toFloatOrNull($this->readPath($payload, ['transport_cost', 'delivery_cost', 'shipping.cost']));
if ($label !== '' && $cost !== null) {
$label .= ': ' . $this->formatMoneyCompact($cost) . ' zł';
}
return $label;
}
private function formatMoneyCompact(float $amount): string
{
$formatted = number_format($amount, 2, '.', '');
return rtrim(rtrim($formatted, '0'), '.');
}
/**
* @return array{code:string,label:string,street:string,zip_code:string,city:string}
*/
private function parsePickupPoint(string $raw): array
{
$value = trim($this->sanitizePlainText($raw));
if ($value === '') {
return ['code' => '', 'label' => '', 'street' => '', 'zip_code' => '', 'city' => ''];
}
$code = '';
$address = $value;
$parts = preg_split('/\s*\|\s*/', $value);
if (is_array($parts) && count($parts) >= 2) {
$code = trim((string) ($parts[0] ?? ''));
$address = trim((string) ($parts[1] ?? ''));
}
$street = '';
$zip = '';
$city = '';
if (preg_match('/^(.*?),\s*(\d{2}-\d{3})\s+(.+)$/u', $address, $matches) === 1) {
$street = trim((string) ($matches[1] ?? ''));
$zip = trim((string) ($matches[2] ?? ''));
$city = trim((string) ($matches[3] ?? ''));
} elseif (preg_match('/(\d{2}-\d{3})\s+(.+)$/u', $address, $matches) === 1) {
$zip = trim((string) ($matches[1] ?? ''));
$city = trim((string) ($matches[2] ?? ''));
}
return [
'code' => $code,
'label' => $value,
'street' => $street,
'zip_code' => $zip,
'city' => $city,
];
}
private function isAfterCursor(string $sourceUpdatedAt, string $sourceOrderId, ?string $cursorUpdatedAt, ?string $cursorOrderId): bool
{
if ($cursorUpdatedAt === null || $cursorUpdatedAt === '') {
return true;
}
if ($sourceUpdatedAt > $cursorUpdatedAt) {
return true;
}
if ($sourceUpdatedAt < $cursorUpdatedAt) {
return false;
}
if ($cursorOrderId === null || $cursorOrderId === '') {
return true;
}
return strcmp($sourceOrderId, $cursorOrderId) > 0;
}
private function toFloatOrNull(mixed $value): ?float
{
if ($value === null || $value === '') {
return null;
}
if (is_string($value)) {
$value = str_replace(',', '.', trim($value));
}
if (!is_numeric($value)) {
return null;
}
return (float) $value;
}
private function toFloatOrDefault(mixed $value, float $default): float
{
$result = $this->toFloatOrNull($value);
return $result ?? $default;
}
private function readPath(mixed $payload, array $paths): mixed
{
foreach ($paths as $path) {
$value = $this->readSinglePath($payload, (string) $path);
if ($value !== null && $value !== '') {
return $value;
}
}
return null;
}
private function readSinglePath(mixed $payload, string $path): mixed
{
if ($path === '') {
return null;
}
$segments = explode('.', $path);
$current = $payload;
foreach ($segments as $segment) {
if (!is_array($current) || !array_key_exists($segment, $current)) {
return null;
}
$current = $current[$segment];
}
return $current;
}
}

View File

@@ -3,7 +3,6 @@ declare(strict_types=1);
namespace App\Modules\Settings;
use App\Core\Constants\IntegrationSources;
use App\Core\Support\StringHelper;
use App\Modules\Orders\OrderImportRepository;
use App\Modules\Orders\OrdersRepository;
@@ -18,7 +17,9 @@ final class ShopproOrdersSyncService
private readonly ShopproApiClient $apiClient,
private readonly OrderImportRepository $orderImportRepository,
private readonly ShopproStatusMappingRepository $statusMappings,
private readonly OrdersRepository $orders
private readonly OrdersRepository $orders,
private readonly ShopproOrderMapper $mapper,
private readonly ShopproProductImageResolver $imageResolver
) {
}
@@ -102,7 +103,7 @@ final class ShopproOrdersSyncService
if ($items === []) {
break;
}
$candidates = $this->buildCandidates($items, $cursorUpdatedAt, $cursorOrderId);
$candidates = $this->mapper->buildCandidates($items, $cursorUpdatedAt, $cursorOrderId);
$this->processPageCandidates(
$candidates, $integrationId, $baseUrl, (string) $apiKey, $timeout,
$statusMap, $maxOrders, $result, $productImageCache, $shouldStop,
@@ -214,10 +215,10 @@ final class ShopproOrdersSyncService
array &$productImageCache
): void {
try {
$productImages = $this->resolveProductImagesForOrder(
$productImages = $this->imageResolver->resolveProductImagesForOrder(
$baseUrl, $apiKey, $timeout, $rawOrder, $productImageCache
);
$aggregate = $this->mapOrderAggregate(
$aggregate = $this->mapper->mapOrderAggregate(
$integrationId, $rawOrder, $statusMap, $sourceOrderId, $sourceUpdatedAt, $productImages
);
$save = $this->orderImportRepository->upsertOrderAggregate(
@@ -315,920 +316,4 @@ final class ShopproOrdersSyncService
return $cursor > $settings ? $cursor : $settings;
}
/**
* @param array<int, array<string, mixed>> $items
* @return array<int, array{source_order_id:string,source_updated_at:string,payload:array<string,mixed>}>
*/
private function buildCandidates(array $items, ?string $cursorUpdatedAt, ?string $cursorOrderId): array
{
$result = [];
foreach ($items as $row) {
$sourceOrderId = $this->normalizeOrderId($this->readPath($row, ['id', 'order_id', 'external_order_id']));
$sourceUpdatedAt = StringHelper::normalizeDateTime((string) $this->readPath($row, ['updated_at', 'date_updated', 'modified_at', 'date_modified', 'created_at', 'date_created']));
if ($sourceOrderId === '' || $sourceUpdatedAt === null) {
continue;
}
if (!$this->isAfterCursor($sourceUpdatedAt, $sourceOrderId, $cursorUpdatedAt, $cursorOrderId)) {
continue;
}
$result[] = [
'source_order_id' => $sourceOrderId,
'source_updated_at' => $sourceUpdatedAt,
'payload' => $row,
];
}
usort($result, static function (array $a, array $b): int {
$cmp = strcmp((string) ($a['source_updated_at'] ?? ''), (string) ($b['source_updated_at'] ?? ''));
if ($cmp !== 0) {
return $cmp;
}
return strcmp((string) ($a['source_order_id'] ?? ''), (string) ($b['source_order_id'] ?? ''));
});
return $result;
}
private function isAfterCursor(string $sourceUpdatedAt, string $sourceOrderId, ?string $cursorUpdatedAt, ?string $cursorOrderId): bool
{
if ($cursorUpdatedAt === null || $cursorUpdatedAt === '') {
return true;
}
if ($sourceUpdatedAt > $cursorUpdatedAt) {
return true;
}
if ($sourceUpdatedAt < $cursorUpdatedAt) {
return false;
}
if ($cursorOrderId === null || $cursorOrderId === '') {
return true;
}
return strcmp($sourceOrderId, $cursorOrderId) > 0;
}
/**
* @param array<string, mixed> $payload
* @param array<string, string> $statusMap
* @param array<int, string> $productImagesById
* @return array{
* order:array<string,mixed>,
* addresses:array<int,array<string,mixed>>,
* items:array<int,array<string,mixed>>,
* payments:array<int,array<string,mixed>>,
* shipments:array<int,array<string,mixed>>,
* notes:array<int,array<string,mixed>>,
* status_history:array<int,array<string,mixed>>
* }
*/
private function mapOrderAggregate(
int $integrationId,
array $payload,
array $statusMap,
string $fallbackOrderId,
string $fallbackUpdatedAt,
array $productImagesById = []
): array {
$sourceOrderId = $this->normalizeOrderId($this->readPath($payload, ['id', 'order_id', 'external_order_id']));
if ($sourceOrderId === '') {
$sourceOrderId = $fallbackOrderId;
}
$sourceCreatedAt = StringHelper::normalizeDateTime((string) $this->readPath($payload, ['created_at', 'date_created', 'date_add']));
$sourceUpdatedAt = StringHelper::normalizeDateTime((string) $this->readPath($payload, ['updated_at', 'date_updated', 'modified_at', 'date_modified', 'created_at']));
if ($sourceUpdatedAt === null) {
$sourceUpdatedAt = $fallbackUpdatedAt !== '' ? $fallbackUpdatedAt : date('Y-m-d H:i:s');
}
$originalStatus = strtolower(trim((string) $this->readPath($payload, ['status', 'status_code', 'order_status'])));
$effectiveStatus = $statusMap[$originalStatus] ?? $originalStatus;
if ($effectiveStatus === '') {
$effectiveStatus = 'new';
}
$currency = trim((string) $this->readPath($payload, ['currency', 'totals.currency']));
if ($currency === '') {
$currency = 'PLN';
}
$totalGross = $this->toFloatOrNull($this->readPath($payload, [
'total_gross', 'total_with_tax', 'summary.total', 'totals.gross', 'summary', 'amount',
]));
$transportCost = $this->toFloatOrNull($this->readPath($payload, ['transport_cost', 'delivery_cost', 'shipping.cost']));
if ($totalGross === null && $transportCost !== null) {
$productsSum = 0.0;
$hasProducts = false;
$rawItemsForSummary = $this->readPath($payload, ['products', 'items', 'order_items']);
if (is_array($rawItemsForSummary)) {
foreach ($rawItemsForSummary as $rawItem) {
if (!is_array($rawItem)) {
continue;
}
$itemPrice = $this->toFloatOrNull($this->readPath($rawItem, [
'price_brutto', 'price_gross', 'gross_price', 'price',
]));
$itemQty = $this->toFloatOrDefault($this->readPath($rawItem, ['quantity', 'qty']), 1.0);
if ($itemPrice === null) {
continue;
}
$hasProducts = true;
$productsSum += ($itemPrice * $itemQty);
}
}
if ($hasProducts) {
$totalGross = $productsSum + $transportCost;
}
}
$totalNet = $this->toFloatOrNull($this->readPath($payload, ['total_net', 'total_without_tax', 'totals.net']));
$totalPaid = $this->toFloatOrNull($this->readPath($payload, ['total_paid', 'payments.total_paid', 'payment.total', 'paid_amount']));
$paidFlag = $this->readPath($payload, ['paid', 'is_paid']);
$isPaid = $this->normalizePaidFlag($paidFlag);
if ($totalPaid === null) {
if ($isPaid && $totalGross !== null) {
$totalPaid = $totalGross;
}
}
$deliveryLabel = $this->buildDeliveryMethodLabel($payload);
$order = [
'integration_id' => $integrationId,
'source' => IntegrationSources::SHOPPRO,
'source_order_id' => $sourceOrderId,
'external_order_id' => $sourceOrderId,
'external_platform_id' => IntegrationSources::SHOPPRO,
'external_platform_account_id' => null,
'external_status_id' => $effectiveStatus,
'external_payment_type_id' => StringHelper::nullableString((string) $this->readPath($payload, ['payment_method', 'payment.method', 'payments.method'])),
'payment_status' => $this->mapPaymentStatus($payload, $isPaid),
'external_carrier_id' => StringHelper::nullableString($deliveryLabel),
'external_carrier_account_id' => StringHelper::nullableString((string) $this->readPath($payload, [
'transport_id', 'shipping.method_id', 'delivery.method_id',
])),
'customer_login' => StringHelper::nullableString((string) $this->readPath($payload, [
'buyer_email', 'customer.email', 'buyer.email', 'client.email', 'email', 'customer.login', 'buyer.login',
])),
'is_invoice' => $this->resolveInvoiceRequested($payload),
'is_encrypted' => false,
'is_canceled_by_buyer' => false,
'currency' => $currency,
'total_without_tax' => $totalNet,
'total_with_tax' => $totalGross,
'total_paid' => $totalPaid,
'send_date_min' => null,
'send_date_max' => null,
'ordered_at' => $sourceCreatedAt,
'source_created_at' => $sourceCreatedAt,
'source_updated_at' => $sourceUpdatedAt,
'preferences_json' => null,
'payload_json' => $payload,
'fetched_at' => date('Y-m-d H:i:s'),
];
$addresses = $this->mapAddresses($payload);
$items = $this->mapItems($payload, $productImagesById);
$payments = $this->mapPayments($payload, $currency, $totalPaid);
$shipments = $this->mapShipments($payload);
$notes = $this->mapNotes($payload);
$statusHistory = [[
'from_status_id' => null,
'to_status_id' => $effectiveStatus,
'changed_at' => $sourceUpdatedAt,
'change_source' => 'import',
'comment' => $originalStatus !== '' ? 'shopPRO status: ' . $originalStatus : null,
'payload_json' => null,
]];
return [
'order' => $order,
'addresses' => $addresses,
'items' => $items,
'payments' => $payments,
'shipments' => $shipments,
'notes' => $notes,
'status_history' => $statusHistory,
];
}
/**
* @param array<string, mixed> $payload
* @return array<int, array<string, mixed>>
*/
private function mapAddresses(array $payload): array
{
$result = [];
$customerData = $this->buildCustomerAddress($payload);
$result[] = $customerData['address'];
$invoiceAddress = $this->buildInvoiceAddress(
$payload,
$customerData['name'],
$customerData['email'],
$customerData['phone']
);
if ($invoiceAddress !== null) {
$result[] = $invoiceAddress;
}
$deliveryAddress = $this->buildDeliveryAddress(
$payload,
$customerData['name'],
$customerData['email'],
$customerData['phone']
);
if ($deliveryAddress !== null) {
$result[] = $deliveryAddress;
}
return $result;
}
/**
* @param array<string, mixed> $payload
* @return array{address:array<string,mixed>,name:?string,email:?string,phone:?string}
*/
private function buildCustomerAddress(array $payload): array
{
$customerFirstName = StringHelper::nullableString((string) $this->readPath($payload, [
'buyer.first_name', 'buyer.firstname', 'customer.first_name', 'customer.firstname',
'client.first_name', 'client.firstname', 'billing_address.first_name', 'billing_address.firstname',
'first_name', 'firstname', 'client_name', 'imie',
]));
$customerLastName = StringHelper::nullableString((string) $this->readPath($payload, [
'buyer.last_name', 'buyer.lastname', 'customer.last_name', 'customer.lastname',
'client.last_name', 'client.lastname', 'billing_address.last_name', 'billing_address.lastname',
'last_name', 'lastname', 'client_surname', 'nazwisko',
]));
$customerName = StringHelper::nullableString((string) $this->readPath($payload, [
'buyer_name', 'buyer.name', 'customer.name', 'client.name', 'billing_address.name',
'receiver.name', 'client', 'customer_full_name', 'client_full_name',
])) ?? $this->composeName($customerFirstName, $customerLastName, 'Klient');
$customerEmail = StringHelper::nullableString((string) $this->readPath($payload, [
'buyer_email', 'buyer.email', 'customer.email', 'client.email', 'billing_address.email',
'shipping_address.email', 'delivery_address.email', 'email', 'client_email', 'mail',
]));
$customerPhone = StringHelper::nullableString((string) $this->readPath($payload, [
'buyer_phone', 'buyer.phone', 'customer.phone', 'client.phone', 'billing_address.phone',
'shipping_address.phone', 'delivery_address.phone', 'phone', 'telephone', 'client_phone',
'phone_number', 'client_phone_number',
]));
$address = [
'address_type' => 'customer',
'name' => $customerName ?? 'Klient',
'phone' => $customerPhone,
'email' => $customerEmail,
'street_name' => StringHelper::nullableString((string) $this->readPath($payload, [
'buyer_address.street', 'customer.address.street', 'billing_address.street', 'client.address.street',
'address.street', 'street', 'client_street', 'ulica',
])),
'street_number' => StringHelper::nullableString((string) $this->readPath($payload, [
'buyer_address.street_number', 'customer.address.street_number', 'billing_address.street_number',
'billing_address.house_number', 'client.address.street_number', 'address.street_number',
'house_number', 'street_no', 'street_number', 'nr_domu',
])),
'city' => StringHelper::nullableString((string) $this->readPath($payload, [
'buyer_address.city', 'customer.address.city', 'billing_address.city', 'client.address.city',
'address.city', 'city', 'client_city', 'miejscowosc',
])),
'zip_code' => StringHelper::nullableString((string) $this->readPath($payload, [
'buyer_address.zip', 'buyer_address.postcode', 'customer.address.zip', 'customer.address.postcode',
'billing_address.zip', 'billing_address.postcode', 'client.address.zip', 'address.zip',
'address.postcode', 'zip', 'postcode', 'client_postal_code', 'kod_pocztowy',
])),
'country' => StringHelper::nullableString((string) $this->readPath($payload, [
'buyer_address.country', 'customer.address.country', 'billing_address.country', 'client.address.country',
'address.country', 'country', 'kraj',
])),
'payload_json' => [
'buyer' => $this->readPath($payload, ['buyer']),
'customer' => $this->readPath($payload, ['customer']),
'billing_address' => $this->readPath($payload, ['billing_address']),
'buyer_address' => $this->readPath($payload, ['buyer_address']),
'address' => $this->readPath($payload, ['address']),
],
];
return ['address' => $address, 'name' => $customerName, 'email' => $customerEmail, 'phone' => $customerPhone];
}
/**
* @param array<string, mixed> $payload
* @return array<string, mixed>|null
*/
private function buildDeliveryAddress(array $payload, ?string $customerName, ?string $customerEmail, ?string $customerPhone): ?array
{
$deliveryFirstName = StringHelper::nullableString((string) $this->readPath($payload, [
'delivery.address.first_name', 'delivery.address.firstname', 'shipping.address.first_name', 'shipping.address.firstname',
'delivery_address.first_name', 'delivery_address.firstname', 'shipping_address.first_name', 'shipping_address.firstname',
'receiver.first_name', 'receiver.firstname', 'delivery_first_name', 'shipping_first_name',
]));
$deliveryLastName = StringHelper::nullableString((string) $this->readPath($payload, [
'delivery.address.last_name', 'delivery.address.lastname', 'shipping.address.last_name', 'shipping.address.lastname',
'delivery_address.last_name', 'delivery_address.lastname', 'shipping_address.last_name', 'shipping_address.lastname',
'receiver.last_name', 'receiver.lastname', 'delivery_last_name', 'shipping_last_name',
]));
$deliveryName = StringHelper::nullableString((string) $this->readPath($payload, [
'delivery.address.name', 'shipping.address.name', 'delivery_address.name', 'shipping_address.name',
'receiver.name', 'delivery_name', 'shipping_name',
])) ?? $this->composeName($deliveryFirstName, $deliveryLastName, null);
$pickupData = $this->parsePickupPoint((string) $this->readPath($payload, ['inpost_paczkomat', 'orlen_point', 'pickup_point']));
$fields = [
'name' => $deliveryName ?? StringHelper::nullableString($this->buildDeliveryMethodLabel($payload)),
'phone' => StringHelper::nullableString((string) $this->readPath($payload, [
'delivery.address.phone', 'shipping.address.phone', 'delivery_address.phone', 'shipping_address.phone',
'receiver.phone', 'delivery_phone', 'shipping_phone',
])) ?? $customerPhone,
'email' => StringHelper::nullableString((string) $this->readPath($payload, [
'delivery.address.email', 'shipping.address.email', 'delivery_address.email', 'shipping_address.email',
'receiver.email', 'delivery_email', 'shipping_email',
])) ?? $customerEmail,
'street_name' => StringHelper::nullableString((string) $this->readPath($payload, [
'delivery.address.street', 'shipping.address.street', 'delivery_address.street', 'shipping_address.street',
'receiver.address.street', 'delivery_street', 'shipping_street',
])) ?? StringHelper::nullableString($pickupData['street'] ?? ''),
'street_number' => StringHelper::nullableString((string) $this->readPath($payload, [
'delivery.address.street_number', 'shipping.address.street_number', 'delivery_address.street_number', 'shipping_address.street_number',
'delivery.address.house_number', 'shipping.address.house_number', 'receiver.address.street_number',
'receiver.address.house_number', 'delivery_street_number', 'shipping_street_number',
])),
'city' => StringHelper::nullableString((string) $this->readPath($payload, [
'delivery.address.city', 'shipping.address.city', 'delivery_address.city', 'shipping_address.city',
'receiver.address.city', 'delivery_city', 'shipping_city',
])) ?? StringHelper::nullableString($pickupData['city'] ?? ''),
'zip_code' => StringHelper::nullableString((string) $this->readPath($payload, [
'delivery.address.zip', 'delivery.address.postcode', 'shipping.address.zip', 'shipping.address.postcode',
'delivery_address.zip', 'delivery_address.postcode', 'shipping_address.zip', 'shipping_address.postcode',
'receiver.address.zip', 'receiver.address.postcode', 'delivery_zip', 'delivery_postcode',
'shipping_zip', 'shipping_postcode',
])) ?? StringHelper::nullableString($pickupData['zip_code'] ?? ''),
'country' => StringHelper::nullableString((string) $this->readPath($payload, [
'delivery.address.country', 'shipping.address.country', 'delivery_address.country', 'shipping_address.country',
'receiver.address.country', 'delivery_country', 'shipping_country',
])),
'parcel_external_id' => StringHelper::nullableString($pickupData['code'] ?? ''),
'parcel_name' => StringHelper::nullableString($pickupData['label'] ?? ''),
'payload_json' => [
'delivery' => $this->readPath($payload, ['delivery']),
'shipping' => $this->readPath($payload, ['shipping']),
'delivery_address' => $this->readPath($payload, ['delivery_address']),
'shipping_address' => $this->readPath($payload, ['shipping_address']),
'receiver' => $this->readPath($payload, ['receiver']),
'inpost_paczkomat' => $this->readPath($payload, ['inpost_paczkomat']),
'orlen_point' => $this->readPath($payload, ['orlen_point']),
],
];
if (!$this->hasAddressData($fields)) {
return null;
}
return [
'address_type' => 'delivery',
'name' => $fields['name'] ?? $customerName ?? 'Dostawa',
'phone' => $fields['phone'] ?? null,
'email' => $fields['email'] ?? $customerEmail,
'street_name' => $fields['street_name'] ?? null,
'street_number' => $fields['street_number'] ?? null,
'city' => $fields['city'] ?? null,
'zip_code' => $fields['zip_code'] ?? null,
'country' => $fields['country'] ?? null,
'parcel_external_id' => $fields['parcel_external_id'] ?? null,
'parcel_name' => $fields['parcel_name'] ?? null,
'payload_json' => is_array($fields['payload_json'] ?? null) ? $fields['payload_json'] : null,
];
}
/**
* @param array<string, mixed> $payload
*/
private function resolveInvoiceRequested(array $payload): bool
{
$explicitInvoice = $this->readPath($payload, ['is_invoice', 'invoice.required', 'invoice']);
if (!empty($explicitInvoice)) {
return true;
}
$companyName = StringHelper::nullableString((string) $this->readPath($payload, [
'invoice.company_name', 'invoice.company', 'billing.company_name', 'billing.company',
'firm_name', 'company_name', 'client_company', 'buyer_company',
]));
$taxNumber = StringHelper::nullableString((string) $this->readPath($payload, [
'invoice.tax_id', 'invoice.nip', 'billing.tax_id', 'billing.nip',
'firm_nip', 'company_nip', 'tax_id', 'nip',
]));
return $companyName !== null || $taxNumber !== null;
}
/**
* @param array<string, mixed> $payload
* @return array<string, mixed>|null
*/
private function buildInvoiceAddress(
array $payload,
?string $customerName,
?string $customerEmail,
?string $customerPhone
): ?array {
$companyName = StringHelper::nullableString((string) $this->readPath($payload, [
'invoice.company_name', 'invoice.company', 'billing.company_name', 'billing.company',
'firm_name', 'company_name', 'client_company', 'buyer_company',
]));
$companyTaxNumber = StringHelper::nullableString((string) $this->readPath($payload, [
'invoice.tax_id', 'invoice.nip', 'billing.tax_id', 'billing.nip',
'firm_nip', 'company_nip', 'tax_id', 'nip',
]));
$invoiceFirstName = StringHelper::nullableString((string) $this->readPath($payload, [
'invoice.first_name', 'invoice.firstname', 'billing_address.first_name', 'billing_address.firstname',
'buyer.first_name', 'customer.first_name', 'client_name',
]));
$invoiceLastName = StringHelper::nullableString((string) $this->readPath($payload, [
'invoice.last_name', 'invoice.lastname', 'billing_address.last_name', 'billing_address.lastname',
'buyer.last_name', 'customer.last_name', 'client_surname',
]));
$invoiceName = $companyName ?? $this->composeName($invoiceFirstName, $invoiceLastName, $customerName ?? 'Faktura');
$streetName = StringHelper::nullableString((string) $this->readPath($payload, [
'invoice.address.street', 'invoice.street', 'billing_address.street', 'billing.street',
'firm_street', 'company_street',
]));
$streetNumber = StringHelper::nullableString((string) $this->readPath($payload, [
'invoice.address.street_number', 'invoice.street_number', 'invoice.house_number',
'billing_address.street_number', 'billing_address.house_number',
'billing.street_number', 'house_number', 'street_number',
]));
$city = StringHelper::nullableString((string) $this->readPath($payload, [
'invoice.address.city', 'invoice.city', 'billing_address.city', 'billing.city',
'firm_city', 'company_city',
]));
$zipCode = StringHelper::nullableString((string) $this->readPath($payload, [
'invoice.address.zip', 'invoice.address.postcode', 'invoice.zip', 'invoice.postcode',
'billing_address.zip', 'billing_address.postcode', 'billing.zip', 'billing.postcode',
'firm_postal_code', 'company_postal_code',
]));
$country = StringHelper::nullableString((string) $this->readPath($payload, [
'invoice.address.country', 'invoice.country', 'billing_address.country', 'billing.country',
'firm_country', 'company_country',
]));
$email = StringHelper::nullableString((string) $this->readPath($payload, [
'invoice.email', 'billing_address.email', 'billing.email', 'client_email',
])) ?? $customerEmail;
$phone = StringHelper::nullableString((string) $this->readPath($payload, [
'invoice.phone', 'billing_address.phone', 'billing.phone', 'client_phone',
])) ?? $customerPhone;
$hasInvoiceData = $companyName !== null
|| $companyTaxNumber !== null
|| $streetName !== null
|| $city !== null
|| $zipCode !== null;
if (!$hasInvoiceData) {
return null;
}
return [
'address_type' => 'invoice',
'name' => $invoiceName ?? 'Faktura',
'phone' => $phone,
'email' => $email,
'street_name' => $streetName,
'street_number' => $streetNumber,
'city' => $city,
'zip_code' => $zipCode,
'country' => $country,
'company_tax_number' => $companyTaxNumber,
'company_name' => $companyName,
'payload_json' => [
'invoice' => $this->readPath($payload, ['invoice']),
'billing' => $this->readPath($payload, ['billing']),
'billing_address' => $this->readPath($payload, ['billing_address']),
'firm_name' => $this->readPath($payload, ['firm_name']),
'firm_nip' => $this->readPath($payload, ['firm_nip']),
'firm_street' => $this->readPath($payload, ['firm_street']),
'firm_postal_code' => $this->readPath($payload, ['firm_postal_code']),
'firm_city' => $this->readPath($payload, ['firm_city']),
],
];
}
private function composeName(?string $firstName, ?string $lastName, ?string $fallback): ?string
{
$name = trim(trim((string) $firstName) . ' ' . trim((string) $lastName));
if ($name !== '') {
return $name;
}
$fallbackValue = trim((string) $fallback);
return $fallbackValue !== '' ? $fallbackValue : null;
}
/**
* @param array<string, mixed> $address
*/
private function hasAddressData(array $address): bool
{
$fields = ['name', 'phone', 'email', 'street_name', 'street_number', 'city', 'zip_code', 'country'];
foreach ($fields as $field) {
$value = trim((string) ($address[$field] ?? ''));
if ($value !== '') {
return true;
}
}
return false;
}
/**
* @param array<string, mixed> $payload
* @param array<int, string> $productImagesById
* @return array<int, array<string, mixed>>
*/
private function mapItems(array $payload, array $productImagesById = []): array
{
$rawItems = $this->readPath($payload, ['items']);
if (!is_array($rawItems)) {
$rawItems = $this->readPath($payload, ['order_items']);
}
if (!is_array($rawItems)) {
$rawItems = $this->readPath($payload, ['products']);
}
if (!is_array($rawItems)) {
return [];
}
$result = [];
$sort = 0;
foreach ($rawItems as $row) {
if (!is_array($row)) {
continue;
}
$name = trim((string) $this->readPath($row, ['name', 'title']));
if ($name === '') {
continue;
}
$productId = (int) $this->readPath($row, ['product_id']);
$parentProductId = (int) $this->readPath($row, ['parent_product_id']);
$mediaUrl = StringHelper::nullableString((string) $this->readPath($row, ['image', 'image_url', 'img_url', 'img', 'photo', 'photo_url']));
if ($mediaUrl === null && $productId > 0 && isset($productImagesById[$productId])) {
$mediaUrl = StringHelper::nullableString((string) $productImagesById[$productId]);
}
if ($mediaUrl === null && $parentProductId > 0 && isset($productImagesById[$parentProductId])) {
$mediaUrl = StringHelper::nullableString((string) $productImagesById[$parentProductId]);
}
$result[] = [
'source_item_id' => StringHelper::nullableString((string) $this->readPath($row, ['id', 'item_id'])),
'external_item_id' => StringHelper::nullableString((string) $this->readPath($row, ['id', 'item_id'])),
'ean' => StringHelper::nullableString((string) $this->readPath($row, ['ean'])),
'sku' => StringHelper::nullableString((string) $this->readPath($row, ['sku', 'symbol', 'code'])),
'original_name' => $name,
'original_code' => StringHelper::nullableString((string) $this->readPath($row, ['code', 'symbol'])),
'original_price_with_tax' => $this->toFloatOrNull($this->readPath($row, ['price_gross', 'gross_price', 'price', 'price_brutto'])),
'original_price_without_tax' => $this->toFloatOrNull($this->readPath($row, ['price_net', 'net_price', 'price_netto'])),
'media_url' => $mediaUrl,
'quantity' => $this->toFloatOrDefault($this->readPath($row, ['quantity', 'qty']), 1.0),
'tax_rate' => $this->toFloatOrNull($this->readPath($row, ['vat', 'tax_rate'])),
'item_status' => null,
'unit' => StringHelper::nullableString((string) $this->readPath($row, ['unit'])),
'item_type' => 'product',
'source_product_id' => StringHelper::nullableString((string) ($productId > 0 ? $productId : $parentProductId)),
'source_product_set_id' => StringHelper::nullableString((string) ($parentProductId > 0 ? $parentProductId : '')),
'sort_order' => $sort++,
'payload_json' => $row,
];
}
return $result;
}
/**
* @param array<string, mixed> $payload
* @return array<int, array<string, mixed>>
*/
private function mapPayments(array $payload, string $currency, ?float $totalPaid): array
{
$paymentMethod = StringHelper::nullableString((string) $this->readPath($payload, ['payment_method', 'payment.method']));
if ($paymentMethod === null && $totalPaid === null) {
return [];
}
return [[
'source_payment_id' => null,
'external_payment_id' => null,
'payment_type_id' => $paymentMethod ?? 'unknown',
'payment_date' => StringHelper::nullableString((string) $this->readPath($payload, ['payment_date', 'payment.date'])),
'amount' => $totalPaid,
'currency' => $currency,
'comment' => StringHelper::nullableString((string) $this->readPath($payload, ['payment_status', 'payment.status'])),
'payload_json' => null,
]];
}
/**
* @param array<string, mixed> $payload
* @return array<int, array<string, mixed>>
*/
private function mapShipments(array $payload): array
{
$tracking = StringHelper::nullableString((string) $this->readPath($payload, ['delivery_tracking_number', 'delivery.tracking_number', 'shipping.tracking_number']));
if ($tracking === null) {
return [];
}
return [[
'source_shipment_id' => null,
'external_shipment_id' => null,
'tracking_number' => $tracking,
'carrier_provider_id' => $this->sanitizePlainText((string) ($this->readPath($payload, [
'delivery_method', 'shipping.method', 'transport', 'transport_description',
]) ?? 'unknown')),
'posted_at' => StringHelper::nullableString((string) $this->readPath($payload, ['delivery.posted_at', 'shipping.posted_at'])),
'media_uuid' => null,
'payload_json' => null,
]];
}
/**
* @param array<string, mixed> $payload
* @return array<int, array<string, mixed>>
*/
private function mapNotes(array $payload): array
{
$comment = StringHelper::nullableString((string) $this->readPath($payload, ['notes', 'comment', 'customer_comment']));
if ($comment === null) {
return [];
}
return [[
'source_note_id' => null,
'note_type' => 'message',
'created_at_external' => null,
'comment' => $comment,
'payload_json' => null,
]];
}
private function normalizeOrderId(mixed $value): string
{
return trim((string) $value);
}
private function normalizePaidFlag(mixed $value): bool
{
if ($value === true) {
return true;
}
if ($value === false || $value === null) {
return false;
}
$normalized = strtolower(trim((string) $value));
return in_array($normalized, ['1', 'true', 'yes', 'paid'], true);
}
private function mapPaymentStatus(array $payload, bool $isPaid): ?int
{
if ($isPaid) {
return 2;
}
$raw = strtolower(trim((string) $this->readPath($payload, ['payment_status', 'payment.status'])));
if ($raw === '') {
return 0;
}
return match ($raw) {
'paid', 'finished', 'completed' => 2,
'partially_paid', 'in_progress' => 1,
'cancelled', 'canceled', 'failed', 'unpaid', 'not_paid' => 0,
default => 0,
};
}
private function sanitizePlainText(string $value): string
{
$withoutTags = strip_tags($value);
$decoded = html_entity_decode($withoutTags, ENT_QUOTES | ENT_HTML5, 'UTF-8');
return trim(preg_replace('/\s+/', ' ', $decoded) ?? '');
}
private function buildDeliveryMethodLabel(array $payload): string
{
$label = $this->sanitizePlainText((string) $this->readPath($payload, [
'delivery_method', 'shipping.method', 'delivery.method', 'shipping_method', 'delivery_name', 'shipping_name',
'transport', 'transport_description',
]));
$cost = $this->toFloatOrNull($this->readPath($payload, ['transport_cost', 'delivery_cost', 'shipping.cost']));
if ($label !== '' && $cost !== null) {
$label .= ': ' . $this->formatMoneyCompact($cost) . ' zł';
}
return $label;
}
private function formatMoneyCompact(float $amount): string
{
$formatted = number_format($amount, 2, '.', '');
return rtrim(rtrim($formatted, '0'), '.');
}
/**
* @return array{code:string,label:string,street:string,zip_code:string,city:string}
*/
private function parsePickupPoint(string $raw): array
{
$value = trim($this->sanitizePlainText($raw));
if ($value === '') {
return ['code' => '', 'label' => '', 'street' => '', 'zip_code' => '', 'city' => ''];
}
$code = '';
$address = $value;
$parts = preg_split('/\s*\|\s*/', $value);
if (is_array($parts) && count($parts) >= 2) {
$code = trim((string) ($parts[0] ?? ''));
$address = trim((string) ($parts[1] ?? ''));
}
$street = '';
$zip = '';
$city = '';
if (preg_match('/^(.*?),\s*(\d{2}-\d{3})\s+(.+)$/u', $address, $matches) === 1) {
$street = trim((string) ($matches[1] ?? ''));
$zip = trim((string) ($matches[2] ?? ''));
$city = trim((string) ($matches[3] ?? ''));
} elseif (preg_match('/(\d{2}-\d{3})\s+(.+)$/u', $address, $matches) === 1) {
$zip = trim((string) ($matches[1] ?? ''));
$city = trim((string) ($matches[2] ?? ''));
}
return [
'code' => $code,
'label' => $value,
'street' => $street,
'zip_code' => $zip,
'city' => $city,
];
}
/**
* @param array<string, mixed> $orderPayload
* @param array<int, string> $cache
* @return array<int, string>
*/
private function resolveProductImagesForOrder(
string $baseUrl,
string $apiKey,
int $timeout,
array $orderPayload,
array &$cache
): array {
$result = [];
$rawItems = $this->readPath($orderPayload, ['products', 'items', 'order_items']);
if (!is_array($rawItems)) {
return [];
}
foreach ($rawItems as $item) {
if (!is_array($item)) {
continue;
}
$productId = (int) $this->readPath($item, ['product_id']);
$parentProductId = (int) $this->readPath($item, ['parent_product_id']);
if ($productId <= 0 && $parentProductId <= 0) {
continue;
}
foreach ([$productId, $parentProductId] as $candidateId) {
if ($candidateId <= 0) {
continue;
}
if (!isset($cache[$candidateId])) {
$cache[$candidateId] = $this->fetchPrimaryProductImageUrl($baseUrl, $apiKey, $timeout, $candidateId) ?? '';
}
}
if ($productId > 0) {
$url = trim((string) ($cache[$productId] ?? ''));
if ($url !== '') {
$result[$productId] = $url;
}
}
if ($parentProductId > 0) {
$url = trim((string) ($cache[$parentProductId] ?? ''));
if ($url !== '') {
$result[$parentProductId] = $url;
}
}
}
return $result;
}
private function fetchPrimaryProductImageUrl(string $baseUrl, string $apiKey, int $timeout, int $productId): ?string
{
$response = $this->apiClient->fetchProductById($baseUrl, $apiKey, $timeout, $productId);
if (($response['ok'] ?? false) !== true || !is_array($response['product'] ?? null)) {
return null;
}
$product = (array) $response['product'];
$images = $this->readPath($product, ['images', 'photos', 'gallery']);
if (is_array($images)) {
foreach ($images as $image) {
if (is_array($image)) {
$src = trim((string) ($image['src'] ?? $image['url'] ?? $image['image'] ?? ''));
if ($src !== '') {
return $this->normalizeMediaUrl($baseUrl, $src);
}
} elseif (is_string($image)) {
$src = trim($image);
if ($src !== '') {
return $this->normalizeMediaUrl($baseUrl, $src);
}
}
}
}
$flat = trim((string) $this->readPath($product, ['image', 'image_url', 'photo', 'photo_url', 'img', 'img_url']));
if ($flat !== '') {
return $this->normalizeMediaUrl($baseUrl, $flat);
}
return null;
}
private function normalizeMediaUrl(string $baseUrl, string $value): string
{
$trimmed = trim($value);
if ($trimmed === '') {
return '';
}
if (str_starts_with($trimmed, '//')) {
return 'https:' . $trimmed;
}
if (preg_match('#^https?://#i', $trimmed) === 1) {
return $trimmed;
}
return rtrim($baseUrl, '/') . '/' . ltrim($trimmed, '/');
}
private function toFloatOrNull(mixed $value): ?float
{
if ($value === null || $value === '') {
return null;
}
if (is_string($value)) {
$value = str_replace(',', '.', trim($value));
}
if (!is_numeric($value)) {
return null;
}
return (float) $value;
}
private function toFloatOrDefault(mixed $value, float $default): float
{
$result = $this->toFloatOrNull($value);
return $result ?? $default;
}
private function readPath(mixed $payload, array $paths): mixed
{
foreach ($paths as $path) {
$value = $this->readSinglePath($payload, (string) $path);
if ($value !== null && $value !== '') {
return $value;
}
}
return null;
}
private function readSinglePath(mixed $payload, string $path): mixed
{
if ($path === '') {
return null;
}
$segments = explode('.', $path);
$current = $payload;
foreach ($segments as $segment) {
if (!is_array($current) || !array_key_exists($segment, $current)) {
return null;
}
$current = $current[$segment];
}
return $current;
}
}

View File

@@ -0,0 +1,145 @@
<?php
declare(strict_types=1);
namespace App\Modules\Settings;
final class ShopproProductImageResolver
{
public function __construct(
private readonly ShopproApiClient $apiClient
) {
}
/**
* @param array<string, mixed> $orderPayload
* @param array<int, string> $cache
* @return array<int, string>
*/
public function resolveProductImagesForOrder(
string $baseUrl,
string $apiKey,
int $timeout,
array $orderPayload,
array &$cache
): array {
$result = [];
$rawItems = $this->readPath($orderPayload, ['products', 'items', 'order_items']);
if (!is_array($rawItems)) {
return [];
}
foreach ($rawItems as $item) {
if (!is_array($item)) {
continue;
}
$productId = (int) $this->readPath($item, ['product_id']);
$parentProductId = (int) $this->readPath($item, ['parent_product_id']);
if ($productId <= 0 && $parentProductId <= 0) {
continue;
}
foreach ([$productId, $parentProductId] as $candidateId) {
if ($candidateId <= 0) {
continue;
}
if (!isset($cache[$candidateId])) {
$cache[$candidateId] = $this->fetchPrimaryProductImageUrl($baseUrl, $apiKey, $timeout, $candidateId) ?? '';
}
}
if ($productId > 0) {
$url = trim((string) ($cache[$productId] ?? ''));
if ($url !== '') {
$result[$productId] = $url;
}
}
if ($parentProductId > 0) {
$url = trim((string) ($cache[$parentProductId] ?? ''));
if ($url !== '') {
$result[$parentProductId] = $url;
}
}
}
return $result;
}
private function fetchPrimaryProductImageUrl(string $baseUrl, string $apiKey, int $timeout, int $productId): ?string
{
$response = $this->apiClient->fetchProductById($baseUrl, $apiKey, $timeout, $productId);
if (($response['ok'] ?? false) !== true || !is_array($response['product'] ?? null)) {
return null;
}
$product = (array) $response['product'];
$images = $this->readPath($product, ['images', 'photos', 'gallery']);
if (is_array($images)) {
foreach ($images as $image) {
if (is_array($image)) {
$src = trim((string) ($image['src'] ?? $image['url'] ?? $image['image'] ?? ''));
if ($src !== '') {
return $this->normalizeMediaUrl($baseUrl, $src);
}
} elseif (is_string($image)) {
$src = trim($image);
if ($src !== '') {
return $this->normalizeMediaUrl($baseUrl, $src);
}
}
}
}
$flat = trim((string) $this->readPath($product, ['image', 'image_url', 'photo', 'photo_url', 'img', 'img_url']));
if ($flat !== '') {
return $this->normalizeMediaUrl($baseUrl, $flat);
}
return null;
}
private function normalizeMediaUrl(string $baseUrl, string $value): string
{
$trimmed = trim($value);
if ($trimmed === '') {
return '';
}
if (str_starts_with($trimmed, '//')) {
return 'https:' . $trimmed;
}
if (preg_match('#^https?://#i', $trimmed) === 1) {
return $trimmed;
}
return rtrim($baseUrl, '/') . '/' . ltrim($trimmed, '/');
}
private function readPath(mixed $payload, array $paths): mixed
{
foreach ($paths as $path) {
$value = $this->readSinglePath($payload, (string) $path);
if ($value !== null && $value !== '') {
return $value;
}
}
return null;
}
private function readSinglePath(mixed $payload, string $path): mixed
{
if ($path === '') {
return null;
}
$segments = explode('.', $path);
$current = $payload;
foreach ($segments as $segment) {
if (!is_array($current) || !array_key_exists($segment, $current)) {
return null;
}
$current = $current[$segment];
}
return $current;
}
}