feat(06-sonarqube-quality): split god classes — ShopproOrdersSyncService + AllegroIntegrationController (06-05)
ShopproOrdersSyncService: 39→9 methods via ShopproOrderMapper + ShopproProductImageResolver. AllegroIntegrationController: 35→25 methods via AllegroStatusMappingController + AllegroDeliveryMappingController. S1448 violations: 6x→2x. CronHandlerFactory and routes/web.php updated. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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]);
|
||||
|
||||
@@ -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(
|
||||
|
||||
226
src/Modules/Settings/AllegroDeliveryMappingController.php
Normal file
226
src/Modules/Settings/AllegroDeliveryMappingController.php
Normal file
@@ -0,0 +1,226 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Settings;
|
||||
|
||||
use App\Core\Http\Request;
|
||||
use App\Core\Http\Response;
|
||||
use App\Core\I18n\Translator;
|
||||
use App\Core\Constants\IntegrationSources;
|
||||
use App\Core\Constants\RedirectPaths;
|
||||
use App\Core\Security\Csrf;
|
||||
use App\Core\Support\Flash;
|
||||
use DateInterval;
|
||||
use DateTimeImmutable;
|
||||
use RuntimeException;
|
||||
use Throwable;
|
||||
|
||||
final class AllegroDeliveryMappingController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly Translator $translator,
|
||||
private readonly AllegroIntegrationRepository $repository,
|
||||
private readonly AllegroOAuthClient $oauthClient,
|
||||
private readonly ?AllegroApiClient $apiClient = null,
|
||||
private readonly ?CarrierDeliveryMethodMappingRepository $deliveryMappings = null,
|
||||
private readonly ?ApaczkaIntegrationRepository $apaczkaRepository = null,
|
||||
private readonly ?ApaczkaApiClient $apaczkaApiClient = null
|
||||
) {
|
||||
}
|
||||
|
||||
public function getDeliveryMappingsRepository(): ?CarrierDeliveryMethodMappingRepository
|
||||
{
|
||||
return $this->deliveryMappings;
|
||||
}
|
||||
|
||||
public function saveDeliveryMappings(Request $request): Response
|
||||
{
|
||||
$csrfError = $this->validateCsrf((string) $request->input('_token', ''));
|
||||
if ($csrfError !== null) {
|
||||
return $csrfError;
|
||||
}
|
||||
|
||||
if ($this->deliveryMappings === null) {
|
||||
Flash::set('settings_error', 'Delivery mappings not configured.');
|
||||
return Response::redirect(RedirectPaths::ALLEGRO_DELIVERY_TAB);
|
||||
}
|
||||
|
||||
$orderMethods = (array) $request->input('order_delivery_method', []);
|
||||
$carriers = (array) $request->input('carrier', []);
|
||||
$allegroMethodIds = (array) $request->input('allegro_delivery_method_id', []);
|
||||
$apaczkaMethodIds = (array) $request->input('apaczka_delivery_method_id', []);
|
||||
$credentialsIds = (array) $request->input('allegro_credentials_id', []);
|
||||
$carrierIds = (array) $request->input('allegro_carrier_id', []);
|
||||
$serviceNames = (array) $request->input('allegro_service_name', []);
|
||||
|
||||
$mappings = [];
|
||||
foreach ($orderMethods as $idx => $orderMethod) {
|
||||
$orderMethod = trim((string) $orderMethod);
|
||||
$carrier = trim((string) ($carriers[$idx] ?? 'allegro'));
|
||||
$provider = $carrier === 'apaczka' ? 'apaczka' : 'allegro_wza';
|
||||
$providerServiceId = $provider === 'apaczka'
|
||||
? trim((string) ($apaczkaMethodIds[$idx] ?? ''))
|
||||
: trim((string) ($allegroMethodIds[$idx] ?? ''));
|
||||
if ($orderMethod === '' || $providerServiceId === '') {
|
||||
continue;
|
||||
}
|
||||
$mappings[] = [
|
||||
'order_delivery_method' => $orderMethod,
|
||||
'provider' => $provider,
|
||||
'provider_service_id' => $providerServiceId,
|
||||
'provider_account_id' => $provider === 'allegro_wza' ? trim((string) ($credentialsIds[$idx] ?? '')) : '',
|
||||
'provider_carrier_id' => $provider === 'allegro_wza' ? trim((string) ($carrierIds[$idx] ?? '')) : '',
|
||||
'provider_service_name' => trim((string) ($serviceNames[$idx] ?? '')),
|
||||
];
|
||||
}
|
||||
|
||||
try {
|
||||
$this->deliveryMappings->saveMappings(IntegrationSources::ALLEGRO, 0, $mappings);
|
||||
Flash::set('settings_success', $this->translator->get('settings.allegro.delivery.flash.saved'));
|
||||
} catch (Throwable $exception) {
|
||||
Flash::set('settings_error', $this->translator->get('settings.allegro.delivery.flash.save_failed') . ' ' . $exception->getMessage());
|
||||
}
|
||||
|
||||
return Response::redirect(RedirectPaths::ALLEGRO_DELIVERY_TAB);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $settings
|
||||
* @return array{0: array<int, array<string, mixed>>, 1: array<int, array<string, mixed>>, 2: string}
|
||||
*/
|
||||
public function loadDeliveryServices(array $settings): array
|
||||
{
|
||||
[$allegroServices, $errorMessage] = $this->loadAllegroDeliveryServices($settings);
|
||||
[$apaczkaServices, $apaczkaError] = $this->loadApaczkaServices();
|
||||
|
||||
if ($errorMessage === '' && $apaczkaError !== '') {
|
||||
$errorMessage = $apaczkaError;
|
||||
}
|
||||
|
||||
return [$allegroServices, $apaczkaServices, $errorMessage];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $settings
|
||||
* @return array{0: array<int, array<string, mixed>>, 1: string}
|
||||
*/
|
||||
private function loadAllegroDeliveryServices(array $settings): array
|
||||
{
|
||||
if ($this->apiClient === null) {
|
||||
return [[], ''];
|
||||
}
|
||||
|
||||
$isConnected = (bool) ($settings['is_connected'] ?? false);
|
||||
if (!$isConnected) {
|
||||
return [[], $this->translator->get('settings.allegro.delivery.not_connected')];
|
||||
}
|
||||
|
||||
try {
|
||||
$oauth = $this->repository->getTokenCredentials();
|
||||
if ($oauth === null) {
|
||||
return [[], $this->translator->get('settings.allegro.delivery.not_connected')];
|
||||
}
|
||||
|
||||
$env = (string) ($oauth['environment'] ?? 'sandbox');
|
||||
$accessToken = trim((string) ($oauth['access_token'] ?? ''));
|
||||
if ($accessToken === '') {
|
||||
return [[], $this->translator->get('settings.allegro.delivery.not_connected')];
|
||||
}
|
||||
|
||||
[$response, $fetchError] = $this->fetchAllegroDeliveryResponse($env, $accessToken, $oauth);
|
||||
$services = is_array($response) && is_array($response['services'] ?? null) ? $response['services'] : [];
|
||||
return [$services, $fetchError];
|
||||
} catch (Throwable $e) {
|
||||
return [[], $e->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $oauth
|
||||
* @return array{0: array<string, mixed>, 1: string}
|
||||
*/
|
||||
private function fetchAllegroDeliveryResponse(string $env, string $accessToken, array $oauth): array
|
||||
{
|
||||
try {
|
||||
$response = $this->apiClient->getDeliveryServices($env, $accessToken);
|
||||
return [is_array($response) ? $response : [], ''];
|
||||
} catch (RuntimeException $ex) {
|
||||
if (trim($ex->getMessage()) !== 'ALLEGRO_HTTP_401') {
|
||||
throw $ex;
|
||||
}
|
||||
$refreshed = $this->refreshOAuthToken($oauth);
|
||||
if ($refreshed === null) {
|
||||
return [[], $this->translator->get('settings.allegro.delivery.not_connected')];
|
||||
}
|
||||
$response = $this->apiClient->getDeliveryServices($env, $refreshed);
|
||||
return [is_array($response) ? $response : [], ''];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{0: array<int, array<string, mixed>>, 1: string}
|
||||
*/
|
||||
private function loadApaczkaServices(): array
|
||||
{
|
||||
if ($this->apaczkaRepository === null || $this->apaczkaApiClient === null) {
|
||||
return [[], ''];
|
||||
}
|
||||
|
||||
try {
|
||||
$credentials = $this->apaczkaRepository->getApiCredentials();
|
||||
if (!is_array($credentials)) {
|
||||
return [[], ''];
|
||||
}
|
||||
$services = $this->apaczkaApiClient->getServiceStructure(
|
||||
(string) ($credentials['app_id'] ?? ''),
|
||||
(string) ($credentials['app_secret'] ?? '')
|
||||
);
|
||||
return [$services, ''];
|
||||
} catch (Throwable $exception) {
|
||||
return [[], $exception->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $oauth
|
||||
*/
|
||||
private function refreshOAuthToken(array $oauth): ?string
|
||||
{
|
||||
try {
|
||||
$token = $this->oauthClient->refreshAccessToken(
|
||||
(string) ($oauth['environment'] ?? 'sandbox'),
|
||||
(string) ($oauth['client_id'] ?? ''),
|
||||
(string) ($oauth['client_secret'] ?? ''),
|
||||
(string) ($oauth['refresh_token'] ?? '')
|
||||
);
|
||||
$expiresIn = max(0, (int) ($token['expires_in'] ?? 0));
|
||||
$expiresAt = $expiresIn > 0
|
||||
? (new DateTimeImmutable('now'))->add(new DateInterval('PT' . $expiresIn . 'S'))->format('Y-m-d H:i:s')
|
||||
: null;
|
||||
$refreshToken = trim((string) ($token['refresh_token'] ?? ''));
|
||||
if ($refreshToken === '') {
|
||||
$refreshToken = (string) ($oauth['refresh_token'] ?? '');
|
||||
}
|
||||
$this->repository->saveTokens(
|
||||
(string) ($token['access_token'] ?? ''),
|
||||
$refreshToken,
|
||||
(string) ($token['token_type'] ?? ''),
|
||||
(string) ($token['scope'] ?? ''),
|
||||
$expiresAt
|
||||
);
|
||||
return trim((string) ($token['access_token'] ?? '')) ?: null;
|
||||
} catch (Throwable) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private function validateCsrf(string $token): ?Response
|
||||
{
|
||||
if (Csrf::validate($token)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Flash::set('settings_error', $this->translator->get('auth.errors.csrf_expired'));
|
||||
return Response::redirect(RedirectPaths::ALLEGRO_INTEGRATION);
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,6 @@ use DateTimeImmutable;
|
||||
use App\Core\Constants\IntegrationSources;
|
||||
use App\Core\Constants\RedirectPaths;
|
||||
use App\Core\Exceptions\IntegrationConfigException;
|
||||
use RuntimeException;
|
||||
use Throwable;
|
||||
|
||||
final class AllegroIntegrationController
|
||||
@@ -54,10 +53,7 @@ final class AllegroIntegrationController
|
||||
private readonly AllegroOrderImportService $orderImportService,
|
||||
private readonly AllegroStatusDiscoveryService $statusDiscoveryService,
|
||||
private readonly string $appUrl,
|
||||
private readonly ?CarrierDeliveryMethodMappingRepository $deliveryMappings = null,
|
||||
private readonly ?AllegroApiClient $apiClient = null,
|
||||
private readonly ?ApaczkaIntegrationRepository $apaczkaRepository = null,
|
||||
private readonly ?ApaczkaApiClient $apaczkaApiClient = null
|
||||
private readonly AllegroDeliveryMappingController $deliveryController
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -82,7 +78,8 @@ final class AllegroIntegrationController
|
||||
$importIntervalSeconds = $this->currentImportIntervalSeconds();
|
||||
$statusSyncDirection = $this->currentStatusSyncDirection();
|
||||
$statusSyncIntervalMinutes = $this->currentStatusSyncIntervalMinutes();
|
||||
$deliveryServicesData = $tab === 'delivery' ? $this->loadDeliveryServices($settings) : [[], [], ''];
|
||||
$deliveryServicesData = $tab === 'delivery' ? $this->deliveryController->loadDeliveryServices($settings) : [[], [], ''];
|
||||
$deliveryMappings = $this->deliveryController->getDeliveryMappingsRepository();
|
||||
|
||||
$html = $this->template->render('settings/allegro', [
|
||||
'title' => $this->translator->get('settings.allegro.title'),
|
||||
@@ -101,8 +98,8 @@ final class AllegroIntegrationController
|
||||
'errorMessage' => (string) Flash::get('settings_error', ''),
|
||||
'successMessage' => (string) Flash::get('settings_success', ''),
|
||||
'warningMessage' => (string) Flash::get('settings_warning', ''),
|
||||
'deliveryMappings' => $this->deliveryMappings !== null ? $this->deliveryMappings->listMappings(IntegrationSources::ALLEGRO, 0) : [],
|
||||
'orderDeliveryMethods' => $this->deliveryMappings !== null ? $this->deliveryMappings->getDistinctOrderDeliveryMethods(IntegrationSources::ALLEGRO, 0) : [],
|
||||
'deliveryMappings' => $deliveryMappings !== null ? $deliveryMappings->listMappings(IntegrationSources::ALLEGRO, 0) : [],
|
||||
'orderDeliveryMethods' => $deliveryMappings !== null ? $deliveryMappings->getDistinctOrderDeliveryMethods(IntegrationSources::ALLEGRO, 0) : [],
|
||||
'allegroDeliveryServices' => $deliveryServicesData[0],
|
||||
'apaczkaDeliveryServices' => $deliveryServicesData[1],
|
||||
'allegroDeliveryServicesError' => $deliveryServicesData[2],
|
||||
@@ -235,134 +232,6 @@ final class AllegroIntegrationController
|
||||
return Response::redirect(RedirectPaths::ALLEGRO_SETTINGS_TAB);
|
||||
}
|
||||
|
||||
public function saveStatusMapping(Request $request): Response
|
||||
{
|
||||
$csrfError = $this->validateCsrf((string) $request->input('_token', ''));
|
||||
if ($csrfError !== null) {
|
||||
return $csrfError;
|
||||
}
|
||||
|
||||
$allegroStatusCode = strtolower(trim((string) $request->input('allegro_status_code', '')));
|
||||
$orderproStatusCode = strtolower(trim((string) $request->input('orderpro_status_code', '')));
|
||||
$allegroStatusName = trim((string) $request->input('allegro_status_name', ''));
|
||||
|
||||
if ($allegroStatusCode === '') {
|
||||
Flash::set('settings_error', $this->translator->get('settings.allegro.statuses.flash.allegro_status_required'));
|
||||
return Response::redirect(RedirectPaths::ALLEGRO_STATUSES_TAB);
|
||||
}
|
||||
|
||||
if ($orderproStatusCode === '') {
|
||||
Flash::set('settings_error', $this->translator->get('settings.allegro.statuses.flash.orderpro_status_required'));
|
||||
return Response::redirect(RedirectPaths::ALLEGRO_STATUSES_TAB);
|
||||
}
|
||||
|
||||
if (!$this->orderStatusCodeExists($orderproStatusCode)) {
|
||||
Flash::set('settings_error', $this->translator->get('settings.allegro.statuses.flash.orderpro_status_not_found'));
|
||||
return Response::redirect(RedirectPaths::ALLEGRO_STATUSES_TAB);
|
||||
}
|
||||
|
||||
try {
|
||||
$this->statusMappings->upsertMapping($allegroStatusCode, $allegroStatusName !== '' ? $allegroStatusName : null, $orderproStatusCode);
|
||||
Flash::set('settings_success', $this->translator->get('settings.allegro.statuses.flash.saved'));
|
||||
} catch (Throwable $exception) {
|
||||
Flash::set('settings_error', $this->translator->get('settings.allegro.statuses.flash.save_failed') . ' ' . $exception->getMessage());
|
||||
}
|
||||
|
||||
return Response::redirect(RedirectPaths::ALLEGRO_STATUSES_TAB);
|
||||
}
|
||||
|
||||
public function saveStatusMappingsBulk(Request $request): Response
|
||||
{
|
||||
$csrfError = $this->validateCsrf((string) $request->input('_token', ''));
|
||||
if ($csrfError !== null) {
|
||||
return $csrfError;
|
||||
}
|
||||
|
||||
$codes = $request->input('allegro_status_code', []);
|
||||
$names = $request->input('allegro_status_name', []);
|
||||
$selectedOrderproCodes = $request->input('orderpro_status_code', []);
|
||||
if (!is_array($codes) || !is_array($names) || !is_array($selectedOrderproCodes)) {
|
||||
Flash::set('settings_error', $this->translator->get('settings.allegro.statuses.flash.save_failed'));
|
||||
return Response::redirect(RedirectPaths::ALLEGRO_STATUSES_TAB);
|
||||
}
|
||||
|
||||
try {
|
||||
foreach ($codes as $index => $rawCode) {
|
||||
$allegroStatusCode = strtolower(trim((string) $rawCode));
|
||||
if ($allegroStatusCode === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$allegroStatusName = trim((string) ($names[$index] ?? ''));
|
||||
$orderproStatusCodeRaw = strtolower(trim((string) ($selectedOrderproCodes[$index] ?? '')));
|
||||
$orderproStatusCode = $orderproStatusCodeRaw !== '' ? $orderproStatusCodeRaw : null;
|
||||
|
||||
if ($orderproStatusCode !== null && !$this->orderStatusCodeExists($orderproStatusCode)) {
|
||||
Flash::set('settings_error', $this->translator->get('settings.allegro.statuses.flash.orderpro_status_not_found'));
|
||||
return Response::redirect(RedirectPaths::ALLEGRO_STATUSES_TAB);
|
||||
}
|
||||
|
||||
$this->statusMappings->upsertMapping(
|
||||
$allegroStatusCode,
|
||||
$allegroStatusName !== '' ? $allegroStatusName : null,
|
||||
$orderproStatusCode
|
||||
);
|
||||
}
|
||||
|
||||
Flash::set('settings_success', $this->translator->get('settings.allegro.statuses.flash.saved_bulk'));
|
||||
} catch (Throwable $exception) {
|
||||
Flash::set('settings_error', $this->translator->get('settings.allegro.statuses.flash.save_failed') . ' ' . $exception->getMessage());
|
||||
}
|
||||
|
||||
return Response::redirect(RedirectPaths::ALLEGRO_STATUSES_TAB);
|
||||
}
|
||||
|
||||
public function deleteStatusMapping(Request $request): Response
|
||||
{
|
||||
$csrfError = $this->validateCsrf((string) $request->input('_token', ''));
|
||||
if ($csrfError !== null) {
|
||||
return $csrfError;
|
||||
}
|
||||
|
||||
$mappingId = max(0, (int) $request->input('mapping_id', 0));
|
||||
if ($mappingId <= 0) {
|
||||
Flash::set('settings_error', $this->translator->get('settings.allegro.statuses.flash.mapping_not_found'));
|
||||
return Response::redirect(RedirectPaths::ALLEGRO_STATUSES_TAB);
|
||||
}
|
||||
|
||||
try {
|
||||
$this->statusMappings->deleteMappingById($mappingId);
|
||||
Flash::set('settings_success', $this->translator->get('settings.allegro.statuses.flash.deleted'));
|
||||
} catch (Throwable $exception) {
|
||||
Flash::set('settings_error', $this->translator->get('settings.allegro.statuses.flash.delete_failed') . ' ' . $exception->getMessage());
|
||||
}
|
||||
|
||||
return Response::redirect(RedirectPaths::ALLEGRO_STATUSES_TAB);
|
||||
}
|
||||
|
||||
public function syncStatusesFromAllegro(Request $request): Response
|
||||
{
|
||||
$csrfError = $this->validateCsrf((string) $request->input('_token', ''));
|
||||
if ($csrfError !== null) {
|
||||
return $csrfError;
|
||||
}
|
||||
|
||||
try {
|
||||
$result = $this->statusDiscoveryService->discoverAndStoreStatuses(5, 100);
|
||||
Flash::set(
|
||||
'settings_success',
|
||||
$this->translator->get('settings.allegro.statuses.flash.sync_ok', [
|
||||
'discovered' => (string) ((int) ($result['discovered'] ?? 0)),
|
||||
'samples' => (string) ((int) ($result['samples'] ?? 0)),
|
||||
])
|
||||
);
|
||||
} catch (Throwable $exception) {
|
||||
Flash::set('settings_error', $this->translator->get('settings.allegro.statuses.flash.sync_failed') . ' ' . $exception->getMessage());
|
||||
}
|
||||
|
||||
return Response::redirect(RedirectPaths::ALLEGRO_STATUSES_TAB);
|
||||
}
|
||||
|
||||
public function startOAuth(Request $request): Response
|
||||
{
|
||||
$csrfError = $this->validateCsrf((string) $request->input('_token', ''));
|
||||
@@ -493,187 +362,6 @@ final class AllegroIntegrationController
|
||||
return Response::redirect(RedirectPaths::ALLEGRO_INTEGRATION);
|
||||
}
|
||||
|
||||
public function saveDeliveryMappings(Request $request): Response
|
||||
{
|
||||
$csrfError = $this->validateCsrf((string) $request->input('_token', ''));
|
||||
if ($csrfError !== null) {
|
||||
return $csrfError;
|
||||
}
|
||||
|
||||
if ($this->deliveryMappings === null) {
|
||||
Flash::set('settings_error', 'Delivery mappings not configured.');
|
||||
return Response::redirect(RedirectPaths::ALLEGRO_DELIVERY_TAB);
|
||||
}
|
||||
|
||||
$orderMethods = (array) $request->input('order_delivery_method', []);
|
||||
$carriers = (array) $request->input('carrier', []);
|
||||
$allegroMethodIds = (array) $request->input('allegro_delivery_method_id', []);
|
||||
$apaczkaMethodIds = (array) $request->input('apaczka_delivery_method_id', []);
|
||||
$credentialsIds = (array) $request->input('allegro_credentials_id', []);
|
||||
$carrierIds = (array) $request->input('allegro_carrier_id', []);
|
||||
$serviceNames = (array) $request->input('allegro_service_name', []);
|
||||
|
||||
$mappings = [];
|
||||
foreach ($orderMethods as $idx => $orderMethod) {
|
||||
$orderMethod = trim((string) $orderMethod);
|
||||
$carrier = trim((string) ($carriers[$idx] ?? 'allegro'));
|
||||
$provider = $carrier === 'apaczka' ? 'apaczka' : 'allegro_wza';
|
||||
$providerServiceId = $provider === 'apaczka'
|
||||
? trim((string) ($apaczkaMethodIds[$idx] ?? ''))
|
||||
: trim((string) ($allegroMethodIds[$idx] ?? ''));
|
||||
if ($orderMethod === '' || $providerServiceId === '') {
|
||||
continue;
|
||||
}
|
||||
$mappings[] = [
|
||||
'order_delivery_method' => $orderMethod,
|
||||
'provider' => $provider,
|
||||
'provider_service_id' => $providerServiceId,
|
||||
'provider_account_id' => $provider === 'allegro_wza' ? trim((string) ($credentialsIds[$idx] ?? '')) : '',
|
||||
'provider_carrier_id' => $provider === 'allegro_wza' ? trim((string) ($carrierIds[$idx] ?? '')) : '',
|
||||
'provider_service_name' => trim((string) ($serviceNames[$idx] ?? '')),
|
||||
];
|
||||
}
|
||||
|
||||
try {
|
||||
$this->deliveryMappings->saveMappings(IntegrationSources::ALLEGRO, 0, $mappings);
|
||||
Flash::set('settings_success', $this->translator->get('settings.allegro.delivery.flash.saved'));
|
||||
} catch (Throwable $exception) {
|
||||
Flash::set('settings_error', $this->translator->get('settings.allegro.delivery.flash.save_failed') . ' ' . $exception->getMessage());
|
||||
}
|
||||
|
||||
return Response::redirect(RedirectPaths::ALLEGRO_DELIVERY_TAB);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $settings
|
||||
* @return array{0: array<int, array<string, mixed>>, 1: array<int, array<string, mixed>>, 2: string}
|
||||
*/
|
||||
private function loadDeliveryServices(array $settings): array
|
||||
{
|
||||
[$allegroServices, $errorMessage] = $this->loadAllegroDeliveryServices($settings);
|
||||
[$apaczkaServices, $apaczkaError] = $this->loadApaczkaServices();
|
||||
|
||||
if ($errorMessage === '' && $apaczkaError !== '') {
|
||||
$errorMessage = $apaczkaError;
|
||||
}
|
||||
|
||||
return [$allegroServices, $apaczkaServices, $errorMessage];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $settings
|
||||
* @return array{0: array<int, array<string, mixed>>, 1: string}
|
||||
*/
|
||||
private function loadAllegroDeliveryServices(array $settings): array
|
||||
{
|
||||
if ($this->apiClient === null) {
|
||||
return [[], ''];
|
||||
}
|
||||
|
||||
$isConnected = (bool) ($settings['is_connected'] ?? false);
|
||||
if (!$isConnected) {
|
||||
return [[], $this->translator->get('settings.allegro.delivery.not_connected')];
|
||||
}
|
||||
|
||||
try {
|
||||
$oauth = $this->repository->getTokenCredentials();
|
||||
if ($oauth === null) {
|
||||
return [[], $this->translator->get('settings.allegro.delivery.not_connected')];
|
||||
}
|
||||
|
||||
$env = (string) ($oauth['environment'] ?? 'sandbox');
|
||||
$accessToken = trim((string) ($oauth['access_token'] ?? ''));
|
||||
if ($accessToken === '') {
|
||||
return [[], $this->translator->get('settings.allegro.delivery.not_connected')];
|
||||
}
|
||||
|
||||
[$response, $fetchError] = $this->fetchAllegroDeliveryResponse($env, $accessToken, $oauth);
|
||||
$services = is_array($response) && is_array($response['services'] ?? null) ? $response['services'] : [];
|
||||
return [$services, $fetchError];
|
||||
} catch (Throwable $e) {
|
||||
return [[], $e->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $oauth
|
||||
* @return array{0: array<string, mixed>, 1: string}
|
||||
*/
|
||||
private function fetchAllegroDeliveryResponse(string $env, string $accessToken, array $oauth): array
|
||||
{
|
||||
try {
|
||||
$response = $this->apiClient->getDeliveryServices($env, $accessToken);
|
||||
return [is_array($response) ? $response : [], ''];
|
||||
} catch (RuntimeException $ex) {
|
||||
if (trim($ex->getMessage()) !== 'ALLEGRO_HTTP_401') {
|
||||
throw $ex;
|
||||
}
|
||||
$refreshed = $this->refreshOAuthToken($oauth);
|
||||
if ($refreshed === null) {
|
||||
return [[], $this->translator->get('settings.allegro.delivery.not_connected')];
|
||||
}
|
||||
$response = $this->apiClient->getDeliveryServices($env, $refreshed);
|
||||
return [is_array($response) ? $response : [], ''];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{0: array<int, array<string, mixed>>, 1: string}
|
||||
*/
|
||||
private function loadApaczkaServices(): array
|
||||
{
|
||||
if ($this->apaczkaRepository === null || $this->apaczkaApiClient === null) {
|
||||
return [[], ''];
|
||||
}
|
||||
|
||||
try {
|
||||
$credentials = $this->apaczkaRepository->getApiCredentials();
|
||||
if (!is_array($credentials)) {
|
||||
return [[], ''];
|
||||
}
|
||||
$services = $this->apaczkaApiClient->getServiceStructure(
|
||||
(string) ($credentials['app_id'] ?? ''),
|
||||
(string) ($credentials['app_secret'] ?? '')
|
||||
);
|
||||
return [$services, ''];
|
||||
} catch (Throwable $exception) {
|
||||
return [[], $exception->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $oauth
|
||||
*/
|
||||
private function refreshOAuthToken(array $oauth): ?string
|
||||
{
|
||||
try {
|
||||
$token = $this->oauthClient->refreshAccessToken(
|
||||
(string) ($oauth['environment'] ?? 'sandbox'),
|
||||
(string) ($oauth['client_id'] ?? ''),
|
||||
(string) ($oauth['client_secret'] ?? ''),
|
||||
(string) ($oauth['refresh_token'] ?? '')
|
||||
);
|
||||
$expiresIn = max(0, (int) ($token['expires_in'] ?? 0));
|
||||
$expiresAt = $expiresIn > 0
|
||||
? (new DateTimeImmutable('now'))->add(new DateInterval('PT' . $expiresIn . 'S'))->format('Y-m-d H:i:s')
|
||||
: null;
|
||||
$refreshToken = trim((string) ($token['refresh_token'] ?? ''));
|
||||
if ($refreshToken === '') {
|
||||
$refreshToken = (string) ($oauth['refresh_token'] ?? '');
|
||||
}
|
||||
$this->repository->saveTokens(
|
||||
(string) ($token['access_token'] ?? ''),
|
||||
$refreshToken,
|
||||
(string) ($token['token_type'] ?? ''),
|
||||
(string) ($token['scope'] ?? ''),
|
||||
$expiresAt
|
||||
);
|
||||
return trim((string) ($token['access_token'] ?? '')) ?: null;
|
||||
} catch (Throwable) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private function defaultRedirectUri(): string
|
||||
{
|
||||
$base = trim($this->appUrl);
|
||||
@@ -745,23 +433,6 @@ final class AllegroIntegrationController
|
||||
return $date instanceof DateTimeImmutable && $date->format('Y-m-d') === $value;
|
||||
}
|
||||
|
||||
private function orderStatusCodeExists(string $code): bool
|
||||
{
|
||||
$needle = strtolower(trim($code));
|
||||
if ($needle === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($this->orderStatuses->listStatuses() as $row) {
|
||||
$statusCode = strtolower(trim((string) ($row['code'] ?? '')));
|
||||
if ($statusCode === $needle) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $imageDiagnostics
|
||||
*/
|
||||
|
||||
178
src/Modules/Settings/AllegroStatusMappingController.php
Normal file
178
src/Modules/Settings/AllegroStatusMappingController.php
Normal file
@@ -0,0 +1,178 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Settings;
|
||||
|
||||
use App\Core\Http\Request;
|
||||
use App\Core\Http\Response;
|
||||
use App\Core\I18n\Translator;
|
||||
use App\Core\Constants\RedirectPaths;
|
||||
use App\Core\Security\Csrf;
|
||||
use App\Core\Support\Flash;
|
||||
use Throwable;
|
||||
|
||||
final class AllegroStatusMappingController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly Translator $translator,
|
||||
private readonly AllegroStatusMappingRepository $statusMappings,
|
||||
private readonly OrderStatusRepository $orderStatuses,
|
||||
private readonly AllegroStatusDiscoveryService $statusDiscoveryService
|
||||
) {
|
||||
}
|
||||
|
||||
public function saveStatusMapping(Request $request): Response
|
||||
{
|
||||
$csrfError = $this->validateCsrf((string) $request->input('_token', ''));
|
||||
if ($csrfError !== null) {
|
||||
return $csrfError;
|
||||
}
|
||||
|
||||
$allegroStatusCode = strtolower(trim((string) $request->input('allegro_status_code', '')));
|
||||
$orderproStatusCode = strtolower(trim((string) $request->input('orderpro_status_code', '')));
|
||||
$allegroStatusName = trim((string) $request->input('allegro_status_name', ''));
|
||||
|
||||
if ($allegroStatusCode === '') {
|
||||
Flash::set('settings_error', $this->translator->get('settings.allegro.statuses.flash.allegro_status_required'));
|
||||
return Response::redirect(RedirectPaths::ALLEGRO_STATUSES_TAB);
|
||||
}
|
||||
|
||||
if ($orderproStatusCode === '') {
|
||||
Flash::set('settings_error', $this->translator->get('settings.allegro.statuses.flash.orderpro_status_required'));
|
||||
return Response::redirect(RedirectPaths::ALLEGRO_STATUSES_TAB);
|
||||
}
|
||||
|
||||
if (!$this->orderStatusCodeExists($orderproStatusCode)) {
|
||||
Flash::set('settings_error', $this->translator->get('settings.allegro.statuses.flash.orderpro_status_not_found'));
|
||||
return Response::redirect(RedirectPaths::ALLEGRO_STATUSES_TAB);
|
||||
}
|
||||
|
||||
try {
|
||||
$this->statusMappings->upsertMapping($allegroStatusCode, $allegroStatusName !== '' ? $allegroStatusName : null, $orderproStatusCode);
|
||||
Flash::set('settings_success', $this->translator->get('settings.allegro.statuses.flash.saved'));
|
||||
} catch (Throwable $exception) {
|
||||
Flash::set('settings_error', $this->translator->get('settings.allegro.statuses.flash.save_failed') . ' ' . $exception->getMessage());
|
||||
}
|
||||
|
||||
return Response::redirect(RedirectPaths::ALLEGRO_STATUSES_TAB);
|
||||
}
|
||||
|
||||
public function saveStatusMappingsBulk(Request $request): Response
|
||||
{
|
||||
$csrfError = $this->validateCsrf((string) $request->input('_token', ''));
|
||||
if ($csrfError !== null) {
|
||||
return $csrfError;
|
||||
}
|
||||
|
||||
$codes = $request->input('allegro_status_code', []);
|
||||
$names = $request->input('allegro_status_name', []);
|
||||
$selectedOrderproCodes = $request->input('orderpro_status_code', []);
|
||||
if (!is_array($codes) || !is_array($names) || !is_array($selectedOrderproCodes)) {
|
||||
Flash::set('settings_error', $this->translator->get('settings.allegro.statuses.flash.save_failed'));
|
||||
return Response::redirect(RedirectPaths::ALLEGRO_STATUSES_TAB);
|
||||
}
|
||||
|
||||
try {
|
||||
foreach ($codes as $index => $rawCode) {
|
||||
$allegroStatusCode = strtolower(trim((string) $rawCode));
|
||||
if ($allegroStatusCode === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$allegroStatusName = trim((string) ($names[$index] ?? ''));
|
||||
$orderproStatusCodeRaw = strtolower(trim((string) ($selectedOrderproCodes[$index] ?? '')));
|
||||
$orderproStatusCode = $orderproStatusCodeRaw !== '' ? $orderproStatusCodeRaw : null;
|
||||
|
||||
if ($orderproStatusCode !== null && !$this->orderStatusCodeExists($orderproStatusCode)) {
|
||||
Flash::set('settings_error', $this->translator->get('settings.allegro.statuses.flash.orderpro_status_not_found'));
|
||||
return Response::redirect(RedirectPaths::ALLEGRO_STATUSES_TAB);
|
||||
}
|
||||
|
||||
$this->statusMappings->upsertMapping(
|
||||
$allegroStatusCode,
|
||||
$allegroStatusName !== '' ? $allegroStatusName : null,
|
||||
$orderproStatusCode
|
||||
);
|
||||
}
|
||||
|
||||
Flash::set('settings_success', $this->translator->get('settings.allegro.statuses.flash.saved_bulk'));
|
||||
} catch (Throwable $exception) {
|
||||
Flash::set('settings_error', $this->translator->get('settings.allegro.statuses.flash.save_failed') . ' ' . $exception->getMessage());
|
||||
}
|
||||
|
||||
return Response::redirect(RedirectPaths::ALLEGRO_STATUSES_TAB);
|
||||
}
|
||||
|
||||
public function deleteStatusMapping(Request $request): Response
|
||||
{
|
||||
$csrfError = $this->validateCsrf((string) $request->input('_token', ''));
|
||||
if ($csrfError !== null) {
|
||||
return $csrfError;
|
||||
}
|
||||
|
||||
$mappingId = max(0, (int) $request->input('mapping_id', 0));
|
||||
if ($mappingId <= 0) {
|
||||
Flash::set('settings_error', $this->translator->get('settings.allegro.statuses.flash.mapping_not_found'));
|
||||
return Response::redirect(RedirectPaths::ALLEGRO_STATUSES_TAB);
|
||||
}
|
||||
|
||||
try {
|
||||
$this->statusMappings->deleteMappingById($mappingId);
|
||||
Flash::set('settings_success', $this->translator->get('settings.allegro.statuses.flash.deleted'));
|
||||
} catch (Throwable $exception) {
|
||||
Flash::set('settings_error', $this->translator->get('settings.allegro.statuses.flash.delete_failed') . ' ' . $exception->getMessage());
|
||||
}
|
||||
|
||||
return Response::redirect(RedirectPaths::ALLEGRO_STATUSES_TAB);
|
||||
}
|
||||
|
||||
public function syncStatusesFromAllegro(Request $request): Response
|
||||
{
|
||||
$csrfError = $this->validateCsrf((string) $request->input('_token', ''));
|
||||
if ($csrfError !== null) {
|
||||
return $csrfError;
|
||||
}
|
||||
|
||||
try {
|
||||
$result = $this->statusDiscoveryService->discoverAndStoreStatuses(5, 100);
|
||||
Flash::set(
|
||||
'settings_success',
|
||||
$this->translator->get('settings.allegro.statuses.flash.sync_ok', [
|
||||
'discovered' => (string) ((int) ($result['discovered'] ?? 0)),
|
||||
'samples' => (string) ((int) ($result['samples'] ?? 0)),
|
||||
])
|
||||
);
|
||||
} catch (Throwable $exception) {
|
||||
Flash::set('settings_error', $this->translator->get('settings.allegro.statuses.flash.sync_failed') . ' ' . $exception->getMessage());
|
||||
}
|
||||
|
||||
return Response::redirect(RedirectPaths::ALLEGRO_STATUSES_TAB);
|
||||
}
|
||||
|
||||
private function orderStatusCodeExists(string $code): bool
|
||||
{
|
||||
$needle = strtolower(trim($code));
|
||||
if ($needle === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($this->orderStatuses->listStatuses() as $row) {
|
||||
$statusCode = strtolower(trim((string) ($row['code'] ?? '')));
|
||||
if ($statusCode === $needle) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function validateCsrf(string $token): ?Response
|
||||
{
|
||||
if (Csrf::validate($token)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Flash::set('settings_error', $this->translator->get('auth.errors.csrf_expired'));
|
||||
return Response::redirect(RedirectPaths::ALLEGRO_INTEGRATION);
|
||||
}
|
||||
}
|
||||
824
src/Modules/Settings/ShopproOrderMapper.php
Normal file
824
src/Modules/Settings/ShopproOrderMapper.php
Normal file
@@ -0,0 +1,824 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Settings;
|
||||
|
||||
use App\Core\Constants\IntegrationSources;
|
||||
use App\Core\Support\StringHelper;
|
||||
|
||||
final class ShopproOrderMapper
|
||||
{
|
||||
/**
|
||||
* @param array<int, array<string, mixed>> $items
|
||||
* @return array<int, array{source_order_id:string,source_updated_at:string,payload:array<string,mixed>}>
|
||||
*/
|
||||
public function buildCandidates(array $items, ?string $cursorUpdatedAt, ?string $cursorOrderId): array
|
||||
{
|
||||
$result = [];
|
||||
foreach ($items as $row) {
|
||||
$sourceOrderId = $this->normalizeOrderId($this->readPath($row, ['id', 'order_id', 'external_order_id']));
|
||||
$sourceUpdatedAt = StringHelper::normalizeDateTime((string) $this->readPath($row, ['updated_at', 'date_updated', 'modified_at', 'date_modified', 'created_at', 'date_created']));
|
||||
if ($sourceOrderId === '' || $sourceUpdatedAt === null) {
|
||||
continue;
|
||||
}
|
||||
if (!$this->isAfterCursor($sourceUpdatedAt, $sourceOrderId, $cursorUpdatedAt, $cursorOrderId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$result[] = [
|
||||
'source_order_id' => $sourceOrderId,
|
||||
'source_updated_at' => $sourceUpdatedAt,
|
||||
'payload' => $row,
|
||||
];
|
||||
}
|
||||
|
||||
usort($result, static function (array $a, array $b): int {
|
||||
$cmp = strcmp((string) ($a['source_updated_at'] ?? ''), (string) ($b['source_updated_at'] ?? ''));
|
||||
if ($cmp !== 0) {
|
||||
return $cmp;
|
||||
}
|
||||
|
||||
return strcmp((string) ($a['source_order_id'] ?? ''), (string) ($b['source_order_id'] ?? ''));
|
||||
});
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $payload
|
||||
* @param array<string, string> $statusMap
|
||||
* @param array<int, string> $productImagesById
|
||||
* @return array{
|
||||
* order:array<string,mixed>,
|
||||
* addresses:array<int,array<string,mixed>>,
|
||||
* items:array<int,array<string,mixed>>,
|
||||
* payments:array<int,array<string,mixed>>,
|
||||
* shipments:array<int,array<string,mixed>>,
|
||||
* notes:array<int,array<string,mixed>>,
|
||||
* status_history:array<int,array<string,mixed>>
|
||||
* }
|
||||
*/
|
||||
public function mapOrderAggregate(
|
||||
int $integrationId,
|
||||
array $payload,
|
||||
array $statusMap,
|
||||
string $fallbackOrderId,
|
||||
string $fallbackUpdatedAt,
|
||||
array $productImagesById = []
|
||||
): array {
|
||||
$sourceOrderId = $this->normalizeOrderId($this->readPath($payload, ['id', 'order_id', 'external_order_id']));
|
||||
if ($sourceOrderId === '') {
|
||||
$sourceOrderId = $fallbackOrderId;
|
||||
}
|
||||
|
||||
$sourceCreatedAt = StringHelper::normalizeDateTime((string) $this->readPath($payload, ['created_at', 'date_created', 'date_add']));
|
||||
$sourceUpdatedAt = StringHelper::normalizeDateTime((string) $this->readPath($payload, ['updated_at', 'date_updated', 'modified_at', 'date_modified', 'created_at']));
|
||||
if ($sourceUpdatedAt === null) {
|
||||
$sourceUpdatedAt = $fallbackUpdatedAt !== '' ? $fallbackUpdatedAt : date('Y-m-d H:i:s');
|
||||
}
|
||||
|
||||
$originalStatus = strtolower(trim((string) $this->readPath($payload, ['status', 'status_code', 'order_status'])));
|
||||
$effectiveStatus = $statusMap[$originalStatus] ?? $originalStatus;
|
||||
if ($effectiveStatus === '') {
|
||||
$effectiveStatus = 'new';
|
||||
}
|
||||
|
||||
$currency = trim((string) $this->readPath($payload, ['currency', 'totals.currency']));
|
||||
if ($currency === '') {
|
||||
$currency = 'PLN';
|
||||
}
|
||||
|
||||
$totalGross = $this->toFloatOrNull($this->readPath($payload, [
|
||||
'total_gross', 'total_with_tax', 'summary.total', 'totals.gross', 'summary', 'amount',
|
||||
]));
|
||||
$transportCost = $this->toFloatOrNull($this->readPath($payload, ['transport_cost', 'delivery_cost', 'shipping.cost']));
|
||||
if ($totalGross === null && $transportCost !== null) {
|
||||
$productsSum = 0.0;
|
||||
$hasProducts = false;
|
||||
$rawItemsForSummary = $this->readPath($payload, ['products', 'items', 'order_items']);
|
||||
if (is_array($rawItemsForSummary)) {
|
||||
foreach ($rawItemsForSummary as $rawItem) {
|
||||
if (!is_array($rawItem)) {
|
||||
continue;
|
||||
}
|
||||
$itemPrice = $this->toFloatOrNull($this->readPath($rawItem, [
|
||||
'price_brutto', 'price_gross', 'gross_price', 'price',
|
||||
]));
|
||||
$itemQty = $this->toFloatOrDefault($this->readPath($rawItem, ['quantity', 'qty']), 1.0);
|
||||
if ($itemPrice === null) {
|
||||
continue;
|
||||
}
|
||||
$hasProducts = true;
|
||||
$productsSum += ($itemPrice * $itemQty);
|
||||
}
|
||||
}
|
||||
if ($hasProducts) {
|
||||
$totalGross = $productsSum + $transportCost;
|
||||
}
|
||||
}
|
||||
$totalNet = $this->toFloatOrNull($this->readPath($payload, ['total_net', 'total_without_tax', 'totals.net']));
|
||||
$totalPaid = $this->toFloatOrNull($this->readPath($payload, ['total_paid', 'payments.total_paid', 'payment.total', 'paid_amount']));
|
||||
$paidFlag = $this->readPath($payload, ['paid', 'is_paid']);
|
||||
$isPaid = $this->normalizePaidFlag($paidFlag);
|
||||
if ($totalPaid === null) {
|
||||
if ($isPaid && $totalGross !== null) {
|
||||
$totalPaid = $totalGross;
|
||||
}
|
||||
}
|
||||
|
||||
$deliveryLabel = $this->buildDeliveryMethodLabel($payload);
|
||||
|
||||
$order = [
|
||||
'integration_id' => $integrationId,
|
||||
'source' => IntegrationSources::SHOPPRO,
|
||||
'source_order_id' => $sourceOrderId,
|
||||
'external_order_id' => $sourceOrderId,
|
||||
'external_platform_id' => IntegrationSources::SHOPPRO,
|
||||
'external_platform_account_id' => null,
|
||||
'external_status_id' => $effectiveStatus,
|
||||
'external_payment_type_id' => StringHelper::nullableString((string) $this->readPath($payload, ['payment_method', 'payment.method', 'payments.method'])),
|
||||
'payment_status' => $this->mapPaymentStatus($payload, $isPaid),
|
||||
'external_carrier_id' => StringHelper::nullableString($deliveryLabel),
|
||||
'external_carrier_account_id' => StringHelper::nullableString((string) $this->readPath($payload, [
|
||||
'transport_id', 'shipping.method_id', 'delivery.method_id',
|
||||
])),
|
||||
'customer_login' => StringHelper::nullableString((string) $this->readPath($payload, [
|
||||
'buyer_email', 'customer.email', 'buyer.email', 'client.email', 'email', 'customer.login', 'buyer.login',
|
||||
])),
|
||||
'is_invoice' => $this->resolveInvoiceRequested($payload),
|
||||
'is_encrypted' => false,
|
||||
'is_canceled_by_buyer' => false,
|
||||
'currency' => $currency,
|
||||
'total_without_tax' => $totalNet,
|
||||
'total_with_tax' => $totalGross,
|
||||
'total_paid' => $totalPaid,
|
||||
'send_date_min' => null,
|
||||
'send_date_max' => null,
|
||||
'ordered_at' => $sourceCreatedAt,
|
||||
'source_created_at' => $sourceCreatedAt,
|
||||
'source_updated_at' => $sourceUpdatedAt,
|
||||
'preferences_json' => null,
|
||||
'payload_json' => $payload,
|
||||
'fetched_at' => date('Y-m-d H:i:s'),
|
||||
];
|
||||
|
||||
$addresses = $this->mapAddresses($payload);
|
||||
$items = $this->mapItems($payload, $productImagesById);
|
||||
$payments = $this->mapPayments($payload, $currency, $totalPaid);
|
||||
$shipments = $this->mapShipments($payload);
|
||||
$notes = $this->mapNotes($payload);
|
||||
$statusHistory = [[
|
||||
'from_status_id' => null,
|
||||
'to_status_id' => $effectiveStatus,
|
||||
'changed_at' => $sourceUpdatedAt,
|
||||
'change_source' => 'import',
|
||||
'comment' => $originalStatus !== '' ? 'shopPRO status: ' . $originalStatus : null,
|
||||
'payload_json' => null,
|
||||
]];
|
||||
|
||||
return [
|
||||
'order' => $order,
|
||||
'addresses' => $addresses,
|
||||
'items' => $items,
|
||||
'payments' => $payments,
|
||||
'shipments' => $shipments,
|
||||
'notes' => $notes,
|
||||
'status_history' => $statusHistory,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $payload
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
private function mapAddresses(array $payload): array
|
||||
{
|
||||
$result = [];
|
||||
|
||||
$customerData = $this->buildCustomerAddress($payload);
|
||||
$result[] = $customerData['address'];
|
||||
|
||||
$invoiceAddress = $this->buildInvoiceAddress(
|
||||
$payload,
|
||||
$customerData['name'],
|
||||
$customerData['email'],
|
||||
$customerData['phone']
|
||||
);
|
||||
if ($invoiceAddress !== null) {
|
||||
$result[] = $invoiceAddress;
|
||||
}
|
||||
|
||||
$deliveryAddress = $this->buildDeliveryAddress(
|
||||
$payload,
|
||||
$customerData['name'],
|
||||
$customerData['email'],
|
||||
$customerData['phone']
|
||||
);
|
||||
if ($deliveryAddress !== null) {
|
||||
$result[] = $deliveryAddress;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $payload
|
||||
* @return array{address:array<string,mixed>,name:?string,email:?string,phone:?string}
|
||||
*/
|
||||
private function buildCustomerAddress(array $payload): array
|
||||
{
|
||||
$customerFirstName = StringHelper::nullableString((string) $this->readPath($payload, [
|
||||
'buyer.first_name', 'buyer.firstname', 'customer.first_name', 'customer.firstname',
|
||||
'client.first_name', 'client.firstname', 'billing_address.first_name', 'billing_address.firstname',
|
||||
'first_name', 'firstname', 'client_name', 'imie',
|
||||
]));
|
||||
$customerLastName = StringHelper::nullableString((string) $this->readPath($payload, [
|
||||
'buyer.last_name', 'buyer.lastname', 'customer.last_name', 'customer.lastname',
|
||||
'client.last_name', 'client.lastname', 'billing_address.last_name', 'billing_address.lastname',
|
||||
'last_name', 'lastname', 'client_surname', 'nazwisko',
|
||||
]));
|
||||
$customerName = StringHelper::nullableString((string) $this->readPath($payload, [
|
||||
'buyer_name', 'buyer.name', 'customer.name', 'client.name', 'billing_address.name',
|
||||
'receiver.name', 'client', 'customer_full_name', 'client_full_name',
|
||||
])) ?? $this->composeName($customerFirstName, $customerLastName, 'Klient');
|
||||
|
||||
$customerEmail = StringHelper::nullableString((string) $this->readPath($payload, [
|
||||
'buyer_email', 'buyer.email', 'customer.email', 'client.email', 'billing_address.email',
|
||||
'shipping_address.email', 'delivery_address.email', 'email', 'client_email', 'mail',
|
||||
]));
|
||||
$customerPhone = StringHelper::nullableString((string) $this->readPath($payload, [
|
||||
'buyer_phone', 'buyer.phone', 'customer.phone', 'client.phone', 'billing_address.phone',
|
||||
'shipping_address.phone', 'delivery_address.phone', 'phone', 'telephone', 'client_phone',
|
||||
'phone_number', 'client_phone_number',
|
||||
]));
|
||||
|
||||
$address = [
|
||||
'address_type' => 'customer',
|
||||
'name' => $customerName ?? 'Klient',
|
||||
'phone' => $customerPhone,
|
||||
'email' => $customerEmail,
|
||||
'street_name' => StringHelper::nullableString((string) $this->readPath($payload, [
|
||||
'buyer_address.street', 'customer.address.street', 'billing_address.street', 'client.address.street',
|
||||
'address.street', 'street', 'client_street', 'ulica',
|
||||
])),
|
||||
'street_number' => StringHelper::nullableString((string) $this->readPath($payload, [
|
||||
'buyer_address.street_number', 'customer.address.street_number', 'billing_address.street_number',
|
||||
'billing_address.house_number', 'client.address.street_number', 'address.street_number',
|
||||
'house_number', 'street_no', 'street_number', 'nr_domu',
|
||||
])),
|
||||
'city' => StringHelper::nullableString((string) $this->readPath($payload, [
|
||||
'buyer_address.city', 'customer.address.city', 'billing_address.city', 'client.address.city',
|
||||
'address.city', 'city', 'client_city', 'miejscowosc',
|
||||
])),
|
||||
'zip_code' => StringHelper::nullableString((string) $this->readPath($payload, [
|
||||
'buyer_address.zip', 'buyer_address.postcode', 'customer.address.zip', 'customer.address.postcode',
|
||||
'billing_address.zip', 'billing_address.postcode', 'client.address.zip', 'address.zip',
|
||||
'address.postcode', 'zip', 'postcode', 'client_postal_code', 'kod_pocztowy',
|
||||
])),
|
||||
'country' => StringHelper::nullableString((string) $this->readPath($payload, [
|
||||
'buyer_address.country', 'customer.address.country', 'billing_address.country', 'client.address.country',
|
||||
'address.country', 'country', 'kraj',
|
||||
])),
|
||||
'payload_json' => [
|
||||
'buyer' => $this->readPath($payload, ['buyer']),
|
||||
'customer' => $this->readPath($payload, ['customer']),
|
||||
'billing_address' => $this->readPath($payload, ['billing_address']),
|
||||
'buyer_address' => $this->readPath($payload, ['buyer_address']),
|
||||
'address' => $this->readPath($payload, ['address']),
|
||||
],
|
||||
];
|
||||
|
||||
return ['address' => $address, 'name' => $customerName, 'email' => $customerEmail, 'phone' => $customerPhone];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $payload
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
private function buildDeliveryAddress(array $payload, ?string $customerName, ?string $customerEmail, ?string $customerPhone): ?array
|
||||
{
|
||||
$deliveryFirstName = StringHelper::nullableString((string) $this->readPath($payload, [
|
||||
'delivery.address.first_name', 'delivery.address.firstname', 'shipping.address.first_name', 'shipping.address.firstname',
|
||||
'delivery_address.first_name', 'delivery_address.firstname', 'shipping_address.first_name', 'shipping_address.firstname',
|
||||
'receiver.first_name', 'receiver.firstname', 'delivery_first_name', 'shipping_first_name',
|
||||
]));
|
||||
$deliveryLastName = StringHelper::nullableString((string) $this->readPath($payload, [
|
||||
'delivery.address.last_name', 'delivery.address.lastname', 'shipping.address.last_name', 'shipping.address.lastname',
|
||||
'delivery_address.last_name', 'delivery_address.lastname', 'shipping_address.last_name', 'shipping_address.lastname',
|
||||
'receiver.last_name', 'receiver.lastname', 'delivery_last_name', 'shipping_last_name',
|
||||
]));
|
||||
$deliveryName = StringHelper::nullableString((string) $this->readPath($payload, [
|
||||
'delivery.address.name', 'shipping.address.name', 'delivery_address.name', 'shipping_address.name',
|
||||
'receiver.name', 'delivery_name', 'shipping_name',
|
||||
])) ?? $this->composeName($deliveryFirstName, $deliveryLastName, null);
|
||||
|
||||
$pickupData = $this->parsePickupPoint((string) $this->readPath($payload, ['inpost_paczkomat', 'orlen_point', 'pickup_point']));
|
||||
$fields = [
|
||||
'name' => $deliveryName ?? StringHelper::nullableString($this->buildDeliveryMethodLabel($payload)),
|
||||
'phone' => StringHelper::nullableString((string) $this->readPath($payload, [
|
||||
'delivery.address.phone', 'shipping.address.phone', 'delivery_address.phone', 'shipping_address.phone',
|
||||
'receiver.phone', 'delivery_phone', 'shipping_phone',
|
||||
])) ?? $customerPhone,
|
||||
'email' => StringHelper::nullableString((string) $this->readPath($payload, [
|
||||
'delivery.address.email', 'shipping.address.email', 'delivery_address.email', 'shipping_address.email',
|
||||
'receiver.email', 'delivery_email', 'shipping_email',
|
||||
])) ?? $customerEmail,
|
||||
'street_name' => StringHelper::nullableString((string) $this->readPath($payload, [
|
||||
'delivery.address.street', 'shipping.address.street', 'delivery_address.street', 'shipping_address.street',
|
||||
'receiver.address.street', 'delivery_street', 'shipping_street',
|
||||
])) ?? StringHelper::nullableString($pickupData['street'] ?? ''),
|
||||
'street_number' => StringHelper::nullableString((string) $this->readPath($payload, [
|
||||
'delivery.address.street_number', 'shipping.address.street_number', 'delivery_address.street_number', 'shipping_address.street_number',
|
||||
'delivery.address.house_number', 'shipping.address.house_number', 'receiver.address.street_number',
|
||||
'receiver.address.house_number', 'delivery_street_number', 'shipping_street_number',
|
||||
])),
|
||||
'city' => StringHelper::nullableString((string) $this->readPath($payload, [
|
||||
'delivery.address.city', 'shipping.address.city', 'delivery_address.city', 'shipping_address.city',
|
||||
'receiver.address.city', 'delivery_city', 'shipping_city',
|
||||
])) ?? StringHelper::nullableString($pickupData['city'] ?? ''),
|
||||
'zip_code' => StringHelper::nullableString((string) $this->readPath($payload, [
|
||||
'delivery.address.zip', 'delivery.address.postcode', 'shipping.address.zip', 'shipping.address.postcode',
|
||||
'delivery_address.zip', 'delivery_address.postcode', 'shipping_address.zip', 'shipping_address.postcode',
|
||||
'receiver.address.zip', 'receiver.address.postcode', 'delivery_zip', 'delivery_postcode',
|
||||
'shipping_zip', 'shipping_postcode',
|
||||
])) ?? StringHelper::nullableString($pickupData['zip_code'] ?? ''),
|
||||
'country' => StringHelper::nullableString((string) $this->readPath($payload, [
|
||||
'delivery.address.country', 'shipping.address.country', 'delivery_address.country', 'shipping_address.country',
|
||||
'receiver.address.country', 'delivery_country', 'shipping_country',
|
||||
])),
|
||||
'parcel_external_id' => StringHelper::nullableString($pickupData['code'] ?? ''),
|
||||
'parcel_name' => StringHelper::nullableString($pickupData['label'] ?? ''),
|
||||
'payload_json' => [
|
||||
'delivery' => $this->readPath($payload, ['delivery']),
|
||||
'shipping' => $this->readPath($payload, ['shipping']),
|
||||
'delivery_address' => $this->readPath($payload, ['delivery_address']),
|
||||
'shipping_address' => $this->readPath($payload, ['shipping_address']),
|
||||
'receiver' => $this->readPath($payload, ['receiver']),
|
||||
'inpost_paczkomat' => $this->readPath($payload, ['inpost_paczkomat']),
|
||||
'orlen_point' => $this->readPath($payload, ['orlen_point']),
|
||||
],
|
||||
];
|
||||
|
||||
if (!$this->hasAddressData($fields)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'address_type' => 'delivery',
|
||||
'name' => $fields['name'] ?? $customerName ?? 'Dostawa',
|
||||
'phone' => $fields['phone'] ?? null,
|
||||
'email' => $fields['email'] ?? $customerEmail,
|
||||
'street_name' => $fields['street_name'] ?? null,
|
||||
'street_number' => $fields['street_number'] ?? null,
|
||||
'city' => $fields['city'] ?? null,
|
||||
'zip_code' => $fields['zip_code'] ?? null,
|
||||
'country' => $fields['country'] ?? null,
|
||||
'parcel_external_id' => $fields['parcel_external_id'] ?? null,
|
||||
'parcel_name' => $fields['parcel_name'] ?? null,
|
||||
'payload_json' => is_array($fields['payload_json'] ?? null) ? $fields['payload_json'] : null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $payload
|
||||
*/
|
||||
private function resolveInvoiceRequested(array $payload): bool
|
||||
{
|
||||
$explicitInvoice = $this->readPath($payload, ['is_invoice', 'invoice.required', 'invoice']);
|
||||
if (!empty($explicitInvoice)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$companyName = StringHelper::nullableString((string) $this->readPath($payload, [
|
||||
'invoice.company_name', 'invoice.company', 'billing.company_name', 'billing.company',
|
||||
'firm_name', 'company_name', 'client_company', 'buyer_company',
|
||||
]));
|
||||
$taxNumber = StringHelper::nullableString((string) $this->readPath($payload, [
|
||||
'invoice.tax_id', 'invoice.nip', 'billing.tax_id', 'billing.nip',
|
||||
'firm_nip', 'company_nip', 'tax_id', 'nip',
|
||||
]));
|
||||
|
||||
return $companyName !== null || $taxNumber !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $payload
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
private function buildInvoiceAddress(
|
||||
array $payload,
|
||||
?string $customerName,
|
||||
?string $customerEmail,
|
||||
?string $customerPhone
|
||||
): ?array {
|
||||
$companyName = StringHelper::nullableString((string) $this->readPath($payload, [
|
||||
'invoice.company_name', 'invoice.company', 'billing.company_name', 'billing.company',
|
||||
'firm_name', 'company_name', 'client_company', 'buyer_company',
|
||||
]));
|
||||
$companyTaxNumber = StringHelper::nullableString((string) $this->readPath($payload, [
|
||||
'invoice.tax_id', 'invoice.nip', 'billing.tax_id', 'billing.nip',
|
||||
'firm_nip', 'company_nip', 'tax_id', 'nip',
|
||||
]));
|
||||
$invoiceFirstName = StringHelper::nullableString((string) $this->readPath($payload, [
|
||||
'invoice.first_name', 'invoice.firstname', 'billing_address.first_name', 'billing_address.firstname',
|
||||
'buyer.first_name', 'customer.first_name', 'client_name',
|
||||
]));
|
||||
$invoiceLastName = StringHelper::nullableString((string) $this->readPath($payload, [
|
||||
'invoice.last_name', 'invoice.lastname', 'billing_address.last_name', 'billing_address.lastname',
|
||||
'buyer.last_name', 'customer.last_name', 'client_surname',
|
||||
]));
|
||||
$invoiceName = $companyName ?? $this->composeName($invoiceFirstName, $invoiceLastName, $customerName ?? 'Faktura');
|
||||
|
||||
$streetName = StringHelper::nullableString((string) $this->readPath($payload, [
|
||||
'invoice.address.street', 'invoice.street', 'billing_address.street', 'billing.street',
|
||||
'firm_street', 'company_street',
|
||||
]));
|
||||
$streetNumber = StringHelper::nullableString((string) $this->readPath($payload, [
|
||||
'invoice.address.street_number', 'invoice.street_number', 'invoice.house_number',
|
||||
'billing_address.street_number', 'billing_address.house_number',
|
||||
'billing.street_number', 'house_number', 'street_number',
|
||||
]));
|
||||
$city = StringHelper::nullableString((string) $this->readPath($payload, [
|
||||
'invoice.address.city', 'invoice.city', 'billing_address.city', 'billing.city',
|
||||
'firm_city', 'company_city',
|
||||
]));
|
||||
$zipCode = StringHelper::nullableString((string) $this->readPath($payload, [
|
||||
'invoice.address.zip', 'invoice.address.postcode', 'invoice.zip', 'invoice.postcode',
|
||||
'billing_address.zip', 'billing_address.postcode', 'billing.zip', 'billing.postcode',
|
||||
'firm_postal_code', 'company_postal_code',
|
||||
]));
|
||||
$country = StringHelper::nullableString((string) $this->readPath($payload, [
|
||||
'invoice.address.country', 'invoice.country', 'billing_address.country', 'billing.country',
|
||||
'firm_country', 'company_country',
|
||||
]));
|
||||
$email = StringHelper::nullableString((string) $this->readPath($payload, [
|
||||
'invoice.email', 'billing_address.email', 'billing.email', 'client_email',
|
||||
])) ?? $customerEmail;
|
||||
$phone = StringHelper::nullableString((string) $this->readPath($payload, [
|
||||
'invoice.phone', 'billing_address.phone', 'billing.phone', 'client_phone',
|
||||
])) ?? $customerPhone;
|
||||
|
||||
$hasInvoiceData = $companyName !== null
|
||||
|| $companyTaxNumber !== null
|
||||
|| $streetName !== null
|
||||
|| $city !== null
|
||||
|| $zipCode !== null;
|
||||
if (!$hasInvoiceData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'address_type' => 'invoice',
|
||||
'name' => $invoiceName ?? 'Faktura',
|
||||
'phone' => $phone,
|
||||
'email' => $email,
|
||||
'street_name' => $streetName,
|
||||
'street_number' => $streetNumber,
|
||||
'city' => $city,
|
||||
'zip_code' => $zipCode,
|
||||
'country' => $country,
|
||||
'company_tax_number' => $companyTaxNumber,
|
||||
'company_name' => $companyName,
|
||||
'payload_json' => [
|
||||
'invoice' => $this->readPath($payload, ['invoice']),
|
||||
'billing' => $this->readPath($payload, ['billing']),
|
||||
'billing_address' => $this->readPath($payload, ['billing_address']),
|
||||
'firm_name' => $this->readPath($payload, ['firm_name']),
|
||||
'firm_nip' => $this->readPath($payload, ['firm_nip']),
|
||||
'firm_street' => $this->readPath($payload, ['firm_street']),
|
||||
'firm_postal_code' => $this->readPath($payload, ['firm_postal_code']),
|
||||
'firm_city' => $this->readPath($payload, ['firm_city']),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
private function composeName(?string $firstName, ?string $lastName, ?string $fallback): ?string
|
||||
{
|
||||
$name = trim(trim((string) $firstName) . ' ' . trim((string) $lastName));
|
||||
if ($name !== '') {
|
||||
return $name;
|
||||
}
|
||||
|
||||
$fallbackValue = trim((string) $fallback);
|
||||
return $fallbackValue !== '' ? $fallbackValue : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $address
|
||||
*/
|
||||
private function hasAddressData(array $address): bool
|
||||
{
|
||||
$fields = ['name', 'phone', 'email', 'street_name', 'street_number', 'city', 'zip_code', 'country'];
|
||||
foreach ($fields as $field) {
|
||||
$value = trim((string) ($address[$field] ?? ''));
|
||||
if ($value !== '') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $payload
|
||||
* @param array<int, string> $productImagesById
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
private function mapItems(array $payload, array $productImagesById = []): array
|
||||
{
|
||||
$rawItems = $this->readPath($payload, ['items']);
|
||||
if (!is_array($rawItems)) {
|
||||
$rawItems = $this->readPath($payload, ['order_items']);
|
||||
}
|
||||
if (!is_array($rawItems)) {
|
||||
$rawItems = $this->readPath($payload, ['products']);
|
||||
}
|
||||
if (!is_array($rawItems)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$result = [];
|
||||
$sort = 0;
|
||||
foreach ($rawItems as $row) {
|
||||
if (!is_array($row)) {
|
||||
continue;
|
||||
}
|
||||
$name = trim((string) $this->readPath($row, ['name', 'title']));
|
||||
if ($name === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$productId = (int) $this->readPath($row, ['product_id']);
|
||||
$parentProductId = (int) $this->readPath($row, ['parent_product_id']);
|
||||
$mediaUrl = StringHelper::nullableString((string) $this->readPath($row, ['image', 'image_url', 'img_url', 'img', 'photo', 'photo_url']));
|
||||
if ($mediaUrl === null && $productId > 0 && isset($productImagesById[$productId])) {
|
||||
$mediaUrl = StringHelper::nullableString((string) $productImagesById[$productId]);
|
||||
}
|
||||
if ($mediaUrl === null && $parentProductId > 0 && isset($productImagesById[$parentProductId])) {
|
||||
$mediaUrl = StringHelper::nullableString((string) $productImagesById[$parentProductId]);
|
||||
}
|
||||
|
||||
$result[] = [
|
||||
'source_item_id' => StringHelper::nullableString((string) $this->readPath($row, ['id', 'item_id'])),
|
||||
'external_item_id' => StringHelper::nullableString((string) $this->readPath($row, ['id', 'item_id'])),
|
||||
'ean' => StringHelper::nullableString((string) $this->readPath($row, ['ean'])),
|
||||
'sku' => StringHelper::nullableString((string) $this->readPath($row, ['sku', 'symbol', 'code'])),
|
||||
'original_name' => $name,
|
||||
'original_code' => StringHelper::nullableString((string) $this->readPath($row, ['code', 'symbol'])),
|
||||
'original_price_with_tax' => $this->toFloatOrNull($this->readPath($row, ['price_gross', 'gross_price', 'price', 'price_brutto'])),
|
||||
'original_price_without_tax' => $this->toFloatOrNull($this->readPath($row, ['price_net', 'net_price', 'price_netto'])),
|
||||
'media_url' => $mediaUrl,
|
||||
'quantity' => $this->toFloatOrDefault($this->readPath($row, ['quantity', 'qty']), 1.0),
|
||||
'tax_rate' => $this->toFloatOrNull($this->readPath($row, ['vat', 'tax_rate'])),
|
||||
'item_status' => null,
|
||||
'unit' => StringHelper::nullableString((string) $this->readPath($row, ['unit'])),
|
||||
'item_type' => 'product',
|
||||
'source_product_id' => StringHelper::nullableString((string) ($productId > 0 ? $productId : $parentProductId)),
|
||||
'source_product_set_id' => StringHelper::nullableString((string) ($parentProductId > 0 ? $parentProductId : '')),
|
||||
'sort_order' => $sort++,
|
||||
'payload_json' => $row,
|
||||
];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $payload
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
private function mapPayments(array $payload, string $currency, ?float $totalPaid): array
|
||||
{
|
||||
$paymentMethod = StringHelper::nullableString((string) $this->readPath($payload, ['payment_method', 'payment.method']));
|
||||
if ($paymentMethod === null && $totalPaid === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [[
|
||||
'source_payment_id' => null,
|
||||
'external_payment_id' => null,
|
||||
'payment_type_id' => $paymentMethod ?? 'unknown',
|
||||
'payment_date' => StringHelper::nullableString((string) $this->readPath($payload, ['payment_date', 'payment.date'])),
|
||||
'amount' => $totalPaid,
|
||||
'currency' => $currency,
|
||||
'comment' => StringHelper::nullableString((string) $this->readPath($payload, ['payment_status', 'payment.status'])),
|
||||
'payload_json' => null,
|
||||
]];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $payload
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
private function mapShipments(array $payload): array
|
||||
{
|
||||
$tracking = StringHelper::nullableString((string) $this->readPath($payload, ['delivery_tracking_number', 'delivery.tracking_number', 'shipping.tracking_number']));
|
||||
if ($tracking === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [[
|
||||
'source_shipment_id' => null,
|
||||
'external_shipment_id' => null,
|
||||
'tracking_number' => $tracking,
|
||||
'carrier_provider_id' => $this->sanitizePlainText((string) ($this->readPath($payload, [
|
||||
'delivery_method', 'shipping.method', 'transport', 'transport_description',
|
||||
]) ?? 'unknown')),
|
||||
'posted_at' => StringHelper::nullableString((string) $this->readPath($payload, ['delivery.posted_at', 'shipping.posted_at'])),
|
||||
'media_uuid' => null,
|
||||
'payload_json' => null,
|
||||
]];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $payload
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
private function mapNotes(array $payload): array
|
||||
{
|
||||
$comment = StringHelper::nullableString((string) $this->readPath($payload, ['notes', 'comment', 'customer_comment']));
|
||||
if ($comment === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [[
|
||||
'source_note_id' => null,
|
||||
'note_type' => 'message',
|
||||
'created_at_external' => null,
|
||||
'comment' => $comment,
|
||||
'payload_json' => null,
|
||||
]];
|
||||
}
|
||||
|
||||
private function normalizeOrderId(mixed $value): string
|
||||
{
|
||||
return trim((string) $value);
|
||||
}
|
||||
|
||||
private function normalizePaidFlag(mixed $value): bool
|
||||
{
|
||||
if ($value === true) {
|
||||
return true;
|
||||
}
|
||||
if ($value === false || $value === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$normalized = strtolower(trim((string) $value));
|
||||
return in_array($normalized, ['1', 'true', 'yes', 'paid'], true);
|
||||
}
|
||||
|
||||
private function mapPaymentStatus(array $payload, bool $isPaid): ?int
|
||||
{
|
||||
if ($isPaid) {
|
||||
return 2;
|
||||
}
|
||||
|
||||
$raw = strtolower(trim((string) $this->readPath($payload, ['payment_status', 'payment.status'])));
|
||||
if ($raw === '') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return match ($raw) {
|
||||
'paid', 'finished', 'completed' => 2,
|
||||
'partially_paid', 'in_progress' => 1,
|
||||
'cancelled', 'canceled', 'failed', 'unpaid', 'not_paid' => 0,
|
||||
default => 0,
|
||||
};
|
||||
}
|
||||
|
||||
private function sanitizePlainText(string $value): string
|
||||
{
|
||||
$withoutTags = strip_tags($value);
|
||||
$decoded = html_entity_decode($withoutTags, ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
||||
return trim(preg_replace('/\s+/', ' ', $decoded) ?? '');
|
||||
}
|
||||
|
||||
private function buildDeliveryMethodLabel(array $payload): string
|
||||
{
|
||||
$label = $this->sanitizePlainText((string) $this->readPath($payload, [
|
||||
'delivery_method', 'shipping.method', 'delivery.method', 'shipping_method', 'delivery_name', 'shipping_name',
|
||||
'transport', 'transport_description',
|
||||
]));
|
||||
$cost = $this->toFloatOrNull($this->readPath($payload, ['transport_cost', 'delivery_cost', 'shipping.cost']));
|
||||
if ($label !== '' && $cost !== null) {
|
||||
$label .= ': ' . $this->formatMoneyCompact($cost) . ' zł';
|
||||
}
|
||||
|
||||
return $label;
|
||||
}
|
||||
|
||||
private function formatMoneyCompact(float $amount): string
|
||||
{
|
||||
$formatted = number_format($amount, 2, '.', '');
|
||||
return rtrim(rtrim($formatted, '0'), '.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{code:string,label:string,street:string,zip_code:string,city:string}
|
||||
*/
|
||||
private function parsePickupPoint(string $raw): array
|
||||
{
|
||||
$value = trim($this->sanitizePlainText($raw));
|
||||
if ($value === '') {
|
||||
return ['code' => '', 'label' => '', 'street' => '', 'zip_code' => '', 'city' => ''];
|
||||
}
|
||||
|
||||
$code = '';
|
||||
$address = $value;
|
||||
$parts = preg_split('/\s*\|\s*/', $value);
|
||||
if (is_array($parts) && count($parts) >= 2) {
|
||||
$code = trim((string) ($parts[0] ?? ''));
|
||||
$address = trim((string) ($parts[1] ?? ''));
|
||||
}
|
||||
|
||||
$street = '';
|
||||
$zip = '';
|
||||
$city = '';
|
||||
if (preg_match('/^(.*?),\s*(\d{2}-\d{3})\s+(.+)$/u', $address, $matches) === 1) {
|
||||
$street = trim((string) ($matches[1] ?? ''));
|
||||
$zip = trim((string) ($matches[2] ?? ''));
|
||||
$city = trim((string) ($matches[3] ?? ''));
|
||||
} elseif (preg_match('/(\d{2}-\d{3})\s+(.+)$/u', $address, $matches) === 1) {
|
||||
$zip = trim((string) ($matches[1] ?? ''));
|
||||
$city = trim((string) ($matches[2] ?? ''));
|
||||
}
|
||||
|
||||
return [
|
||||
'code' => $code,
|
||||
'label' => $value,
|
||||
'street' => $street,
|
||||
'zip_code' => $zip,
|
||||
'city' => $city,
|
||||
];
|
||||
}
|
||||
|
||||
private function isAfterCursor(string $sourceUpdatedAt, string $sourceOrderId, ?string $cursorUpdatedAt, ?string $cursorOrderId): bool
|
||||
{
|
||||
if ($cursorUpdatedAt === null || $cursorUpdatedAt === '') {
|
||||
return true;
|
||||
}
|
||||
if ($sourceUpdatedAt > $cursorUpdatedAt) {
|
||||
return true;
|
||||
}
|
||||
if ($sourceUpdatedAt < $cursorUpdatedAt) {
|
||||
return false;
|
||||
}
|
||||
if ($cursorOrderId === null || $cursorOrderId === '') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return strcmp($sourceOrderId, $cursorOrderId) > 0;
|
||||
}
|
||||
|
||||
private function toFloatOrNull(mixed $value): ?float
|
||||
{
|
||||
if ($value === null || $value === '') {
|
||||
return null;
|
||||
}
|
||||
if (is_string($value)) {
|
||||
$value = str_replace(',', '.', trim($value));
|
||||
}
|
||||
if (!is_numeric($value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (float) $value;
|
||||
}
|
||||
|
||||
private function toFloatOrDefault(mixed $value, float $default): float
|
||||
{
|
||||
$result = $this->toFloatOrNull($value);
|
||||
return $result ?? $default;
|
||||
}
|
||||
|
||||
private function readPath(mixed $payload, array $paths): mixed
|
||||
{
|
||||
foreach ($paths as $path) {
|
||||
$value = $this->readSinglePath($payload, (string) $path);
|
||||
if ($value !== null && $value !== '') {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function readSinglePath(mixed $payload, string $path): mixed
|
||||
{
|
||||
if ($path === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$segments = explode('.', $path);
|
||||
$current = $payload;
|
||||
foreach ($segments as $segment) {
|
||||
if (!is_array($current) || !array_key_exists($segment, $current)) {
|
||||
return null;
|
||||
}
|
||||
$current = $current[$segment];
|
||||
}
|
||||
|
||||
return $current;
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Settings;
|
||||
|
||||
use App\Core\Constants\IntegrationSources;
|
||||
use App\Core\Support\StringHelper;
|
||||
use App\Modules\Orders\OrderImportRepository;
|
||||
use App\Modules\Orders\OrdersRepository;
|
||||
@@ -18,7 +17,9 @@ final class ShopproOrdersSyncService
|
||||
private readonly ShopproApiClient $apiClient,
|
||||
private readonly OrderImportRepository $orderImportRepository,
|
||||
private readonly ShopproStatusMappingRepository $statusMappings,
|
||||
private readonly OrdersRepository $orders
|
||||
private readonly OrdersRepository $orders,
|
||||
private readonly ShopproOrderMapper $mapper,
|
||||
private readonly ShopproProductImageResolver $imageResolver
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -102,7 +103,7 @@ final class ShopproOrdersSyncService
|
||||
if ($items === []) {
|
||||
break;
|
||||
}
|
||||
$candidates = $this->buildCandidates($items, $cursorUpdatedAt, $cursorOrderId);
|
||||
$candidates = $this->mapper->buildCandidates($items, $cursorUpdatedAt, $cursorOrderId);
|
||||
$this->processPageCandidates(
|
||||
$candidates, $integrationId, $baseUrl, (string) $apiKey, $timeout,
|
||||
$statusMap, $maxOrders, $result, $productImageCache, $shouldStop,
|
||||
@@ -214,10 +215,10 @@ final class ShopproOrdersSyncService
|
||||
array &$productImageCache
|
||||
): void {
|
||||
try {
|
||||
$productImages = $this->resolveProductImagesForOrder(
|
||||
$productImages = $this->imageResolver->resolveProductImagesForOrder(
|
||||
$baseUrl, $apiKey, $timeout, $rawOrder, $productImageCache
|
||||
);
|
||||
$aggregate = $this->mapOrderAggregate(
|
||||
$aggregate = $this->mapper->mapOrderAggregate(
|
||||
$integrationId, $rawOrder, $statusMap, $sourceOrderId, $sourceUpdatedAt, $productImages
|
||||
);
|
||||
$save = $this->orderImportRepository->upsertOrderAggregate(
|
||||
@@ -315,920 +316,4 @@ final class ShopproOrdersSyncService
|
||||
return $cursor > $settings ? $cursor : $settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array<string, mixed>> $items
|
||||
* @return array<int, array{source_order_id:string,source_updated_at:string,payload:array<string,mixed>}>
|
||||
*/
|
||||
private function buildCandidates(array $items, ?string $cursorUpdatedAt, ?string $cursorOrderId): array
|
||||
{
|
||||
$result = [];
|
||||
foreach ($items as $row) {
|
||||
$sourceOrderId = $this->normalizeOrderId($this->readPath($row, ['id', 'order_id', 'external_order_id']));
|
||||
$sourceUpdatedAt = StringHelper::normalizeDateTime((string) $this->readPath($row, ['updated_at', 'date_updated', 'modified_at', 'date_modified', 'created_at', 'date_created']));
|
||||
if ($sourceOrderId === '' || $sourceUpdatedAt === null) {
|
||||
continue;
|
||||
}
|
||||
if (!$this->isAfterCursor($sourceUpdatedAt, $sourceOrderId, $cursorUpdatedAt, $cursorOrderId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$result[] = [
|
||||
'source_order_id' => $sourceOrderId,
|
||||
'source_updated_at' => $sourceUpdatedAt,
|
||||
'payload' => $row,
|
||||
];
|
||||
}
|
||||
|
||||
usort($result, static function (array $a, array $b): int {
|
||||
$cmp = strcmp((string) ($a['source_updated_at'] ?? ''), (string) ($b['source_updated_at'] ?? ''));
|
||||
if ($cmp !== 0) {
|
||||
return $cmp;
|
||||
}
|
||||
|
||||
return strcmp((string) ($a['source_order_id'] ?? ''), (string) ($b['source_order_id'] ?? ''));
|
||||
});
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function isAfterCursor(string $sourceUpdatedAt, string $sourceOrderId, ?string $cursorUpdatedAt, ?string $cursorOrderId): bool
|
||||
{
|
||||
if ($cursorUpdatedAt === null || $cursorUpdatedAt === '') {
|
||||
return true;
|
||||
}
|
||||
if ($sourceUpdatedAt > $cursorUpdatedAt) {
|
||||
return true;
|
||||
}
|
||||
if ($sourceUpdatedAt < $cursorUpdatedAt) {
|
||||
return false;
|
||||
}
|
||||
if ($cursorOrderId === null || $cursorOrderId === '') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return strcmp($sourceOrderId, $cursorOrderId) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $payload
|
||||
* @param array<string, string> $statusMap
|
||||
* @param array<int, string> $productImagesById
|
||||
* @return array{
|
||||
* order:array<string,mixed>,
|
||||
* addresses:array<int,array<string,mixed>>,
|
||||
* items:array<int,array<string,mixed>>,
|
||||
* payments:array<int,array<string,mixed>>,
|
||||
* shipments:array<int,array<string,mixed>>,
|
||||
* notes:array<int,array<string,mixed>>,
|
||||
* status_history:array<int,array<string,mixed>>
|
||||
* }
|
||||
*/
|
||||
private function mapOrderAggregate(
|
||||
int $integrationId,
|
||||
array $payload,
|
||||
array $statusMap,
|
||||
string $fallbackOrderId,
|
||||
string $fallbackUpdatedAt,
|
||||
array $productImagesById = []
|
||||
): array {
|
||||
$sourceOrderId = $this->normalizeOrderId($this->readPath($payload, ['id', 'order_id', 'external_order_id']));
|
||||
if ($sourceOrderId === '') {
|
||||
$sourceOrderId = $fallbackOrderId;
|
||||
}
|
||||
|
||||
$sourceCreatedAt = StringHelper::normalizeDateTime((string) $this->readPath($payload, ['created_at', 'date_created', 'date_add']));
|
||||
$sourceUpdatedAt = StringHelper::normalizeDateTime((string) $this->readPath($payload, ['updated_at', 'date_updated', 'modified_at', 'date_modified', 'created_at']));
|
||||
if ($sourceUpdatedAt === null) {
|
||||
$sourceUpdatedAt = $fallbackUpdatedAt !== '' ? $fallbackUpdatedAt : date('Y-m-d H:i:s');
|
||||
}
|
||||
|
||||
$originalStatus = strtolower(trim((string) $this->readPath($payload, ['status', 'status_code', 'order_status'])));
|
||||
$effectiveStatus = $statusMap[$originalStatus] ?? $originalStatus;
|
||||
if ($effectiveStatus === '') {
|
||||
$effectiveStatus = 'new';
|
||||
}
|
||||
|
||||
$currency = trim((string) $this->readPath($payload, ['currency', 'totals.currency']));
|
||||
if ($currency === '') {
|
||||
$currency = 'PLN';
|
||||
}
|
||||
|
||||
$totalGross = $this->toFloatOrNull($this->readPath($payload, [
|
||||
'total_gross', 'total_with_tax', 'summary.total', 'totals.gross', 'summary', 'amount',
|
||||
]));
|
||||
$transportCost = $this->toFloatOrNull($this->readPath($payload, ['transport_cost', 'delivery_cost', 'shipping.cost']));
|
||||
if ($totalGross === null && $transportCost !== null) {
|
||||
$productsSum = 0.0;
|
||||
$hasProducts = false;
|
||||
$rawItemsForSummary = $this->readPath($payload, ['products', 'items', 'order_items']);
|
||||
if (is_array($rawItemsForSummary)) {
|
||||
foreach ($rawItemsForSummary as $rawItem) {
|
||||
if (!is_array($rawItem)) {
|
||||
continue;
|
||||
}
|
||||
$itemPrice = $this->toFloatOrNull($this->readPath($rawItem, [
|
||||
'price_brutto', 'price_gross', 'gross_price', 'price',
|
||||
]));
|
||||
$itemQty = $this->toFloatOrDefault($this->readPath($rawItem, ['quantity', 'qty']), 1.0);
|
||||
if ($itemPrice === null) {
|
||||
continue;
|
||||
}
|
||||
$hasProducts = true;
|
||||
$productsSum += ($itemPrice * $itemQty);
|
||||
}
|
||||
}
|
||||
if ($hasProducts) {
|
||||
$totalGross = $productsSum + $transportCost;
|
||||
}
|
||||
}
|
||||
$totalNet = $this->toFloatOrNull($this->readPath($payload, ['total_net', 'total_without_tax', 'totals.net']));
|
||||
$totalPaid = $this->toFloatOrNull($this->readPath($payload, ['total_paid', 'payments.total_paid', 'payment.total', 'paid_amount']));
|
||||
$paidFlag = $this->readPath($payload, ['paid', 'is_paid']);
|
||||
$isPaid = $this->normalizePaidFlag($paidFlag);
|
||||
if ($totalPaid === null) {
|
||||
if ($isPaid && $totalGross !== null) {
|
||||
$totalPaid = $totalGross;
|
||||
}
|
||||
}
|
||||
|
||||
$deliveryLabel = $this->buildDeliveryMethodLabel($payload);
|
||||
|
||||
$order = [
|
||||
'integration_id' => $integrationId,
|
||||
'source' => IntegrationSources::SHOPPRO,
|
||||
'source_order_id' => $sourceOrderId,
|
||||
'external_order_id' => $sourceOrderId,
|
||||
'external_platform_id' => IntegrationSources::SHOPPRO,
|
||||
'external_platform_account_id' => null,
|
||||
'external_status_id' => $effectiveStatus,
|
||||
'external_payment_type_id' => StringHelper::nullableString((string) $this->readPath($payload, ['payment_method', 'payment.method', 'payments.method'])),
|
||||
'payment_status' => $this->mapPaymentStatus($payload, $isPaid),
|
||||
'external_carrier_id' => StringHelper::nullableString($deliveryLabel),
|
||||
'external_carrier_account_id' => StringHelper::nullableString((string) $this->readPath($payload, [
|
||||
'transport_id', 'shipping.method_id', 'delivery.method_id',
|
||||
])),
|
||||
'customer_login' => StringHelper::nullableString((string) $this->readPath($payload, [
|
||||
'buyer_email', 'customer.email', 'buyer.email', 'client.email', 'email', 'customer.login', 'buyer.login',
|
||||
])),
|
||||
'is_invoice' => $this->resolveInvoiceRequested($payload),
|
||||
'is_encrypted' => false,
|
||||
'is_canceled_by_buyer' => false,
|
||||
'currency' => $currency,
|
||||
'total_without_tax' => $totalNet,
|
||||
'total_with_tax' => $totalGross,
|
||||
'total_paid' => $totalPaid,
|
||||
'send_date_min' => null,
|
||||
'send_date_max' => null,
|
||||
'ordered_at' => $sourceCreatedAt,
|
||||
'source_created_at' => $sourceCreatedAt,
|
||||
'source_updated_at' => $sourceUpdatedAt,
|
||||
'preferences_json' => null,
|
||||
'payload_json' => $payload,
|
||||
'fetched_at' => date('Y-m-d H:i:s'),
|
||||
];
|
||||
|
||||
$addresses = $this->mapAddresses($payload);
|
||||
$items = $this->mapItems($payload, $productImagesById);
|
||||
$payments = $this->mapPayments($payload, $currency, $totalPaid);
|
||||
$shipments = $this->mapShipments($payload);
|
||||
$notes = $this->mapNotes($payload);
|
||||
$statusHistory = [[
|
||||
'from_status_id' => null,
|
||||
'to_status_id' => $effectiveStatus,
|
||||
'changed_at' => $sourceUpdatedAt,
|
||||
'change_source' => 'import',
|
||||
'comment' => $originalStatus !== '' ? 'shopPRO status: ' . $originalStatus : null,
|
||||
'payload_json' => null,
|
||||
]];
|
||||
|
||||
return [
|
||||
'order' => $order,
|
||||
'addresses' => $addresses,
|
||||
'items' => $items,
|
||||
'payments' => $payments,
|
||||
'shipments' => $shipments,
|
||||
'notes' => $notes,
|
||||
'status_history' => $statusHistory,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $payload
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
private function mapAddresses(array $payload): array
|
||||
{
|
||||
$result = [];
|
||||
|
||||
$customerData = $this->buildCustomerAddress($payload);
|
||||
$result[] = $customerData['address'];
|
||||
|
||||
$invoiceAddress = $this->buildInvoiceAddress(
|
||||
$payload,
|
||||
$customerData['name'],
|
||||
$customerData['email'],
|
||||
$customerData['phone']
|
||||
);
|
||||
if ($invoiceAddress !== null) {
|
||||
$result[] = $invoiceAddress;
|
||||
}
|
||||
|
||||
$deliveryAddress = $this->buildDeliveryAddress(
|
||||
$payload,
|
||||
$customerData['name'],
|
||||
$customerData['email'],
|
||||
$customerData['phone']
|
||||
);
|
||||
if ($deliveryAddress !== null) {
|
||||
$result[] = $deliveryAddress;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $payload
|
||||
* @return array{address:array<string,mixed>,name:?string,email:?string,phone:?string}
|
||||
*/
|
||||
private function buildCustomerAddress(array $payload): array
|
||||
{
|
||||
$customerFirstName = StringHelper::nullableString((string) $this->readPath($payload, [
|
||||
'buyer.first_name', 'buyer.firstname', 'customer.first_name', 'customer.firstname',
|
||||
'client.first_name', 'client.firstname', 'billing_address.first_name', 'billing_address.firstname',
|
||||
'first_name', 'firstname', 'client_name', 'imie',
|
||||
]));
|
||||
$customerLastName = StringHelper::nullableString((string) $this->readPath($payload, [
|
||||
'buyer.last_name', 'buyer.lastname', 'customer.last_name', 'customer.lastname',
|
||||
'client.last_name', 'client.lastname', 'billing_address.last_name', 'billing_address.lastname',
|
||||
'last_name', 'lastname', 'client_surname', 'nazwisko',
|
||||
]));
|
||||
$customerName = StringHelper::nullableString((string) $this->readPath($payload, [
|
||||
'buyer_name', 'buyer.name', 'customer.name', 'client.name', 'billing_address.name',
|
||||
'receiver.name', 'client', 'customer_full_name', 'client_full_name',
|
||||
])) ?? $this->composeName($customerFirstName, $customerLastName, 'Klient');
|
||||
|
||||
$customerEmail = StringHelper::nullableString((string) $this->readPath($payload, [
|
||||
'buyer_email', 'buyer.email', 'customer.email', 'client.email', 'billing_address.email',
|
||||
'shipping_address.email', 'delivery_address.email', 'email', 'client_email', 'mail',
|
||||
]));
|
||||
$customerPhone = StringHelper::nullableString((string) $this->readPath($payload, [
|
||||
'buyer_phone', 'buyer.phone', 'customer.phone', 'client.phone', 'billing_address.phone',
|
||||
'shipping_address.phone', 'delivery_address.phone', 'phone', 'telephone', 'client_phone',
|
||||
'phone_number', 'client_phone_number',
|
||||
]));
|
||||
|
||||
$address = [
|
||||
'address_type' => 'customer',
|
||||
'name' => $customerName ?? 'Klient',
|
||||
'phone' => $customerPhone,
|
||||
'email' => $customerEmail,
|
||||
'street_name' => StringHelper::nullableString((string) $this->readPath($payload, [
|
||||
'buyer_address.street', 'customer.address.street', 'billing_address.street', 'client.address.street',
|
||||
'address.street', 'street', 'client_street', 'ulica',
|
||||
])),
|
||||
'street_number' => StringHelper::nullableString((string) $this->readPath($payload, [
|
||||
'buyer_address.street_number', 'customer.address.street_number', 'billing_address.street_number',
|
||||
'billing_address.house_number', 'client.address.street_number', 'address.street_number',
|
||||
'house_number', 'street_no', 'street_number', 'nr_domu',
|
||||
])),
|
||||
'city' => StringHelper::nullableString((string) $this->readPath($payload, [
|
||||
'buyer_address.city', 'customer.address.city', 'billing_address.city', 'client.address.city',
|
||||
'address.city', 'city', 'client_city', 'miejscowosc',
|
||||
])),
|
||||
'zip_code' => StringHelper::nullableString((string) $this->readPath($payload, [
|
||||
'buyer_address.zip', 'buyer_address.postcode', 'customer.address.zip', 'customer.address.postcode',
|
||||
'billing_address.zip', 'billing_address.postcode', 'client.address.zip', 'address.zip',
|
||||
'address.postcode', 'zip', 'postcode', 'client_postal_code', 'kod_pocztowy',
|
||||
])),
|
||||
'country' => StringHelper::nullableString((string) $this->readPath($payload, [
|
||||
'buyer_address.country', 'customer.address.country', 'billing_address.country', 'client.address.country',
|
||||
'address.country', 'country', 'kraj',
|
||||
])),
|
||||
'payload_json' => [
|
||||
'buyer' => $this->readPath($payload, ['buyer']),
|
||||
'customer' => $this->readPath($payload, ['customer']),
|
||||
'billing_address' => $this->readPath($payload, ['billing_address']),
|
||||
'buyer_address' => $this->readPath($payload, ['buyer_address']),
|
||||
'address' => $this->readPath($payload, ['address']),
|
||||
],
|
||||
];
|
||||
|
||||
return ['address' => $address, 'name' => $customerName, 'email' => $customerEmail, 'phone' => $customerPhone];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $payload
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
private function buildDeliveryAddress(array $payload, ?string $customerName, ?string $customerEmail, ?string $customerPhone): ?array
|
||||
{
|
||||
$deliveryFirstName = StringHelper::nullableString((string) $this->readPath($payload, [
|
||||
'delivery.address.first_name', 'delivery.address.firstname', 'shipping.address.first_name', 'shipping.address.firstname',
|
||||
'delivery_address.first_name', 'delivery_address.firstname', 'shipping_address.first_name', 'shipping_address.firstname',
|
||||
'receiver.first_name', 'receiver.firstname', 'delivery_first_name', 'shipping_first_name',
|
||||
]));
|
||||
$deliveryLastName = StringHelper::nullableString((string) $this->readPath($payload, [
|
||||
'delivery.address.last_name', 'delivery.address.lastname', 'shipping.address.last_name', 'shipping.address.lastname',
|
||||
'delivery_address.last_name', 'delivery_address.lastname', 'shipping_address.last_name', 'shipping_address.lastname',
|
||||
'receiver.last_name', 'receiver.lastname', 'delivery_last_name', 'shipping_last_name',
|
||||
]));
|
||||
$deliveryName = StringHelper::nullableString((string) $this->readPath($payload, [
|
||||
'delivery.address.name', 'shipping.address.name', 'delivery_address.name', 'shipping_address.name',
|
||||
'receiver.name', 'delivery_name', 'shipping_name',
|
||||
])) ?? $this->composeName($deliveryFirstName, $deliveryLastName, null);
|
||||
|
||||
$pickupData = $this->parsePickupPoint((string) $this->readPath($payload, ['inpost_paczkomat', 'orlen_point', 'pickup_point']));
|
||||
$fields = [
|
||||
'name' => $deliveryName ?? StringHelper::nullableString($this->buildDeliveryMethodLabel($payload)),
|
||||
'phone' => StringHelper::nullableString((string) $this->readPath($payload, [
|
||||
'delivery.address.phone', 'shipping.address.phone', 'delivery_address.phone', 'shipping_address.phone',
|
||||
'receiver.phone', 'delivery_phone', 'shipping_phone',
|
||||
])) ?? $customerPhone,
|
||||
'email' => StringHelper::nullableString((string) $this->readPath($payload, [
|
||||
'delivery.address.email', 'shipping.address.email', 'delivery_address.email', 'shipping_address.email',
|
||||
'receiver.email', 'delivery_email', 'shipping_email',
|
||||
])) ?? $customerEmail,
|
||||
'street_name' => StringHelper::nullableString((string) $this->readPath($payload, [
|
||||
'delivery.address.street', 'shipping.address.street', 'delivery_address.street', 'shipping_address.street',
|
||||
'receiver.address.street', 'delivery_street', 'shipping_street',
|
||||
])) ?? StringHelper::nullableString($pickupData['street'] ?? ''),
|
||||
'street_number' => StringHelper::nullableString((string) $this->readPath($payload, [
|
||||
'delivery.address.street_number', 'shipping.address.street_number', 'delivery_address.street_number', 'shipping_address.street_number',
|
||||
'delivery.address.house_number', 'shipping.address.house_number', 'receiver.address.street_number',
|
||||
'receiver.address.house_number', 'delivery_street_number', 'shipping_street_number',
|
||||
])),
|
||||
'city' => StringHelper::nullableString((string) $this->readPath($payload, [
|
||||
'delivery.address.city', 'shipping.address.city', 'delivery_address.city', 'shipping_address.city',
|
||||
'receiver.address.city', 'delivery_city', 'shipping_city',
|
||||
])) ?? StringHelper::nullableString($pickupData['city'] ?? ''),
|
||||
'zip_code' => StringHelper::nullableString((string) $this->readPath($payload, [
|
||||
'delivery.address.zip', 'delivery.address.postcode', 'shipping.address.zip', 'shipping.address.postcode',
|
||||
'delivery_address.zip', 'delivery_address.postcode', 'shipping_address.zip', 'shipping_address.postcode',
|
||||
'receiver.address.zip', 'receiver.address.postcode', 'delivery_zip', 'delivery_postcode',
|
||||
'shipping_zip', 'shipping_postcode',
|
||||
])) ?? StringHelper::nullableString($pickupData['zip_code'] ?? ''),
|
||||
'country' => StringHelper::nullableString((string) $this->readPath($payload, [
|
||||
'delivery.address.country', 'shipping.address.country', 'delivery_address.country', 'shipping_address.country',
|
||||
'receiver.address.country', 'delivery_country', 'shipping_country',
|
||||
])),
|
||||
'parcel_external_id' => StringHelper::nullableString($pickupData['code'] ?? ''),
|
||||
'parcel_name' => StringHelper::nullableString($pickupData['label'] ?? ''),
|
||||
'payload_json' => [
|
||||
'delivery' => $this->readPath($payload, ['delivery']),
|
||||
'shipping' => $this->readPath($payload, ['shipping']),
|
||||
'delivery_address' => $this->readPath($payload, ['delivery_address']),
|
||||
'shipping_address' => $this->readPath($payload, ['shipping_address']),
|
||||
'receiver' => $this->readPath($payload, ['receiver']),
|
||||
'inpost_paczkomat' => $this->readPath($payload, ['inpost_paczkomat']),
|
||||
'orlen_point' => $this->readPath($payload, ['orlen_point']),
|
||||
],
|
||||
];
|
||||
|
||||
if (!$this->hasAddressData($fields)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'address_type' => 'delivery',
|
||||
'name' => $fields['name'] ?? $customerName ?? 'Dostawa',
|
||||
'phone' => $fields['phone'] ?? null,
|
||||
'email' => $fields['email'] ?? $customerEmail,
|
||||
'street_name' => $fields['street_name'] ?? null,
|
||||
'street_number' => $fields['street_number'] ?? null,
|
||||
'city' => $fields['city'] ?? null,
|
||||
'zip_code' => $fields['zip_code'] ?? null,
|
||||
'country' => $fields['country'] ?? null,
|
||||
'parcel_external_id' => $fields['parcel_external_id'] ?? null,
|
||||
'parcel_name' => $fields['parcel_name'] ?? null,
|
||||
'payload_json' => is_array($fields['payload_json'] ?? null) ? $fields['payload_json'] : null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $payload
|
||||
*/
|
||||
private function resolveInvoiceRequested(array $payload): bool
|
||||
{
|
||||
$explicitInvoice = $this->readPath($payload, ['is_invoice', 'invoice.required', 'invoice']);
|
||||
if (!empty($explicitInvoice)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$companyName = StringHelper::nullableString((string) $this->readPath($payload, [
|
||||
'invoice.company_name', 'invoice.company', 'billing.company_name', 'billing.company',
|
||||
'firm_name', 'company_name', 'client_company', 'buyer_company',
|
||||
]));
|
||||
$taxNumber = StringHelper::nullableString((string) $this->readPath($payload, [
|
||||
'invoice.tax_id', 'invoice.nip', 'billing.tax_id', 'billing.nip',
|
||||
'firm_nip', 'company_nip', 'tax_id', 'nip',
|
||||
]));
|
||||
|
||||
return $companyName !== null || $taxNumber !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $payload
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
private function buildInvoiceAddress(
|
||||
array $payload,
|
||||
?string $customerName,
|
||||
?string $customerEmail,
|
||||
?string $customerPhone
|
||||
): ?array {
|
||||
$companyName = StringHelper::nullableString((string) $this->readPath($payload, [
|
||||
'invoice.company_name', 'invoice.company', 'billing.company_name', 'billing.company',
|
||||
'firm_name', 'company_name', 'client_company', 'buyer_company',
|
||||
]));
|
||||
$companyTaxNumber = StringHelper::nullableString((string) $this->readPath($payload, [
|
||||
'invoice.tax_id', 'invoice.nip', 'billing.tax_id', 'billing.nip',
|
||||
'firm_nip', 'company_nip', 'tax_id', 'nip',
|
||||
]));
|
||||
$invoiceFirstName = StringHelper::nullableString((string) $this->readPath($payload, [
|
||||
'invoice.first_name', 'invoice.firstname', 'billing_address.first_name', 'billing_address.firstname',
|
||||
'buyer.first_name', 'customer.first_name', 'client_name',
|
||||
]));
|
||||
$invoiceLastName = StringHelper::nullableString((string) $this->readPath($payload, [
|
||||
'invoice.last_name', 'invoice.lastname', 'billing_address.last_name', 'billing_address.lastname',
|
||||
'buyer.last_name', 'customer.last_name', 'client_surname',
|
||||
]));
|
||||
$invoiceName = $companyName ?? $this->composeName($invoiceFirstName, $invoiceLastName, $customerName ?? 'Faktura');
|
||||
|
||||
$streetName = StringHelper::nullableString((string) $this->readPath($payload, [
|
||||
'invoice.address.street', 'invoice.street', 'billing_address.street', 'billing.street',
|
||||
'firm_street', 'company_street',
|
||||
]));
|
||||
$streetNumber = StringHelper::nullableString((string) $this->readPath($payload, [
|
||||
'invoice.address.street_number', 'invoice.street_number', 'invoice.house_number',
|
||||
'billing_address.street_number', 'billing_address.house_number',
|
||||
'billing.street_number', 'house_number', 'street_number',
|
||||
]));
|
||||
$city = StringHelper::nullableString((string) $this->readPath($payload, [
|
||||
'invoice.address.city', 'invoice.city', 'billing_address.city', 'billing.city',
|
||||
'firm_city', 'company_city',
|
||||
]));
|
||||
$zipCode = StringHelper::nullableString((string) $this->readPath($payload, [
|
||||
'invoice.address.zip', 'invoice.address.postcode', 'invoice.zip', 'invoice.postcode',
|
||||
'billing_address.zip', 'billing_address.postcode', 'billing.zip', 'billing.postcode',
|
||||
'firm_postal_code', 'company_postal_code',
|
||||
]));
|
||||
$country = StringHelper::nullableString((string) $this->readPath($payload, [
|
||||
'invoice.address.country', 'invoice.country', 'billing_address.country', 'billing.country',
|
||||
'firm_country', 'company_country',
|
||||
]));
|
||||
$email = StringHelper::nullableString((string) $this->readPath($payload, [
|
||||
'invoice.email', 'billing_address.email', 'billing.email', 'client_email',
|
||||
])) ?? $customerEmail;
|
||||
$phone = StringHelper::nullableString((string) $this->readPath($payload, [
|
||||
'invoice.phone', 'billing_address.phone', 'billing.phone', 'client_phone',
|
||||
])) ?? $customerPhone;
|
||||
|
||||
$hasInvoiceData = $companyName !== null
|
||||
|| $companyTaxNumber !== null
|
||||
|| $streetName !== null
|
||||
|| $city !== null
|
||||
|| $zipCode !== null;
|
||||
if (!$hasInvoiceData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'address_type' => 'invoice',
|
||||
'name' => $invoiceName ?? 'Faktura',
|
||||
'phone' => $phone,
|
||||
'email' => $email,
|
||||
'street_name' => $streetName,
|
||||
'street_number' => $streetNumber,
|
||||
'city' => $city,
|
||||
'zip_code' => $zipCode,
|
||||
'country' => $country,
|
||||
'company_tax_number' => $companyTaxNumber,
|
||||
'company_name' => $companyName,
|
||||
'payload_json' => [
|
||||
'invoice' => $this->readPath($payload, ['invoice']),
|
||||
'billing' => $this->readPath($payload, ['billing']),
|
||||
'billing_address' => $this->readPath($payload, ['billing_address']),
|
||||
'firm_name' => $this->readPath($payload, ['firm_name']),
|
||||
'firm_nip' => $this->readPath($payload, ['firm_nip']),
|
||||
'firm_street' => $this->readPath($payload, ['firm_street']),
|
||||
'firm_postal_code' => $this->readPath($payload, ['firm_postal_code']),
|
||||
'firm_city' => $this->readPath($payload, ['firm_city']),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
private function composeName(?string $firstName, ?string $lastName, ?string $fallback): ?string
|
||||
{
|
||||
$name = trim(trim((string) $firstName) . ' ' . trim((string) $lastName));
|
||||
if ($name !== '') {
|
||||
return $name;
|
||||
}
|
||||
|
||||
$fallbackValue = trim((string) $fallback);
|
||||
return $fallbackValue !== '' ? $fallbackValue : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $address
|
||||
*/
|
||||
private function hasAddressData(array $address): bool
|
||||
{
|
||||
$fields = ['name', 'phone', 'email', 'street_name', 'street_number', 'city', 'zip_code', 'country'];
|
||||
foreach ($fields as $field) {
|
||||
$value = trim((string) ($address[$field] ?? ''));
|
||||
if ($value !== '') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $payload
|
||||
* @param array<int, string> $productImagesById
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
private function mapItems(array $payload, array $productImagesById = []): array
|
||||
{
|
||||
$rawItems = $this->readPath($payload, ['items']);
|
||||
if (!is_array($rawItems)) {
|
||||
$rawItems = $this->readPath($payload, ['order_items']);
|
||||
}
|
||||
if (!is_array($rawItems)) {
|
||||
$rawItems = $this->readPath($payload, ['products']);
|
||||
}
|
||||
if (!is_array($rawItems)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$result = [];
|
||||
$sort = 0;
|
||||
foreach ($rawItems as $row) {
|
||||
if (!is_array($row)) {
|
||||
continue;
|
||||
}
|
||||
$name = trim((string) $this->readPath($row, ['name', 'title']));
|
||||
if ($name === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$productId = (int) $this->readPath($row, ['product_id']);
|
||||
$parentProductId = (int) $this->readPath($row, ['parent_product_id']);
|
||||
$mediaUrl = StringHelper::nullableString((string) $this->readPath($row, ['image', 'image_url', 'img_url', 'img', 'photo', 'photo_url']));
|
||||
if ($mediaUrl === null && $productId > 0 && isset($productImagesById[$productId])) {
|
||||
$mediaUrl = StringHelper::nullableString((string) $productImagesById[$productId]);
|
||||
}
|
||||
if ($mediaUrl === null && $parentProductId > 0 && isset($productImagesById[$parentProductId])) {
|
||||
$mediaUrl = StringHelper::nullableString((string) $productImagesById[$parentProductId]);
|
||||
}
|
||||
|
||||
$result[] = [
|
||||
'source_item_id' => StringHelper::nullableString((string) $this->readPath($row, ['id', 'item_id'])),
|
||||
'external_item_id' => StringHelper::nullableString((string) $this->readPath($row, ['id', 'item_id'])),
|
||||
'ean' => StringHelper::nullableString((string) $this->readPath($row, ['ean'])),
|
||||
'sku' => StringHelper::nullableString((string) $this->readPath($row, ['sku', 'symbol', 'code'])),
|
||||
'original_name' => $name,
|
||||
'original_code' => StringHelper::nullableString((string) $this->readPath($row, ['code', 'symbol'])),
|
||||
'original_price_with_tax' => $this->toFloatOrNull($this->readPath($row, ['price_gross', 'gross_price', 'price', 'price_brutto'])),
|
||||
'original_price_without_tax' => $this->toFloatOrNull($this->readPath($row, ['price_net', 'net_price', 'price_netto'])),
|
||||
'media_url' => $mediaUrl,
|
||||
'quantity' => $this->toFloatOrDefault($this->readPath($row, ['quantity', 'qty']), 1.0),
|
||||
'tax_rate' => $this->toFloatOrNull($this->readPath($row, ['vat', 'tax_rate'])),
|
||||
'item_status' => null,
|
||||
'unit' => StringHelper::nullableString((string) $this->readPath($row, ['unit'])),
|
||||
'item_type' => 'product',
|
||||
'source_product_id' => StringHelper::nullableString((string) ($productId > 0 ? $productId : $parentProductId)),
|
||||
'source_product_set_id' => StringHelper::nullableString((string) ($parentProductId > 0 ? $parentProductId : '')),
|
||||
'sort_order' => $sort++,
|
||||
'payload_json' => $row,
|
||||
];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $payload
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
private function mapPayments(array $payload, string $currency, ?float $totalPaid): array
|
||||
{
|
||||
$paymentMethod = StringHelper::nullableString((string) $this->readPath($payload, ['payment_method', 'payment.method']));
|
||||
if ($paymentMethod === null && $totalPaid === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [[
|
||||
'source_payment_id' => null,
|
||||
'external_payment_id' => null,
|
||||
'payment_type_id' => $paymentMethod ?? 'unknown',
|
||||
'payment_date' => StringHelper::nullableString((string) $this->readPath($payload, ['payment_date', 'payment.date'])),
|
||||
'amount' => $totalPaid,
|
||||
'currency' => $currency,
|
||||
'comment' => StringHelper::nullableString((string) $this->readPath($payload, ['payment_status', 'payment.status'])),
|
||||
'payload_json' => null,
|
||||
]];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $payload
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
private function mapShipments(array $payload): array
|
||||
{
|
||||
$tracking = StringHelper::nullableString((string) $this->readPath($payload, ['delivery_tracking_number', 'delivery.tracking_number', 'shipping.tracking_number']));
|
||||
if ($tracking === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [[
|
||||
'source_shipment_id' => null,
|
||||
'external_shipment_id' => null,
|
||||
'tracking_number' => $tracking,
|
||||
'carrier_provider_id' => $this->sanitizePlainText((string) ($this->readPath($payload, [
|
||||
'delivery_method', 'shipping.method', 'transport', 'transport_description',
|
||||
]) ?? 'unknown')),
|
||||
'posted_at' => StringHelper::nullableString((string) $this->readPath($payload, ['delivery.posted_at', 'shipping.posted_at'])),
|
||||
'media_uuid' => null,
|
||||
'payload_json' => null,
|
||||
]];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $payload
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
private function mapNotes(array $payload): array
|
||||
{
|
||||
$comment = StringHelper::nullableString((string) $this->readPath($payload, ['notes', 'comment', 'customer_comment']));
|
||||
if ($comment === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [[
|
||||
'source_note_id' => null,
|
||||
'note_type' => 'message',
|
||||
'created_at_external' => null,
|
||||
'comment' => $comment,
|
||||
'payload_json' => null,
|
||||
]];
|
||||
}
|
||||
|
||||
private function normalizeOrderId(mixed $value): string
|
||||
{
|
||||
return trim((string) $value);
|
||||
}
|
||||
|
||||
private function normalizePaidFlag(mixed $value): bool
|
||||
{
|
||||
if ($value === true) {
|
||||
return true;
|
||||
}
|
||||
if ($value === false || $value === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$normalized = strtolower(trim((string) $value));
|
||||
return in_array($normalized, ['1', 'true', 'yes', 'paid'], true);
|
||||
}
|
||||
|
||||
private function mapPaymentStatus(array $payload, bool $isPaid): ?int
|
||||
{
|
||||
if ($isPaid) {
|
||||
return 2;
|
||||
}
|
||||
|
||||
$raw = strtolower(trim((string) $this->readPath($payload, ['payment_status', 'payment.status'])));
|
||||
if ($raw === '') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return match ($raw) {
|
||||
'paid', 'finished', 'completed' => 2,
|
||||
'partially_paid', 'in_progress' => 1,
|
||||
'cancelled', 'canceled', 'failed', 'unpaid', 'not_paid' => 0,
|
||||
default => 0,
|
||||
};
|
||||
}
|
||||
|
||||
private function sanitizePlainText(string $value): string
|
||||
{
|
||||
$withoutTags = strip_tags($value);
|
||||
$decoded = html_entity_decode($withoutTags, ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
||||
return trim(preg_replace('/\s+/', ' ', $decoded) ?? '');
|
||||
}
|
||||
|
||||
private function buildDeliveryMethodLabel(array $payload): string
|
||||
{
|
||||
$label = $this->sanitizePlainText((string) $this->readPath($payload, [
|
||||
'delivery_method', 'shipping.method', 'delivery.method', 'shipping_method', 'delivery_name', 'shipping_name',
|
||||
'transport', 'transport_description',
|
||||
]));
|
||||
$cost = $this->toFloatOrNull($this->readPath($payload, ['transport_cost', 'delivery_cost', 'shipping.cost']));
|
||||
if ($label !== '' && $cost !== null) {
|
||||
$label .= ': ' . $this->formatMoneyCompact($cost) . ' zł';
|
||||
}
|
||||
|
||||
return $label;
|
||||
}
|
||||
|
||||
private function formatMoneyCompact(float $amount): string
|
||||
{
|
||||
$formatted = number_format($amount, 2, '.', '');
|
||||
return rtrim(rtrim($formatted, '0'), '.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{code:string,label:string,street:string,zip_code:string,city:string}
|
||||
*/
|
||||
private function parsePickupPoint(string $raw): array
|
||||
{
|
||||
$value = trim($this->sanitizePlainText($raw));
|
||||
if ($value === '') {
|
||||
return ['code' => '', 'label' => '', 'street' => '', 'zip_code' => '', 'city' => ''];
|
||||
}
|
||||
|
||||
$code = '';
|
||||
$address = $value;
|
||||
$parts = preg_split('/\s*\|\s*/', $value);
|
||||
if (is_array($parts) && count($parts) >= 2) {
|
||||
$code = trim((string) ($parts[0] ?? ''));
|
||||
$address = trim((string) ($parts[1] ?? ''));
|
||||
}
|
||||
|
||||
$street = '';
|
||||
$zip = '';
|
||||
$city = '';
|
||||
if (preg_match('/^(.*?),\s*(\d{2}-\d{3})\s+(.+)$/u', $address, $matches) === 1) {
|
||||
$street = trim((string) ($matches[1] ?? ''));
|
||||
$zip = trim((string) ($matches[2] ?? ''));
|
||||
$city = trim((string) ($matches[3] ?? ''));
|
||||
} elseif (preg_match('/(\d{2}-\d{3})\s+(.+)$/u', $address, $matches) === 1) {
|
||||
$zip = trim((string) ($matches[1] ?? ''));
|
||||
$city = trim((string) ($matches[2] ?? ''));
|
||||
}
|
||||
|
||||
return [
|
||||
'code' => $code,
|
||||
'label' => $value,
|
||||
'street' => $street,
|
||||
'zip_code' => $zip,
|
||||
'city' => $city,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $orderPayload
|
||||
* @param array<int, string> $cache
|
||||
* @return array<int, string>
|
||||
*/
|
||||
private function resolveProductImagesForOrder(
|
||||
string $baseUrl,
|
||||
string $apiKey,
|
||||
int $timeout,
|
||||
array $orderPayload,
|
||||
array &$cache
|
||||
): array {
|
||||
$result = [];
|
||||
$rawItems = $this->readPath($orderPayload, ['products', 'items', 'order_items']);
|
||||
if (!is_array($rawItems)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
foreach ($rawItems as $item) {
|
||||
if (!is_array($item)) {
|
||||
continue;
|
||||
}
|
||||
$productId = (int) $this->readPath($item, ['product_id']);
|
||||
$parentProductId = (int) $this->readPath($item, ['parent_product_id']);
|
||||
if ($productId <= 0 && $parentProductId <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ([$productId, $parentProductId] as $candidateId) {
|
||||
if ($candidateId <= 0) {
|
||||
continue;
|
||||
}
|
||||
if (!isset($cache[$candidateId])) {
|
||||
$cache[$candidateId] = $this->fetchPrimaryProductImageUrl($baseUrl, $apiKey, $timeout, $candidateId) ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
if ($productId > 0) {
|
||||
$url = trim((string) ($cache[$productId] ?? ''));
|
||||
if ($url !== '') {
|
||||
$result[$productId] = $url;
|
||||
}
|
||||
}
|
||||
if ($parentProductId > 0) {
|
||||
$url = trim((string) ($cache[$parentProductId] ?? ''));
|
||||
if ($url !== '') {
|
||||
$result[$parentProductId] = $url;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function fetchPrimaryProductImageUrl(string $baseUrl, string $apiKey, int $timeout, int $productId): ?string
|
||||
{
|
||||
$response = $this->apiClient->fetchProductById($baseUrl, $apiKey, $timeout, $productId);
|
||||
if (($response['ok'] ?? false) !== true || !is_array($response['product'] ?? null)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$product = (array) $response['product'];
|
||||
$images = $this->readPath($product, ['images', 'photos', 'gallery']);
|
||||
if (is_array($images)) {
|
||||
foreach ($images as $image) {
|
||||
if (is_array($image)) {
|
||||
$src = trim((string) ($image['src'] ?? $image['url'] ?? $image['image'] ?? ''));
|
||||
if ($src !== '') {
|
||||
return $this->normalizeMediaUrl($baseUrl, $src);
|
||||
}
|
||||
} elseif (is_string($image)) {
|
||||
$src = trim($image);
|
||||
if ($src !== '') {
|
||||
return $this->normalizeMediaUrl($baseUrl, $src);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$flat = trim((string) $this->readPath($product, ['image', 'image_url', 'photo', 'photo_url', 'img', 'img_url']));
|
||||
if ($flat !== '') {
|
||||
return $this->normalizeMediaUrl($baseUrl, $flat);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function normalizeMediaUrl(string $baseUrl, string $value): string
|
||||
{
|
||||
$trimmed = trim($value);
|
||||
if ($trimmed === '') {
|
||||
return '';
|
||||
}
|
||||
if (str_starts_with($trimmed, '//')) {
|
||||
return 'https:' . $trimmed;
|
||||
}
|
||||
if (preg_match('#^https?://#i', $trimmed) === 1) {
|
||||
return $trimmed;
|
||||
}
|
||||
|
||||
return rtrim($baseUrl, '/') . '/' . ltrim($trimmed, '/');
|
||||
}
|
||||
|
||||
private function toFloatOrNull(mixed $value): ?float
|
||||
{
|
||||
if ($value === null || $value === '') {
|
||||
return null;
|
||||
}
|
||||
if (is_string($value)) {
|
||||
$value = str_replace(',', '.', trim($value));
|
||||
}
|
||||
if (!is_numeric($value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (float) $value;
|
||||
}
|
||||
|
||||
private function toFloatOrDefault(mixed $value, float $default): float
|
||||
{
|
||||
$result = $this->toFloatOrNull($value);
|
||||
return $result ?? $default;
|
||||
}
|
||||
|
||||
private function readPath(mixed $payload, array $paths): mixed
|
||||
{
|
||||
foreach ($paths as $path) {
|
||||
$value = $this->readSinglePath($payload, (string) $path);
|
||||
if ($value !== null && $value !== '') {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function readSinglePath(mixed $payload, string $path): mixed
|
||||
{
|
||||
if ($path === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$segments = explode('.', $path);
|
||||
$current = $payload;
|
||||
foreach ($segments as $segment) {
|
||||
if (!is_array($current) || !array_key_exists($segment, $current)) {
|
||||
return null;
|
||||
}
|
||||
$current = $current[$segment];
|
||||
}
|
||||
|
||||
return $current;
|
||||
}
|
||||
}
|
||||
|
||||
145
src/Modules/Settings/ShopproProductImageResolver.php
Normal file
145
src/Modules/Settings/ShopproProductImageResolver.php
Normal file
@@ -0,0 +1,145 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Settings;
|
||||
|
||||
final class ShopproProductImageResolver
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ShopproApiClient $apiClient
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $orderPayload
|
||||
* @param array<int, string> $cache
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public function resolveProductImagesForOrder(
|
||||
string $baseUrl,
|
||||
string $apiKey,
|
||||
int $timeout,
|
||||
array $orderPayload,
|
||||
array &$cache
|
||||
): array {
|
||||
$result = [];
|
||||
$rawItems = $this->readPath($orderPayload, ['products', 'items', 'order_items']);
|
||||
if (!is_array($rawItems)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
foreach ($rawItems as $item) {
|
||||
if (!is_array($item)) {
|
||||
continue;
|
||||
}
|
||||
$productId = (int) $this->readPath($item, ['product_id']);
|
||||
$parentProductId = (int) $this->readPath($item, ['parent_product_id']);
|
||||
if ($productId <= 0 && $parentProductId <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ([$productId, $parentProductId] as $candidateId) {
|
||||
if ($candidateId <= 0) {
|
||||
continue;
|
||||
}
|
||||
if (!isset($cache[$candidateId])) {
|
||||
$cache[$candidateId] = $this->fetchPrimaryProductImageUrl($baseUrl, $apiKey, $timeout, $candidateId) ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
if ($productId > 0) {
|
||||
$url = trim((string) ($cache[$productId] ?? ''));
|
||||
if ($url !== '') {
|
||||
$result[$productId] = $url;
|
||||
}
|
||||
}
|
||||
if ($parentProductId > 0) {
|
||||
$url = trim((string) ($cache[$parentProductId] ?? ''));
|
||||
if ($url !== '') {
|
||||
$result[$parentProductId] = $url;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function fetchPrimaryProductImageUrl(string $baseUrl, string $apiKey, int $timeout, int $productId): ?string
|
||||
{
|
||||
$response = $this->apiClient->fetchProductById($baseUrl, $apiKey, $timeout, $productId);
|
||||
if (($response['ok'] ?? false) !== true || !is_array($response['product'] ?? null)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$product = (array) $response['product'];
|
||||
$images = $this->readPath($product, ['images', 'photos', 'gallery']);
|
||||
if (is_array($images)) {
|
||||
foreach ($images as $image) {
|
||||
if (is_array($image)) {
|
||||
$src = trim((string) ($image['src'] ?? $image['url'] ?? $image['image'] ?? ''));
|
||||
if ($src !== '') {
|
||||
return $this->normalizeMediaUrl($baseUrl, $src);
|
||||
}
|
||||
} elseif (is_string($image)) {
|
||||
$src = trim($image);
|
||||
if ($src !== '') {
|
||||
return $this->normalizeMediaUrl($baseUrl, $src);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$flat = trim((string) $this->readPath($product, ['image', 'image_url', 'photo', 'photo_url', 'img', 'img_url']));
|
||||
if ($flat !== '') {
|
||||
return $this->normalizeMediaUrl($baseUrl, $flat);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function normalizeMediaUrl(string $baseUrl, string $value): string
|
||||
{
|
||||
$trimmed = trim($value);
|
||||
if ($trimmed === '') {
|
||||
return '';
|
||||
}
|
||||
if (str_starts_with($trimmed, '//')) {
|
||||
return 'https:' . $trimmed;
|
||||
}
|
||||
if (preg_match('#^https?://#i', $trimmed) === 1) {
|
||||
return $trimmed;
|
||||
}
|
||||
|
||||
return rtrim($baseUrl, '/') . '/' . ltrim($trimmed, '/');
|
||||
}
|
||||
|
||||
private function readPath(mixed $payload, array $paths): mixed
|
||||
{
|
||||
foreach ($paths as $path) {
|
||||
$value = $this->readSinglePath($payload, (string) $path);
|
||||
if ($value !== null && $value !== '') {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function readSinglePath(mixed $payload, string $path): mixed
|
||||
{
|
||||
if ($path === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$segments = explode('.', $path);
|
||||
$current = $payload;
|
||||
foreach ($segments as $segment) {
|
||||
if (!is_array($current) || !array_key_exists($segment, $current)) {
|
||||
return null;
|
||||
}
|
||||
$current = $current[$segment];
|
||||
}
|
||||
|
||||
return $current;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user