*/ 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 $payload * @return array{ * order:array, * addresses:array>, * items:array>, * image_diagnostics:array, * payments:array>, * shipments:array>, * notes:array>, * status_history:array> * } */ 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 $buyer * @param array $delivery * @param array $invoice * @return array> */ 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 $lineItems * @return array{ * items:array>, * image_diagnostics:array * } */ 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 $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 $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 $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 $payment * @return array> */ 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> $apiShipments * @param array $delivery * @return array> */ 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 $payload * @return array> */ 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 $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; } }