Phase 7 complete (5 plans): - 07-01: Performance (N+1→LEFT JOIN, static cache, DB indexes) - 07-02: Stability (SSL verification, cron throttle DB, migration 000014b) - 07-03: UX (orderpro_to_allegro disable, lista zamówień fixes, SSL hotfix) - 07-04: Tests (12 unit tests for AllegroTokenManager + AllegroOrderImportService) - 07-05: InPost ShipX API (natywny provider, workaround remap usunięty) Additional fixes: - 5 broken use-statements fixed across 4 files - vendor/ excluded from ftp-kr auto-upload - PHPUnit + dg/bypass-finals infrastructure Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
712 lines
29 KiB
PHP
712 lines
29 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Modules\Settings;
|
|
|
|
use App\Core\Support\StringHelper;
|
|
use App\Modules\Orders\OrderImportRepository;
|
|
use App\Modules\Orders\OrdersRepository;
|
|
use App\Core\Constants\IntegrationSources;
|
|
use App\Core\Exceptions\AllegroApiException;
|
|
use RuntimeException;
|
|
use Throwable;
|
|
|
|
final class AllegroOrderImportService
|
|
{
|
|
public function __construct(
|
|
private readonly AllegroIntegrationRepository $integrationRepository,
|
|
private readonly AllegroTokenManager $tokenManager,
|
|
private readonly AllegroApiClient $apiClient,
|
|
private readonly OrderImportRepository $orders,
|
|
private readonly AllegroStatusMappingRepository $statusMappings,
|
|
private readonly OrdersRepository $ordersRepository
|
|
) {
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
public function importSingleOrder(string $checkoutFormId): array
|
|
{
|
|
$orderId = trim($checkoutFormId);
|
|
if ($orderId === '') {
|
|
throw new AllegroApiException('Podaj ID zamowienia Allegro.');
|
|
}
|
|
|
|
[$accessToken, $env] = $this->tokenManager->resolveToken();
|
|
|
|
try {
|
|
$payload = $this->apiClient->getCheckoutForm($env, $accessToken, $orderId);
|
|
} catch (RuntimeException $exception) {
|
|
if (trim($exception->getMessage()) !== 'ALLEGRO_HTTP_401') {
|
|
throw $exception;
|
|
}
|
|
|
|
[$accessToken, $env] = $this->tokenManager->resolveToken();
|
|
$payload = $this->apiClient->getCheckoutForm($env, $accessToken, $orderId);
|
|
}
|
|
|
|
$mapped = $this->mapCheckoutFormPayload($payload, $env, $accessToken);
|
|
$saveResult = $this->orders->upsertOrderAggregate(
|
|
$mapped['order'],
|
|
$mapped['addresses'],
|
|
$mapped['items'],
|
|
$mapped['payments'],
|
|
$mapped['shipments'],
|
|
$mapped['notes'],
|
|
$mapped['status_history']
|
|
);
|
|
|
|
$savedOrderId = (int) ($saveResult['order_id'] ?? 0);
|
|
$wasCreated = !empty($saveResult['created']);
|
|
|
|
if ($savedOrderId > 0) {
|
|
$summary = $wasCreated
|
|
? 'Zaimportowano zamowienie z Allegro'
|
|
: 'Zaktualizowano zamowienie z Allegro (re-import)';
|
|
$this->ordersRepository->recordActivity(
|
|
$savedOrderId,
|
|
'import',
|
|
$summary,
|
|
[
|
|
'source' => IntegrationSources::ALLEGRO,
|
|
'source_order_id' => trim($checkoutFormId),
|
|
'created' => $wasCreated,
|
|
],
|
|
'import',
|
|
'Allegro'
|
|
);
|
|
}
|
|
|
|
return [
|
|
'order_id' => $savedOrderId,
|
|
'created' => $wasCreated,
|
|
'source_order_id' => (string) ($mapped['order']['source_order_id'] ?? ''),
|
|
'image_diagnostics' => (array) ($mapped['image_diagnostics'] ?? []),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $payload
|
|
* @return array{
|
|
* order:array<string, mixed>,
|
|
* addresses:array<int, array<string, mixed>>,
|
|
* items:array<int, array<string, mixed>>,
|
|
* image_diagnostics: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 mapCheckoutFormPayload(array $payload, string $environment, string $accessToken): array
|
|
{
|
|
$checkoutFormId = trim((string) ($payload['id'] ?? ''));
|
|
if ($checkoutFormId === '') {
|
|
throw new AllegroApiException('Odpowiedz Allegro nie zawiera ID zamowienia.');
|
|
}
|
|
|
|
$status = trim((string) ($payload['status'] ?? ''));
|
|
$fulfillmentStatus = trim((string) ($payload['fulfillment']['status'] ?? ''));
|
|
$rawAllegroStatus = strtolower($fulfillmentStatus !== '' ? $fulfillmentStatus : $status);
|
|
$mappedOrderproStatus = $this->statusMappings->findMappedOrderproStatusCode($rawAllegroStatus);
|
|
$externalStatus = $mappedOrderproStatus !== null ? $mappedOrderproStatus : $rawAllegroStatus;
|
|
$paymentStatusRaw = strtolower(trim((string) ($payload['payment']['status'] ?? '')));
|
|
|
|
$totalWithTax = $this->amountToFloat($payload['summary']['totalToPay'] ?? null);
|
|
$totalPaid = $this->amountToFloat($payload['summary']['paidAmount'] ?? null);
|
|
if ($totalPaid === null) {
|
|
$totalPaid = $this->amountToFloat($payload['payment']['paidAmount'] ?? null);
|
|
}
|
|
if ($totalPaid === null) {
|
|
$totalPaid = $this->amountToFloat($payload['payment']['amount'] ?? null);
|
|
}
|
|
|
|
$currency = trim((string) ($payload['summary']['totalToPay']['currency'] ?? ''));
|
|
if ($currency === '') {
|
|
$currency = trim((string) ($payload['payment']['amount']['currency'] ?? 'PLN'));
|
|
}
|
|
if ($currency === '') {
|
|
$currency = 'PLN';
|
|
}
|
|
|
|
$buyer = is_array($payload['buyer'] ?? null) ? $payload['buyer'] : [];
|
|
$delivery = is_array($payload['delivery'] ?? null) ? $payload['delivery'] : [];
|
|
$invoice = is_array($payload['invoice'] ?? null) ? $payload['invoice'] : [];
|
|
$payment = is_array($payload['payment'] ?? null) ? $payload['payment'] : [];
|
|
$lineItems = is_array($payload['lineItems'] ?? null) ? $payload['lineItems'] : [];
|
|
$deliveryMethod = is_array($delivery['method'] ?? null) ? $delivery['method'] : [];
|
|
$deliveryMethodId = trim((string) ($deliveryMethod['id'] ?? ''));
|
|
$deliveryMethodName = trim((string) ($deliveryMethod['name'] ?? ''));
|
|
$deliveryForm = $deliveryMethodName !== '' ? $deliveryMethodName : $deliveryMethodId;
|
|
$deliveryTime = is_array($delivery['time'] ?? null) ? $delivery['time'] : [];
|
|
$dispatchTime = is_array($deliveryTime['dispatch'] ?? null) ? $deliveryTime['dispatch'] : [];
|
|
$sendDateMin = StringHelper::normalizeDateTime((string) ($dispatchTime['from'] ?? ''));
|
|
$sendDateMax = StringHelper::normalizeDateTime((string) ($dispatchTime['to'] ?? ''));
|
|
if ($sendDateMin === null) {
|
|
$sendDateMin = StringHelper::normalizeDateTime((string) ($deliveryTime['from'] ?? ''));
|
|
}
|
|
if ($sendDateMax === null) {
|
|
$sendDateMax = StringHelper::normalizeDateTime((string) ($deliveryTime['to'] ?? ''));
|
|
}
|
|
|
|
$boughtAt = StringHelper::normalizeDateTime((string) ($payload['boughtAt'] ?? ''));
|
|
$updatedAt = StringHelper::normalizeDateTime((string) ($payload['updatedAt'] ?? ''));
|
|
$fetchedAt = date('Y-m-d H:i:s');
|
|
|
|
$order = [
|
|
'integration_id' => $this->integrationRepository->getActiveIntegrationId(),
|
|
'source' => IntegrationSources::ALLEGRO,
|
|
'source_order_id' => $checkoutFormId,
|
|
'external_order_id' => $checkoutFormId,
|
|
'external_platform_id' => trim((string) ($payload['marketplace']['id'] ?? 'allegro-pl')),
|
|
'external_platform_account_id' => null,
|
|
'external_status_id' => $externalStatus,
|
|
'external_payment_type_id' => trim((string) ($payment['type'] ?? '')),
|
|
'payment_status' => $this->mapPaymentStatus($paymentStatusRaw),
|
|
'external_carrier_id' => $deliveryForm !== '' ? $deliveryForm : null,
|
|
'external_carrier_account_id' => $deliveryMethodId !== '' ? $deliveryMethodId : null,
|
|
'customer_login' => trim((string) ($buyer['login'] ?? '')),
|
|
'is_invoice' => !empty($invoice['required']),
|
|
'is_encrypted' => false,
|
|
'is_canceled_by_buyer' => in_array($externalStatus, ['cancelled', 'canceled'], true),
|
|
'currency' => strtoupper($currency),
|
|
'total_without_tax' => null,
|
|
'total_with_tax' => $totalWithTax,
|
|
'total_paid' => $totalPaid,
|
|
'send_date_min' => $sendDateMin,
|
|
'send_date_max' => $sendDateMax,
|
|
'ordered_at' => $boughtAt,
|
|
'source_created_at' => $boughtAt,
|
|
'source_updated_at' => $updatedAt,
|
|
'preferences_json' => [
|
|
'status' => $status,
|
|
'fulfillment_status' => $fulfillmentStatus,
|
|
'allegro_status_raw' => $rawAllegroStatus,
|
|
'payment_status' => $paymentStatusRaw,
|
|
'delivery_method_name' => $deliveryMethodName,
|
|
'delivery_method_id' => $deliveryMethodId,
|
|
'delivery_cost' => $delivery['cost'] ?? null,
|
|
'delivery_time' => $deliveryTime,
|
|
],
|
|
'payload_json' => $payload,
|
|
'fetched_at' => $fetchedAt,
|
|
];
|
|
|
|
$addresses = $this->buildAddresses($buyer, $delivery, $invoice);
|
|
$itemsResult = $this->buildItems($lineItems, $environment, $accessToken);
|
|
$items = (array) ($itemsResult['items'] ?? []);
|
|
$payments = $this->buildPayments($payment, $currency);
|
|
$apiShipments = [];
|
|
try {
|
|
$apiShipments = $this->apiClient->getCheckoutFormShipments($environment, $accessToken, $checkoutFormId);
|
|
} catch (Throwable) {
|
|
}
|
|
$shipments = $this->buildShipments($apiShipments, $delivery);
|
|
$notes = $this->buildNotes($payload);
|
|
$statusHistory = [[
|
|
'from_status_id' => null,
|
|
'to_status_id' => $externalStatus !== '' ? $externalStatus : 'unknown',
|
|
'changed_at' => $updatedAt !== null ? $updatedAt : $fetchedAt,
|
|
'change_source' => 'import',
|
|
'comment' => 'Import z Allegro checkout form',
|
|
'payload_json' => [
|
|
'status' => $status,
|
|
'fulfillment_status' => $fulfillmentStatus,
|
|
'allegro_status_raw' => $rawAllegroStatus,
|
|
],
|
|
]];
|
|
|
|
return [
|
|
'order' => $order,
|
|
'addresses' => $addresses,
|
|
'items' => $items,
|
|
'image_diagnostics' => (array) ($itemsResult['image_diagnostics'] ?? []),
|
|
'payments' => $payments,
|
|
'shipments' => $shipments,
|
|
'notes' => $notes,
|
|
'status_history' => $statusHistory,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $buyer
|
|
* @param array<string, mixed> $delivery
|
|
* @param array<string, mixed> $invoice
|
|
* @return array<int, array<string, mixed>>
|
|
*/
|
|
private function buildAddresses(array $buyer, array $delivery, array $invoice): array
|
|
{
|
|
$result = [];
|
|
|
|
$customerName = trim((string) (($buyer['firstName'] ?? '') . ' ' . ($buyer['lastName'] ?? '')));
|
|
if ($customerName === '') {
|
|
$customerName = trim((string) ($buyer['login'] ?? ''));
|
|
}
|
|
if ($customerName === '') {
|
|
$customerName = 'Kupujacy Allegro';
|
|
}
|
|
|
|
$result[] = [
|
|
'address_type' => 'customer',
|
|
'name' => $customerName,
|
|
'phone' => StringHelper::nullableString((string) ($buyer['phoneNumber'] ?? '')),
|
|
'email' => StringHelper::nullableString((string) ($buyer['email'] ?? '')),
|
|
'street_name' => null,
|
|
'street_number' => null,
|
|
'city' => null,
|
|
'zip_code' => null,
|
|
'country' => null,
|
|
'department' => null,
|
|
'parcel_external_id' => null,
|
|
'parcel_name' => null,
|
|
'address_class' => null,
|
|
'company_tax_number' => null,
|
|
'company_name' => null,
|
|
'payload_json' => $buyer,
|
|
];
|
|
|
|
$deliveryAddress = is_array($delivery['address'] ?? null) ? $delivery['address'] : [];
|
|
$pickupPoint = is_array($delivery['pickupPoint'] ?? null) ? $delivery['pickupPoint'] : [];
|
|
$pickupAddress = is_array($pickupPoint['address'] ?? null) ? $pickupPoint['address'] : [];
|
|
if ($deliveryAddress !== [] || $pickupAddress !== []) {
|
|
$isPickupPointDelivery = $pickupAddress !== [];
|
|
|
|
// Always use recipient's personal data from delivery.address for name/phone/email.
|
|
// For pickup points, delivery.address still holds the recipient's data (not the machine location).
|
|
$name = $this->fallbackName($deliveryAddress, 'Dostawa');
|
|
|
|
$street = $isPickupPointDelivery
|
|
? StringHelper::nullableString((string) ($pickupAddress['street'] ?? ''))
|
|
: StringHelper::nullableString((string) ($deliveryAddress['street'] ?? ''));
|
|
$city = $isPickupPointDelivery
|
|
? StringHelper::nullableString((string) ($pickupAddress['city'] ?? ''))
|
|
: StringHelper::nullableString((string) ($deliveryAddress['city'] ?? ''));
|
|
$zipCode = $isPickupPointDelivery
|
|
? StringHelper::nullableString((string) ($pickupAddress['zipCode'] ?? ''))
|
|
: StringHelper::nullableString((string) ($deliveryAddress['zipCode'] ?? ''));
|
|
$country = $isPickupPointDelivery
|
|
? StringHelper::nullableString((string) ($pickupAddress['countryCode'] ?? ''))
|
|
: StringHelper::nullableString((string) ($deliveryAddress['countryCode'] ?? ''));
|
|
|
|
$deliveryPhone = trim((string) ($deliveryAddress['phoneNumber'] ?? ''));
|
|
$buyerPhone = trim((string) ($buyer['phoneNumber'] ?? ''));
|
|
|
|
$result[] = [
|
|
'address_type' => 'delivery',
|
|
'name' => $name,
|
|
'phone' => StringHelper::nullableString($deliveryPhone !== '' ? $deliveryPhone : $buyerPhone),
|
|
'email' => StringHelper::nullableString((string) ($deliveryAddress['email'] ?? $buyer['email'] ?? '')),
|
|
'street_name' => $street,
|
|
'street_number' => null,
|
|
'city' => $city,
|
|
'zip_code' => $zipCode,
|
|
'country' => $country,
|
|
'department' => null,
|
|
'parcel_external_id' => StringHelper::nullableString((string) ($pickupPoint['id'] ?? '')),
|
|
'parcel_name' => StringHelper::nullableString((string) ($pickupPoint['name'] ?? '')),
|
|
'address_class' => null,
|
|
'company_tax_number' => null,
|
|
'company_name' => StringHelper::nullableString((string) ($deliveryAddress['companyName'] ?? '')),
|
|
'payload_json' => [
|
|
'address' => $deliveryAddress,
|
|
'pickup_point' => $pickupPoint,
|
|
],
|
|
];
|
|
}
|
|
|
|
$invoiceAddress = is_array($invoice['address'] ?? null) ? $invoice['address'] : [];
|
|
if ($invoiceAddress !== []) {
|
|
$result[] = [
|
|
'address_type' => 'invoice',
|
|
'name' => $this->fallbackName($invoiceAddress, 'Faktura'),
|
|
'phone' => StringHelper::nullableString((string) ($invoiceAddress['phoneNumber'] ?? '')),
|
|
'email' => StringHelper::nullableString((string) ($invoiceAddress['email'] ?? '')),
|
|
'street_name' => StringHelper::nullableString((string) ($invoiceAddress['street'] ?? '')),
|
|
'street_number' => null,
|
|
'city' => StringHelper::nullableString((string) ($invoiceAddress['city'] ?? '')),
|
|
'zip_code' => StringHelper::nullableString((string) ($invoiceAddress['zipCode'] ?? '')),
|
|
'country' => StringHelper::nullableString((string) ($invoiceAddress['countryCode'] ?? '')),
|
|
'department' => null,
|
|
'parcel_external_id' => null,
|
|
'parcel_name' => null,
|
|
'address_class' => null,
|
|
'company_tax_number' => StringHelper::nullableString((string) ($invoiceAddress['taxId'] ?? '')),
|
|
'company_name' => StringHelper::nullableString((string) ($invoiceAddress['companyName'] ?? '')),
|
|
'payload_json' => $invoiceAddress,
|
|
];
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* @param array<int, mixed> $lineItems
|
|
* @return array{
|
|
* items:array<int, array<string, mixed>>,
|
|
* image_diagnostics:array<string, mixed>
|
|
* }
|
|
*/
|
|
private function buildItems(array $lineItems, string $environment, string $accessToken): array
|
|
{
|
|
$result = [];
|
|
$offerImageCache = [];
|
|
$diagnostics = [
|
|
'total_items' => 0,
|
|
'with_image' => 0,
|
|
'without_image' => 0,
|
|
'source_counts' => [
|
|
'checkout_form' => 0,
|
|
'offer_api' => 0,
|
|
],
|
|
'reason_counts' => [],
|
|
'sample_issues' => [],
|
|
];
|
|
$sortOrder = 0;
|
|
foreach ($lineItems as $itemRaw) {
|
|
if (!is_array($itemRaw)) {
|
|
continue;
|
|
}
|
|
|
|
$diagnostics['total_items'] = (int) $diagnostics['total_items'] + 1;
|
|
$offer = is_array($itemRaw['offer'] ?? null) ? $itemRaw['offer'] : [];
|
|
$name = trim((string) ($offer['name'] ?? ''));
|
|
if ($name === '') {
|
|
$name = 'Pozycja Allegro';
|
|
}
|
|
|
|
$offerId = trim((string) ($offer['id'] ?? ''));
|
|
$mediaUrl = $this->extractLineItemImageUrl($itemRaw);
|
|
$imageSource = 'none';
|
|
$missingReason = null;
|
|
if ($mediaUrl === null && $offerId !== '') {
|
|
$offerImageResult = $this->resolveOfferImageUrlFromApi($offerId, $environment, $accessToken, $offerImageCache);
|
|
$mediaUrl = $offerImageResult['url'];
|
|
if ($mediaUrl !== null) {
|
|
$imageSource = 'offer_api';
|
|
} else {
|
|
$missingReason = $offerImageResult['reason'];
|
|
}
|
|
} elseif ($mediaUrl === null) {
|
|
$missingReason = 'missing_offer_id';
|
|
} else {
|
|
$imageSource = 'checkout_form';
|
|
}
|
|
|
|
if ($mediaUrl !== null) {
|
|
$diagnostics['with_image'] = (int) $diagnostics['with_image'] + 1;
|
|
if ($imageSource === 'offer_api') {
|
|
$diagnostics['source_counts']['offer_api'] = (int) ($diagnostics['source_counts']['offer_api'] ?? 0) + 1;
|
|
} else {
|
|
$diagnostics['source_counts']['checkout_form'] = (int) ($diagnostics['source_counts']['checkout_form'] ?? 0) + 1;
|
|
}
|
|
} else {
|
|
$diagnostics['without_image'] = (int) $diagnostics['without_image'] + 1;
|
|
$reasonCode = $missingReason ?? 'missing_in_checkout_form';
|
|
$reasonCounts = is_array($diagnostics['reason_counts']) ? $diagnostics['reason_counts'] : [];
|
|
$reasonCounts[$reasonCode] = (int) ($reasonCounts[$reasonCode] ?? 0) + 1;
|
|
$diagnostics['reason_counts'] = $reasonCounts;
|
|
|
|
$sampleIssues = is_array($diagnostics['sample_issues']) ? $diagnostics['sample_issues'] : [];
|
|
if (count($sampleIssues) < 5) {
|
|
$sampleIssues[] = [
|
|
'offer_id' => $offerId,
|
|
'name' => $name,
|
|
'reason' => $reasonCode,
|
|
];
|
|
}
|
|
$diagnostics['sample_issues'] = $sampleIssues;
|
|
}
|
|
|
|
$result[] = [
|
|
'source_item_id' => StringHelper::nullableString((string) ($itemRaw['id'] ?? '')),
|
|
'external_item_id' => StringHelper::nullableString((string) ($offer['id'] ?? '')),
|
|
'ean' => null,
|
|
'sku' => null,
|
|
'original_name' => $name,
|
|
'original_code' => StringHelper::nullableString((string) ($offer['id'] ?? '')),
|
|
'original_price_with_tax' => $this->amountToFloat($itemRaw['originalPrice'] ?? null),
|
|
'original_price_without_tax' => null,
|
|
'media_url' => $mediaUrl,
|
|
'quantity' => (float) ($itemRaw['quantity'] ?? 1),
|
|
'tax_rate' => null,
|
|
'item_status' => null,
|
|
'unit' => 'pcs',
|
|
'item_type' => 'product',
|
|
'source_product_id' => StringHelper::nullableString((string) ($offer['id'] ?? '')),
|
|
'source_product_set_id' => null,
|
|
'sort_order' => $sortOrder++,
|
|
'payload_json' => $itemRaw,
|
|
];
|
|
}
|
|
|
|
return [
|
|
'items' => $result,
|
|
'image_diagnostics' => $diagnostics,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param array<string, array{url:?string, reason:?string}> $offerImageCache
|
|
* @return array{url:?string, reason:?string}
|
|
*/
|
|
private function resolveOfferImageUrlFromApi(
|
|
string $offerId,
|
|
string $environment,
|
|
string $accessToken,
|
|
array &$offerImageCache
|
|
): array {
|
|
if (array_key_exists($offerId, $offerImageCache)) {
|
|
return $offerImageCache[$offerId];
|
|
}
|
|
|
|
try {
|
|
$offerPayload = $this->apiClient->getProductOffer($environment, $accessToken, $offerId);
|
|
$url = $this->extractOfferImageUrl($offerPayload);
|
|
if ($url !== null) {
|
|
$offerImageCache[$offerId] = ['url' => $url, 'reason' => null];
|
|
return $offerImageCache[$offerId];
|
|
}
|
|
$offerImageCache[$offerId] = ['url' => null, 'reason' => 'missing_in_offer_api'];
|
|
} catch (Throwable $exception) {
|
|
$reason = $this->mapOfferApiErrorToReason($exception->getMessage());
|
|
$offerImageCache[$offerId] = ['url' => null, 'reason' => $reason];
|
|
}
|
|
|
|
return $offerImageCache[$offerId];
|
|
}
|
|
|
|
private function mapOfferApiErrorToReason(string $message): string
|
|
{
|
|
$normalized = strtoupper(trim($message));
|
|
if (str_contains($normalized, 'HTTP 403')) {
|
|
return 'offer_api_access_denied_403';
|
|
}
|
|
if (str_contains($normalized, 'HTTP 401')) {
|
|
return 'offer_api_unauthorized_401';
|
|
}
|
|
if (str_contains($normalized, 'HTTP 404')) {
|
|
return 'offer_api_not_found_404';
|
|
}
|
|
if (preg_match('/HTTP\s+(\d{3})/', $normalized, $matches) === 1) {
|
|
return 'offer_api_http_' . $matches[1];
|
|
}
|
|
|
|
return 'offer_api_request_failed';
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $offerPayload
|
|
*/
|
|
private function extractOfferImageUrl(array $offerPayload): ?string
|
|
{
|
|
$candidates = [
|
|
(string) ($offerPayload['imageUrl'] ?? ''),
|
|
(string) ($offerPayload['image']['url'] ?? ''),
|
|
];
|
|
|
|
$images = $offerPayload['images'] ?? null;
|
|
if (is_array($images)) {
|
|
$firstImage = $images[0] ?? null;
|
|
if (is_array($firstImage)) {
|
|
$candidates[] = (string) ($firstImage['url'] ?? '');
|
|
} elseif (is_string($firstImage)) {
|
|
$candidates[] = $firstImage;
|
|
}
|
|
}
|
|
|
|
$productSet = $offerPayload['productSet'] ?? null;
|
|
if (is_array($productSet)) {
|
|
$firstSet = $productSet[0] ?? null;
|
|
if (is_array($firstSet)) {
|
|
$product = is_array($firstSet['product'] ?? null) ? $firstSet['product'] : [];
|
|
$productImage = is_array($product['images'] ?? null) ? ($product['images'][0] ?? null) : null;
|
|
if (is_array($productImage)) {
|
|
$candidates[] = (string) ($productImage['url'] ?? '');
|
|
} elseif (is_string($productImage)) {
|
|
$candidates[] = $productImage;
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach ($candidates as $candidate) {
|
|
$url = trim($candidate);
|
|
if ($url === '') {
|
|
continue;
|
|
}
|
|
if (str_starts_with($url, '//')) {
|
|
return 'https:' . $url;
|
|
}
|
|
if (!str_starts_with($url, 'http://') && !str_starts_with($url, 'https://')) {
|
|
continue;
|
|
}
|
|
return $url;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $itemRaw
|
|
*/
|
|
private function extractLineItemImageUrl(array $itemRaw): ?string
|
|
{
|
|
$offer = is_array($itemRaw['offer'] ?? null) ? $itemRaw['offer'] : [];
|
|
|
|
$candidates = [
|
|
(string) ($itemRaw['imageUrl'] ?? ''),
|
|
(string) ($offer['imageUrl'] ?? ''),
|
|
(string) ($offer['image']['url'] ?? ''),
|
|
];
|
|
|
|
$images = $offer['images'] ?? null;
|
|
if (is_array($images)) {
|
|
$firstImage = $images[0] ?? null;
|
|
if (is_array($firstImage)) {
|
|
$candidates[] = (string) ($firstImage['url'] ?? '');
|
|
} elseif (is_string($firstImage)) {
|
|
$candidates[] = $firstImage;
|
|
}
|
|
}
|
|
|
|
foreach ($candidates as $candidate) {
|
|
$url = trim($candidate);
|
|
if ($url === '') {
|
|
continue;
|
|
}
|
|
if (str_starts_with($url, '//')) {
|
|
return 'https:' . $url;
|
|
}
|
|
if (!str_starts_with($url, 'http://') && !str_starts_with($url, 'https://')) {
|
|
continue;
|
|
}
|
|
return $url;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $payment
|
|
* @return array<int, array<string, mixed>>
|
|
*/
|
|
private function buildPayments(array $payment, string $fallbackCurrency): array
|
|
{
|
|
$paymentId = trim((string) ($payment['id'] ?? ''));
|
|
if ($paymentId === '') {
|
|
return [];
|
|
}
|
|
|
|
$amount = $this->amountToFloat($payment['paidAmount'] ?? null);
|
|
if ($amount === null) {
|
|
$amount = $this->amountToFloat($payment['amount'] ?? null);
|
|
}
|
|
|
|
return [[
|
|
'source_payment_id' => $paymentId,
|
|
'external_payment_id' => $paymentId,
|
|
'payment_type_id' => trim((string) ($payment['type'] ?? 'allegro')),
|
|
'payment_date' => StringHelper::normalizeDateTime((string) ($payment['finishedAt'] ?? '')),
|
|
'amount' => $amount,
|
|
'currency' => StringHelper::nullableString((string) ($payment['amount']['currency'] ?? $fallbackCurrency)),
|
|
'comment' => StringHelper::nullableString((string) ($payment['provider'] ?? '')),
|
|
'payload_json' => $payment,
|
|
]];
|
|
}
|
|
|
|
/**
|
|
* @param array<int, array<string, mixed>> $apiShipments
|
|
* @param array<string, mixed> $delivery
|
|
* @return array<int, array<string, mixed>>
|
|
*/
|
|
private function buildShipments(array $apiShipments, array $delivery): array
|
|
{
|
|
$result = [];
|
|
foreach ($apiShipments as $shipmentRaw) {
|
|
if (!is_array($shipmentRaw)) {
|
|
continue;
|
|
}
|
|
$trackingNumber = trim((string) ($shipmentRaw['waybill'] ?? ''));
|
|
if ($trackingNumber === '') {
|
|
continue;
|
|
}
|
|
|
|
$carrierId = trim((string) ($shipmentRaw['carrierId'] ?? $delivery['method']['id'] ?? ''));
|
|
$carrierName = trim((string) ($shipmentRaw['carrierName'] ?? ''));
|
|
$result[] = [
|
|
'source_shipment_id' => StringHelper::nullableString((string) ($shipmentRaw['id'] ?? '')),
|
|
'external_shipment_id' => StringHelper::nullableString((string) ($shipmentRaw['id'] ?? '')),
|
|
'tracking_number' => $trackingNumber,
|
|
'carrier_provider_id' => $carrierId !== '' ? $carrierId : ($carrierName !== '' ? $carrierName : 'allegro'),
|
|
'posted_at' => StringHelper::normalizeDateTime((string) ($shipmentRaw['createdAt'] ?? '')),
|
|
'media_uuid' => null,
|
|
'payload_json' => $shipmentRaw,
|
|
];
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $payload
|
|
* @return array<int, array<string, mixed>>
|
|
*/
|
|
private function buildNotes(array $payload): array
|
|
{
|
|
$message = trim((string) ($payload['messageToSeller'] ?? ''));
|
|
if ($message === '') {
|
|
return [];
|
|
}
|
|
|
|
return [[
|
|
'source_note_id' => null,
|
|
'note_type' => 'buyer_message',
|
|
'created_at_external' => StringHelper::normalizeDateTime((string) ($payload['updatedAt'] ?? '')),
|
|
'comment' => $message,
|
|
'payload_json' => ['messageToSeller' => $message],
|
|
]];
|
|
}
|
|
|
|
private function mapPaymentStatus(string $status): ?int
|
|
{
|
|
return match ($status) {
|
|
'paid', 'finished', 'completed' => 2,
|
|
'partially_paid', 'in_progress' => 1,
|
|
'cancelled', 'canceled', 'failed', 'unpaid' => 0,
|
|
default => null,
|
|
};
|
|
}
|
|
|
|
private function amountToFloat(mixed $amountNode): ?float
|
|
{
|
|
if (!is_array($amountNode)) {
|
|
return null;
|
|
}
|
|
$value = trim((string) ($amountNode['amount'] ?? ''));
|
|
if ($value === '' || !is_numeric($value)) {
|
|
return null;
|
|
}
|
|
|
|
return (float) $value;
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $address
|
|
*/
|
|
private function fallbackName(array $address, string $fallback): string
|
|
{
|
|
$name = trim((string) (($address['firstName'] ?? '') . ' ' . ($address['lastName'] ?? '')));
|
|
if ($name !== '') {
|
|
return $name;
|
|
}
|
|
|
|
$company = trim((string) ($address['companyName'] ?? ''));
|
|
if ($company !== '') {
|
|
return $company;
|
|
}
|
|
|
|
return $fallback;
|
|
}
|
|
|
|
}
|