From eb5c9bf34537c2111f30208711a1887204667f9d Mon Sep 17 00:00:00 2001 From: Jacek Pyziak Date: Fri, 13 Mar 2026 17:25:59 +0100 Subject: [PATCH] =?UTF-8?q?feat(06-sonarqube-quality):=20split=20god=20cla?= =?UTF-8?q?sses=20=E2=80=94=20ShopproOrdersSyncService=20+=20AllegroIntegr?= =?UTF-8?q?ationController=20(06-05)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- routes/web.php | 44 +- src/Modules/Cron/CronHandlerFactory.php | 9 +- .../AllegroDeliveryMappingController.php | 226 +++++ .../Settings/AllegroIntegrationController.php | 339 +------ .../AllegroStatusMappingController.php | 178 ++++ src/Modules/Settings/ShopproOrderMapper.php | 824 ++++++++++++++++ .../Settings/ShopproOrdersSyncService.php | 927 +----------------- .../Settings/ShopproProductImageResolver.php | 145 +++ 8 files changed, 1421 insertions(+), 1271 deletions(-) create mode 100644 src/Modules/Settings/AllegroDeliveryMappingController.php create mode 100644 src/Modules/Settings/AllegroStatusMappingController.php create mode 100644 src/Modules/Settings/ShopproOrderMapper.php create mode 100644 src/Modules/Settings/ShopproProductImageResolver.php diff --git a/routes/web.php b/routes/web.php index 76b0249..3d0afd1 100644 --- a/routes/web.php +++ b/routes/web.php @@ -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]); diff --git a/src/Modules/Cron/CronHandlerFactory.php b/src/Modules/Cron/CronHandlerFactory.php index 251a250..2d48a0f 100644 --- a/src/Modules/Cron/CronHandlerFactory.php +++ b/src/Modules/Cron/CronHandlerFactory.php @@ -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( diff --git a/src/Modules/Settings/AllegroDeliveryMappingController.php b/src/Modules/Settings/AllegroDeliveryMappingController.php new file mode 100644 index 0000000..a647e06 --- /dev/null +++ b/src/Modules/Settings/AllegroDeliveryMappingController.php @@ -0,0 +1,226 @@ +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 $settings + * @return array{0: array>, 1: array>, 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 $settings + * @return array{0: array>, 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 $oauth + * @return array{0: array, 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>, 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 $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); + } +} diff --git a/src/Modules/Settings/AllegroIntegrationController.php b/src/Modules/Settings/AllegroIntegrationController.php index c4eaf77..296e8cb 100644 --- a/src/Modules/Settings/AllegroIntegrationController.php +++ b/src/Modules/Settings/AllegroIntegrationController.php @@ -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 $settings - * @return array{0: array>, 1: array>, 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 $settings - * @return array{0: array>, 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 $oauth - * @return array{0: array, 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>, 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 $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 $imageDiagnostics */ diff --git a/src/Modules/Settings/AllegroStatusMappingController.php b/src/Modules/Settings/AllegroStatusMappingController.php new file mode 100644 index 0000000..aab4710 --- /dev/null +++ b/src/Modules/Settings/AllegroStatusMappingController.php @@ -0,0 +1,178 @@ +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); + } +} diff --git a/src/Modules/Settings/ShopproOrderMapper.php b/src/Modules/Settings/ShopproOrderMapper.php new file mode 100644 index 0000000..b6d8dde --- /dev/null +++ b/src/Modules/Settings/ShopproOrderMapper.php @@ -0,0 +1,824 @@ +> $items + * @return array}> + */ + 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 $payload + * @param array $statusMap + * @param array $productImagesById + * @return array{ + * order:array, + * addresses:array>, + * items:array>, + * payments:array>, + * shipments:array>, + * notes:array>, + * status_history:array> + * } + */ + 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 $payload + * @return array> + */ + 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 $payload + * @return array{address:array,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 $payload + * @return array|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 $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 $payload + * @return array|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 $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 $payload + * @param array $productImagesById + * @return array> + */ + 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 $payload + * @return array> + */ + 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 $payload + * @return array> + */ + 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 $payload + * @return array> + */ + 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; + } +} diff --git a/src/Modules/Settings/ShopproOrdersSyncService.php b/src/Modules/Settings/ShopproOrdersSyncService.php index b52f7c5..943d1e7 100644 --- a/src/Modules/Settings/ShopproOrdersSyncService.php +++ b/src/Modules/Settings/ShopproOrdersSyncService.php @@ -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> $items - * @return array}> - */ - 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 $payload - * @param array $statusMap - * @param array $productImagesById - * @return array{ - * order:array, - * addresses:array>, - * items:array>, - * payments:array>, - * shipments:array>, - * notes:array>, - * status_history:array> - * } - */ - 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 $payload - * @return array> - */ - 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 $payload - * @return array{address:array,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 $payload - * @return array|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 $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 $payload - * @return array|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 $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 $payload - * @param array $productImagesById - * @return array> - */ - 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 $payload - * @return array> - */ - 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 $payload - * @return array> - */ - 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 $payload - * @return array> - */ - 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 $orderPayload - * @param array $cache - * @return array - */ - 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; - } } diff --git a/src/Modules/Settings/ShopproProductImageResolver.php b/src/Modules/Settings/ShopproProductImageResolver.php new file mode 100644 index 0000000..aa0ee61 --- /dev/null +++ b/src/Modules/Settings/ShopproProductImageResolver.php @@ -0,0 +1,145 @@ + $orderPayload + * @param array $cache + * @return array + */ + 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; + } +}