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:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user