Files
orderPRO/src/Modules/Settings/AllegroOrderImportService.php
Jacek Pyziak 7ac4293df4 feat: Implement Allegro Order Sync and Status Management
- Added AllegroOrderSyncStateRepository for managing sync state with Allegro orders.
- Introduced AllegroOrdersSyncService to handle the synchronization of orders from Allegro.
- Created AllegroStatusDiscoveryService to discover and store order statuses from Allegro.
- Developed AllegroStatusMappingRepository for managing status mappings between Allegro and OrderPro.
- Implemented AllegroStatusSyncService to facilitate status synchronization.
- Added CronSettingsController for managing cron job settings related to Allegro integration.
2026-03-04 23:21:35 +01:00

802 lines
31 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Modules\Settings;
use App\Modules\Orders\OrderImportRepository;
use DateInterval;
use DateTimeImmutable;
use RuntimeException;
use Throwable;
final class AllegroOrderImportService
{
public function __construct(
private readonly AllegroIntegrationRepository $integrationRepository,
private readonly AllegroOAuthClient $oauthClient,
private readonly AllegroApiClient $apiClient,
private readonly OrderImportRepository $orders,
private readonly AllegroStatusMappingRepository $statusMappings
) {
}
/**
* @return array<string, mixed>
*/
public function importSingleOrder(string $checkoutFormId): array
{
$orderId = trim($checkoutFormId);
if ($orderId === '') {
throw new RuntimeException('Podaj ID zamowienia Allegro.');
}
$oauth = $this->requireOAuthData();
[$accessToken, $oauth] = $this->resolveAccessToken($oauth);
try {
$payload = $this->apiClient->getCheckoutForm(
(string) ($oauth['environment'] ?? 'sandbox'),
$accessToken,
$orderId
);
} catch (RuntimeException $exception) {
if (trim($exception->getMessage()) !== 'ALLEGRO_HTTP_401') {
throw $exception;
}
[$accessToken, $oauth] = $this->forceRefreshToken($oauth);
$payload = $this->apiClient->getCheckoutForm(
(string) ($oauth['environment'] ?? 'sandbox'),
$accessToken,
$orderId
);
}
$mapped = $this->mapCheckoutFormPayload(
$payload,
(string) ($oauth['environment'] ?? 'sandbox'),
$accessToken
);
$saveResult = $this->orders->upsertOrderAggregate(
$mapped['order'],
$mapped['addresses'],
$mapped['items'],
$mapped['payments'],
$mapped['shipments'],
$mapped['notes'],
$mapped['status_history']
);
return [
'order_id' => (int) ($saveResult['order_id'] ?? 0),
'created' => !empty($saveResult['created']),
'source_order_id' => (string) ($mapped['order']['source_order_id'] ?? ''),
'image_diagnostics' => (array) ($mapped['image_diagnostics'] ?? []),
];
}
/**
* @return array<string, string>
*/
private function requireOAuthData(): array
{
$oauth = $this->integrationRepository->getTokenCredentials();
if ($oauth === null) {
throw new RuntimeException('Brak kompletnych danych OAuth Allegro. Polacz konto ponownie.');
}
return $oauth;
}
/**
* @param array<string, string> $oauth
* @return array{0:string, 1:array<string, string>}
*/
private function resolveAccessToken(array $oauth): array
{
$tokenExpiresAt = trim((string) ($oauth['token_expires_at'] ?? ''));
$accessToken = trim((string) ($oauth['access_token'] ?? ''));
if ($accessToken === '') {
return $this->forceRefreshToken($oauth);
}
if ($tokenExpiresAt === '') {
return [$accessToken, $oauth];
}
try {
$expiresAt = new DateTimeImmutable($tokenExpiresAt);
} catch (Throwable) {
return $this->forceRefreshToken($oauth);
}
if ($expiresAt <= (new DateTimeImmutable('now'))->add(new DateInterval('PT5M'))) {
return $this->forceRefreshToken($oauth);
}
return [$accessToken, $oauth];
}
/**
* @param array<string, string> $oauth
* @return array{0:string, 1:array<string, string>}
*/
private function forceRefreshToken(array $oauth): array
{
$token = $this->oauthClient->refreshAccessToken(
(string) ($oauth['environment'] ?? 'sandbox'),
(string) ($oauth['client_id'] ?? ''),
(string) ($oauth['client_secret'] ?? ''),
(string) ($oauth['refresh_token'] ?? '')
);
$expiresAt = null;
$expiresIn = max(0, (int) ($token['expires_in'] ?? 0));
if ($expiresIn > 0) {
$expiresAt = (new DateTimeImmutable('now'))
->add(new DateInterval('PT' . $expiresIn . 'S'))
->format('Y-m-d H:i:s');
}
$refreshToken = trim((string) ($token['refresh_token'] ?? ''));
if ($refreshToken === '') {
$refreshToken = (string) ($oauth['refresh_token'] ?? '');
}
$this->integrationRepository->saveTokens(
(string) ($token['access_token'] ?? ''),
$refreshToken,
(string) ($token['token_type'] ?? ''),
(string) ($token['scope'] ?? ''),
$expiresAt
);
$updatedOauth = $this->requireOAuthData();
$newAccessToken = trim((string) ($updatedOauth['access_token'] ?? ''));
if ($newAccessToken === '') {
throw new RuntimeException('Nie udalo sie zapisac odswiezonego tokenu Allegro.');
}
return [$newAccessToken, $updatedOauth];
}
/**
* @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 RuntimeException('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 = $this->normalizeDateTime((string) ($dispatchTime['from'] ?? ''));
$sendDateMax = $this->normalizeDateTime((string) ($dispatchTime['to'] ?? ''));
if ($sendDateMin === null) {
$sendDateMin = $this->normalizeDateTime((string) ($deliveryTime['from'] ?? ''));
}
if ($sendDateMax === null) {
$sendDateMax = $this->normalizeDateTime((string) ($deliveryTime['to'] ?? ''));
}
$boughtAt = $this->normalizeDateTime((string) ($payload['boughtAt'] ?? ''));
$updatedAt = $this->normalizeDateTime((string) ($payload['updatedAt'] ?? ''));
$fetchedAt = date('Y-m-d H:i:s');
$order = [
'integration_id' => null,
'source' => '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);
$shipments = $this->buildShipments($payload, $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' => $this->nullableString((string) ($buyer['phoneNumber'] ?? '')),
'email' => $this->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 !== [];
$name = $isPickupPointDelivery
? $this->nullableString((string) ($pickupPoint['name'] ?? ''))
: $this->fallbackName($deliveryAddress, 'Dostawa');
if ($name === null) {
$name = 'Dostawa';
}
$street = $isPickupPointDelivery
? $this->nullableString((string) ($pickupAddress['street'] ?? ''))
: $this->nullableString((string) ($deliveryAddress['street'] ?? ''));
$city = $isPickupPointDelivery
? $this->nullableString((string) ($pickupAddress['city'] ?? ''))
: $this->nullableString((string) ($deliveryAddress['city'] ?? ''));
$zipCode = $isPickupPointDelivery
? $this->nullableString((string) ($pickupAddress['zipCode'] ?? ''))
: $this->nullableString((string) ($deliveryAddress['zipCode'] ?? ''));
$country = $isPickupPointDelivery
? $this->nullableString((string) ($pickupAddress['countryCode'] ?? ''))
: $this->nullableString((string) ($deliveryAddress['countryCode'] ?? ''));
$result[] = [
'address_type' => 'delivery',
'name' => $name,
'phone' => $this->nullableString((string) ($deliveryAddress['phoneNumber'] ?? '')),
'email' => $this->nullableString((string) ($deliveryAddress['email'] ?? $buyer['email'] ?? '')),
'street_name' => $street,
'street_number' => null,
'city' => $city,
'zip_code' => $zipCode,
'country' => $country,
'department' => null,
'parcel_external_id' => $this->nullableString((string) ($pickupPoint['id'] ?? '')),
'parcel_name' => $this->nullableString((string) ($pickupPoint['name'] ?? '')),
'address_class' => null,
'company_tax_number' => null,
'company_name' => $this->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' => $this->nullableString((string) ($invoiceAddress['phoneNumber'] ?? '')),
'email' => $this->nullableString((string) ($invoiceAddress['email'] ?? '')),
'street_name' => $this->nullableString((string) ($invoiceAddress['street'] ?? '')),
'street_number' => null,
'city' => $this->nullableString((string) ($invoiceAddress['city'] ?? '')),
'zip_code' => $this->nullableString((string) ($invoiceAddress['zipCode'] ?? '')),
'country' => $this->nullableString((string) ($invoiceAddress['countryCode'] ?? '')),
'department' => null,
'parcel_external_id' => null,
'parcel_name' => null,
'address_class' => null,
'company_tax_number' => $this->nullableString((string) ($invoiceAddress['taxId'] ?? '')),
'company_name' => $this->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' => $this->nullableString((string) ($itemRaw['id'] ?? '')),
'external_item_id' => $this->nullableString((string) ($offer['id'] ?? '')),
'ean' => null,
'sku' => null,
'original_name' => $name,
'original_code' => $this->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' => $this->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' => $this->normalizeDateTime((string) ($payment['finishedAt'] ?? '')),
'amount' => $amount,
'currency' => $this->nullableString((string) ($payment['amount']['currency'] ?? $fallbackCurrency)),
'comment' => $this->nullableString((string) ($payment['provider'] ?? '')),
'payload_json' => $payment,
]];
}
/**
* @param array<string, mixed> $payload
* @param array<string, mixed> $delivery
* @return array<int, array<string, mixed>>
*/
private function buildShipments(array $payload, array $delivery): array
{
$shipments = is_array($payload['fulfillment']['shipments'] ?? null)
? $payload['fulfillment']['shipments']
: [];
$result = [];
foreach ($shipments as $shipmentRaw) {
if (!is_array($shipmentRaw)) {
continue;
}
$trackingNumber = trim((string) ($shipmentRaw['waybill'] ?? $shipmentRaw['trackingNumber'] ?? ''));
if ($trackingNumber === '') {
continue;
}
$carrierId = trim((string) ($shipmentRaw['carrierId'] ?? $delivery['method']['id'] ?? 'allegro'));
$result[] = [
'source_shipment_id' => $this->nullableString((string) ($shipmentRaw['id'] ?? '')),
'external_shipment_id' => $this->nullableString((string) ($shipmentRaw['id'] ?? '')),
'tracking_number' => $trackingNumber,
'carrier_provider_id' => $carrierId !== '' ? $carrierId : 'allegro',
'posted_at' => $this->normalizeDateTime((string) ($shipmentRaw['createdAt'] ?? $payload['updatedAt'] ?? '')),
'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' => $this->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;
}
private function normalizeDateTime(string $value): ?string
{
$trimmed = trim($value);
if ($trimmed === '') {
return null;
}
try {
return (new DateTimeImmutable($trimmed))->format('Y-m-d H:i:s');
} catch (Throwable) {
return null;
}
}
/**
* @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;
}
private function nullableString(string $value): ?string
{
$trimmed = trim($value);
return $trimmed === '' ? null : $trimmed;
}
}