From 42e647f0071349bbd788ff9541be5075e9372411 Mon Sep 17 00:00:00 2001 From: Jacek Pyziak Date: Fri, 13 Mar 2026 12:33:12 +0100 Subject: [PATCH] feat(06-sonarqube-quality): extract long methods to fix S138 violations (06-06) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ShopproOrdersSyncService: sync() 195→44 lines via syncOneIntegration, fetchOrdersPage, processPageCandidates, importOneOrder; mapAddresses() 166→34 lines via buildCustomerAddress, buildDeliveryAddress. OrdersRepository: paginate() 183→69 lines via buildPaginateFilters, buildListSql, transformOrderRow; findDetails() 101→40 lines via loadOrderAddresses/Items/Payments/Shipments/Documents/Notes/StatusHistory. SonarQube S138 violations: 4 → 0. Co-Authored-By: Claude Sonnet 4.6 --- src/Modules/Orders/OrdersRepository.php | 400 +++++++++------- .../Settings/ShopproOrdersSyncService.php | 447 ++++++++++-------- 2 files changed, 474 insertions(+), 373 deletions(-) diff --git a/src/Modules/Orders/OrdersRepository.php b/src/Modules/Orders/OrdersRepository.php index 61e337c..8580615 100644 --- a/src/Modules/Orders/OrdersRepository.php +++ b/src/Modules/Orders/OrdersRepository.php @@ -24,21 +24,78 @@ final class OrdersRepository $page = max(1, (int) ($filters['page'] ?? 1)); $perPage = max(1, min(100, (int) ($filters['per_page'] ?? 20))); $offset = ($page - 1) * $perPage; - - $where = []; - $params = []; $effectiveStatusSql = $this->effectiveStatusSql('o', 'asm'); $effectiveOrderedAtSql = $this->effectiveOrderedAtSql('o'); + ['where' => $where, 'params' => $params] = $this->buildPaginateFilters($filters, $effectiveStatusSql, $effectiveOrderedAtSql); + $whereSql = $where === [] ? '' : (' WHERE ' . implode(' AND ', $where)); + + $sort = (string) ($filters['sort'] ?? 'ordered_at'); + $sortDir = strtoupper((string) ($filters['sort_dir'] ?? 'DESC')) === 'ASC' ? 'ASC' : 'DESC'; + $sortColumn = match ($sort) { + 'source_order_id' => 'o.source_order_id', + 'external_order_id' => 'o.external_order_id', + 'external_status_id' => 'o.external_status_id', + 'payment_status' => 'o.payment_status', + 'total_with_tax' => 'o.total_with_tax', + 'total_paid' => 'o.total_paid', + 'source_updated_at' => 'o.source_updated_at', + 'fetched_at' => 'o.fetched_at', + 'id' => 'o.id', + default => $effectiveOrderedAtSql, + }; + + try { + $countSql = 'SELECT COUNT(*) FROM orders o ' + . 'LEFT JOIN order_addresses a ON a.order_id = o.id AND a.address_type = "customer" ' + . 'LEFT JOIN allegro_order_status_mappings asm ON o.source = "allegro" AND LOWER(o.external_status_id) = asm.allegro_status_code' + . $whereSql; + $countStmt = $this->pdo->prepare($countSql); + $countStmt->execute($params); + $total = (int) $countStmt->fetchColumn(); + + $listSql = $this->buildListSql($effectiveStatusSql, $effectiveOrderedAtSql, $whereSql, $sortColumn, $sortDir); + $stmt = $this->pdo->prepare($listSql); + foreach ($params as $key => $value) { + $stmt->bindValue(':' . $key, $value, is_int($value) ? PDO::PARAM_INT : PDO::PARAM_STR); + } + $stmt->bindValue(':limit', $perPage, PDO::PARAM_INT); + $stmt->bindValue(':offset', $offset, PDO::PARAM_INT); + $stmt->execute(); + + $rows = $stmt->fetchAll(); + if (!is_array($rows)) { + $rows = []; + } + $itemPreviewsByOrderId = $this->loadOrderItemsPreviews(array_map( + static fn (array $row): int => (int) ($row['id'] ?? 0), + $rows + )); + + return [ + 'items' => array_map(fn (array $row) => $this->transformOrderRow($row, $itemPreviewsByOrderId), $rows), + 'total' => $total, + 'page' => $page, + 'per_page' => $perPage, + 'error' => '', + ]; + } catch (Throwable $exception) { + return ['items' => [], 'total' => 0, 'page' => $page, 'per_page' => $perPage, 'error' => $exception->getMessage()]; + } + } + + /** + * @param array $filters + * @return array{where:array,params:array} + */ + private function buildPaginateFilters(array $filters, string $effectiveStatusSql, string $effectiveOrderedAtSql): array + { + $where = []; + $params = []; + $search = trim((string) ($filters['search'] ?? '')); if ($search !== '') { - $where[] = '(' - . 'o.source_order_id LIKE :search ' - . 'OR o.external_order_id LIKE :search ' - . 'OR o.customer_login LIKE :search ' - . 'OR a.name LIKE :search ' - . 'OR a.email LIKE :search' - . ')'; + $where[] = '(o.source_order_id LIKE :search OR o.external_order_id LIKE :search OR o.customer_login LIKE :search OR a.name LIKE :search OR a.email LIKE :search)'; $params['search'] = '%' . $search . '%'; } @@ -72,33 +129,12 @@ final class OrdersRepository $params['date_to'] = $dateTo . ' 23:59:59'; } - $whereSql = $where === [] ? '' : (' WHERE ' . implode(' AND ', $where)); + return ['where' => $where, 'params' => $params]; + } - $sort = (string) ($filters['sort'] ?? 'ordered_at'); - $sortDir = strtoupper((string) ($filters['sort_dir'] ?? 'DESC')) === 'ASC' ? 'ASC' : 'DESC'; - $sortColumn = match ($sort) { - 'source_order_id' => 'o.source_order_id', - 'external_order_id' => 'o.external_order_id', - 'external_status_id' => 'o.external_status_id', - 'payment_status' => 'o.payment_status', - 'total_with_tax' => 'o.total_with_tax', - 'total_paid' => 'o.total_paid', - 'source_updated_at' => 'o.source_updated_at', - 'fetched_at' => 'o.fetched_at', - 'id' => 'o.id', - default => $effectiveOrderedAtSql, - }; - - try { - $countSql = 'SELECT COUNT(*) FROM orders o ' - . 'LEFT JOIN order_addresses a ON a.order_id = o.id AND a.address_type = "customer" ' - . 'LEFT JOIN allegro_order_status_mappings asm ON o.source = "allegro" AND LOWER(o.external_status_id) = asm.allegro_status_code' - . $whereSql; - $countStmt = $this->pdo->prepare($countSql); - $countStmt->execute($params); - $total = (int) $countStmt->fetchColumn(); - - $listSql = 'SELECT + private function buildListSql(string $effectiveStatusSql, string $effectiveOrderedAtSql, string $whereSql, string $sortColumn, string $sortDir): string + { + return 'SELECT o.id, o.internal_order_number, o.source, @@ -129,78 +165,49 @@ final class OrdersRepository FROM orders o LEFT JOIN order_addresses a ON a.order_id = o.id AND a.address_type = "customer" LEFT JOIN allegro_order_status_mappings asm ON o.source = "allegro" AND LOWER(o.external_status_id) = asm.allegro_status_code' - . $whereSql - . ' ORDER BY ' . $sortColumn . ' ' . $sortDir - . ' LIMIT :limit OFFSET :offset'; + . $whereSql + . ' ORDER BY ' . $sortColumn . ' ' . $sortDir + . ' LIMIT :limit OFFSET :offset'; + } - $stmt = $this->pdo->prepare($listSql); - foreach ($params as $key => $value) { - if (is_int($value)) { - $stmt->bindValue(':' . $key, $value, PDO::PARAM_INT); - } else { - $stmt->bindValue(':' . $key, $value); - } - } - $stmt->bindValue(':limit', $perPage, PDO::PARAM_INT); - $stmt->bindValue(':offset', $offset, PDO::PARAM_INT); - $stmt->execute(); + /** + * @param array $row + * @param array> $itemPreviewsByOrderId + * @return array + */ + private function transformOrderRow(array $row, array $itemPreviewsByOrderId): array + { + $orderId = (int) ($row['id'] ?? 0); - $rows = $stmt->fetchAll(); - if (!is_array($rows)) { - $rows = []; - } - $itemPreviewsByOrderId = $this->loadOrderItemsPreviews(array_map( - static fn (array $row): int => (int) ($row['id'] ?? 0), - $rows - )); - - return [ - 'items' => array_map(static function (array $row) use ($itemPreviewsByOrderId): array { - $orderId = (int) ($row['id'] ?? 0); - return [ - 'id' => $orderId, - 'internal_order_number' => (string) ($row['internal_order_number'] ?? ''), - 'source' => (string) ($row['source'] ?? ''), - 'source_order_id' => (string) ($row['source_order_id'] ?? ''), - 'external_order_id' => (string) ($row['external_order_id'] ?? ''), - 'external_status_id' => (string) ($row['external_status_id'] ?? ''), - 'effective_status_id' => (string) ($row['effective_status_id'] ?? ''), - 'payment_status' => isset($row['payment_status']) ? (int) $row['payment_status'] : null, - 'currency' => (string) ($row['currency'] ?? ''), - 'total_with_tax' => $row['total_with_tax'] !== null ? (float) $row['total_with_tax'] : null, - 'total_paid' => $row['total_paid'] !== null ? (float) $row['total_paid'] : null, - 'ordered_at' => (string) ($row['effective_ordered_at'] ?? ''), - 'source_created_at' => (string) ($row['source_created_at'] ?? ''), - 'source_updated_at' => (string) ($row['source_updated_at'] ?? ''), - 'fetched_at' => (string) ($row['fetched_at'] ?? ''), - 'is_invoice' => (int) ($row['is_invoice'] ?? 0) === 1, - 'is_canceled_by_buyer' => (int) ($row['is_canceled_by_buyer'] ?? 0) === 1, - 'external_carrier_id' => (string) ($row['external_carrier_id'] ?? ''), - 'external_payment_type_id' => (string) ($row['external_payment_type_id'] ?? ''), - 'buyer_name' => (string) ($row['buyer_name'] ?? ''), - 'buyer_email' => (string) ($row['buyer_email'] ?? ''), - 'buyer_city' => (string) ($row['buyer_city'] ?? ''), - 'items_count' => (int) ($row['items_count'] ?? 0), - 'items_qty' => (float) ($row['items_qty'] ?? 0), - 'shipments_count' => (int) ($row['shipments_count'] ?? 0), - 'documents_count' => (int) ($row['documents_count'] ?? 0), - 'items_preview' => (array) ($itemPreviewsByOrderId[$orderId] ?? []), - ]; - }, $rows), - 'total' => $total, - 'page' => $page, - 'per_page' => $perPage, - 'error' => '', - ]; - } catch (Throwable $exception) { - return [ - 'items' => [], - 'total' => 0, - 'page' => $page, - 'per_page' => $perPage, - 'error' => $exception->getMessage(), - ]; - } + return [ + 'id' => $orderId, + 'internal_order_number' => (string) ($row['internal_order_number'] ?? ''), + 'source' => (string) ($row['source'] ?? ''), + 'source_order_id' => (string) ($row['source_order_id'] ?? ''), + 'external_order_id' => (string) ($row['external_order_id'] ?? ''), + 'external_status_id' => (string) ($row['external_status_id'] ?? ''), + 'effective_status_id' => (string) ($row['effective_status_id'] ?? ''), + 'payment_status' => isset($row['payment_status']) ? (int) $row['payment_status'] : null, + 'currency' => (string) ($row['currency'] ?? ''), + 'total_with_tax' => $row['total_with_tax'] !== null ? (float) $row['total_with_tax'] : null, + 'total_paid' => $row['total_paid'] !== null ? (float) $row['total_paid'] : null, + 'ordered_at' => (string) ($row['effective_ordered_at'] ?? ''), + 'source_created_at' => (string) ($row['source_created_at'] ?? ''), + 'source_updated_at' => (string) ($row['source_updated_at'] ?? ''), + 'fetched_at' => (string) ($row['fetched_at'] ?? ''), + 'is_invoice' => (int) ($row['is_invoice'] ?? 0) === 1, + 'is_canceled_by_buyer' => (int) ($row['is_canceled_by_buyer'] ?? 0) === 1, + 'external_carrier_id' => (string) ($row['external_carrier_id'] ?? ''), + 'external_payment_type_id' => (string) ($row['external_payment_type_id'] ?? ''), + 'buyer_name' => (string) ($row['buyer_name'] ?? ''), + 'buyer_email' => (string) ($row['buyer_email'] ?? ''), + 'buyer_city' => (string) ($row['buyer_city'] ?? ''), + 'items_count' => (int) ($row['items_count'] ?? 0), + 'items_qty' => (float) ($row['items_qty'] ?? 0), + 'shipments_count' => (int) ($row['shipments_count'] ?? 0), + 'documents_count' => (int) ($row['documents_count'] ?? 0), + 'items_preview' => (array) ($itemPreviewsByOrderId[$orderId] ?? []), + ]; } /** @@ -414,87 +421,122 @@ final class OrdersRepository return null; } - $addressesStmt = $this->pdo->prepare('SELECT * FROM order_addresses WHERE order_id = :order_id ORDER BY FIELD(address_type, "customer", "invoice", "delivery"), id ASC'); - $addressesStmt->execute(['order_id' => $orderId]); - $addresses = $addressesStmt->fetchAll(PDO::FETCH_ASSOC); - if (!is_array($addresses)) { - $addresses = []; - } - - $itemsMediaSql = $this->resolvedMediaUrlSql('oi', 'o.source'); - $itemsStmt = $this->pdo->prepare('SELECT oi.*, ' . $itemsMediaSql . ' AS resolved_media_url - FROM order_items oi - INNER JOIN orders o ON o.id = oi.order_id - WHERE oi.order_id = :order_id - ORDER BY oi.sort_order ASC, oi.id ASC'); - $itemsStmt->execute(['order_id' => $orderId]); - $items = $itemsStmt->fetchAll(PDO::FETCH_ASSOC); - if (!is_array($items)) { - $items = []; - } - $items = array_map(static function (array $row): array { - $resolvedMediaUrl = trim((string) ($row['resolved_media_url'] ?? '')); - if ($resolvedMediaUrl !== '') { - $row['media_url'] = $resolvedMediaUrl; - } - unset($row['resolved_media_url']); - - return $row; - }, $items); - - $paymentsStmt = $this->pdo->prepare('SELECT * FROM order_payments WHERE order_id = :order_id ORDER BY payment_date ASC, id ASC'); - $paymentsStmt->execute(['order_id' => $orderId]); - $payments = $paymentsStmt->fetchAll(PDO::FETCH_ASSOC); - if (!is_array($payments)) { - $payments = []; - } - - $shipmentsStmt = $this->pdo->prepare('SELECT * FROM order_shipments WHERE order_id = :order_id ORDER BY posted_at ASC, id ASC'); - $shipmentsStmt->execute(['order_id' => $orderId]); - $shipments = $shipmentsStmt->fetchAll(PDO::FETCH_ASSOC); - if (!is_array($shipments)) { - $shipments = []; - } - - $documentsStmt = $this->pdo->prepare('SELECT * FROM order_documents WHERE order_id = :order_id ORDER BY source_created_at ASC, id ASC'); - $documentsStmt->execute(['order_id' => $orderId]); - $documents = $documentsStmt->fetchAll(PDO::FETCH_ASSOC); - if (!is_array($documents)) { - $documents = []; - } - - $notesStmt = $this->pdo->prepare('SELECT * FROM order_notes WHERE order_id = :order_id ORDER BY created_at_external DESC, id DESC'); - $notesStmt->execute(['order_id' => $orderId]); - $notes = $notesStmt->fetchAll(PDO::FETCH_ASSOC); - if (!is_array($notes)) { - $notes = []; - } - - $historyStmt = $this->pdo->prepare('SELECT * FROM order_status_history WHERE order_id = :order_id ORDER BY changed_at DESC, id DESC'); - $historyStmt->execute(['order_id' => $orderId]); - $history = $historyStmt->fetchAll(PDO::FETCH_ASSOC); - if (!is_array($history)) { - $history = []; - } - - $activityLog = $this->loadActivityLog($orderId); - return [ 'order' => $order, - 'addresses' => $addresses, - 'items' => $items, - 'payments' => $payments, - 'shipments' => $shipments, - 'documents' => $documents, - 'notes' => $notes, - 'status_history' => $history, - 'activity_log' => $activityLog, + 'addresses' => $this->loadOrderAddresses($orderId), + 'items' => $this->loadOrderItems($orderId), + 'payments' => $this->loadOrderPayments($orderId), + 'shipments' => $this->loadOrderShipments($orderId), + 'documents' => $this->loadOrderDocuments($orderId), + 'notes' => $this->loadOrderNotes($orderId), + 'status_history' => $this->loadOrderStatusHistory($orderId), + 'activity_log' => $this->loadActivityLog($orderId), ]; } catch (Throwable) { return null; } } + /** + * @return array> + */ + private function loadOrderAddresses(int $orderId): array + { + $stmt = $this->pdo->prepare('SELECT * FROM order_addresses WHERE order_id = :order_id ORDER BY FIELD(address_type, "customer", "invoice", "delivery"), id ASC'); + $stmt->execute(['order_id' => $orderId]); + $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); + + return is_array($rows) ? $rows : []; + } + + /** + * @return array> + */ + private function loadOrderItems(int $orderId): array + { + $itemsMediaSql = $this->resolvedMediaUrlSql('oi', 'o.source'); + $stmt = $this->pdo->prepare('SELECT oi.*, ' . $itemsMediaSql . ' AS resolved_media_url + FROM order_items oi + INNER JOIN orders o ON o.id = oi.order_id + WHERE oi.order_id = :order_id + ORDER BY oi.sort_order ASC, oi.id ASC'); + $stmt->execute(['order_id' => $orderId]); + $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); + if (!is_array($rows)) { + return []; + } + + return array_map(static function (array $row): array { + $resolvedMediaUrl = trim((string) ($row['resolved_media_url'] ?? '')); + if ($resolvedMediaUrl !== '') { + $row['media_url'] = $resolvedMediaUrl; + } + unset($row['resolved_media_url']); + + return $row; + }, $rows); + } + + /** + * @return array> + */ + private function loadOrderPayments(int $orderId): array + { + $stmt = $this->pdo->prepare('SELECT * FROM order_payments WHERE order_id = :order_id ORDER BY payment_date ASC, id ASC'); + $stmt->execute(['order_id' => $orderId]); + $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); + + return is_array($rows) ? $rows : []; + } + + /** + * @return array> + */ + private function loadOrderShipments(int $orderId): array + { + $stmt = $this->pdo->prepare('SELECT * FROM order_shipments WHERE order_id = :order_id ORDER BY posted_at ASC, id ASC'); + $stmt->execute(['order_id' => $orderId]); + $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); + + return is_array($rows) ? $rows : []; + } + + /** + * @return array> + */ + private function loadOrderDocuments(int $orderId): array + { + $stmt = $this->pdo->prepare('SELECT * FROM order_documents WHERE order_id = :order_id ORDER BY source_created_at ASC, id ASC'); + $stmt->execute(['order_id' => $orderId]); + $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); + + return is_array($rows) ? $rows : []; + } + + /** + * @return array> + */ + private function loadOrderNotes(int $orderId): array + { + $stmt = $this->pdo->prepare('SELECT * FROM order_notes WHERE order_id = :order_id ORDER BY created_at_external DESC, id DESC'); + $stmt->execute(['order_id' => $orderId]); + $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); + + return is_array($rows) ? $rows : []; + } + + /** + * @return array> + */ + private function loadOrderStatusHistory(int $orderId): array + { + $stmt = $this->pdo->prepare('SELECT * FROM order_status_history WHERE order_id = :order_id ORDER BY changed_at DESC, id DESC'); + $stmt->execute(['order_id' => $orderId]); + $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); + + return is_array($rows) ? $rows : []; + } + /** * @param array $orderIds * @return array> diff --git a/src/Modules/Settings/ShopproOrdersSyncService.php b/src/Modules/Settings/ShopproOrdersSyncService.php index 8039624..b52f7c5 100644 --- a/src/Modules/Settings/ShopproOrdersSyncService.php +++ b/src/Modules/Settings/ShopproOrdersSyncService.php @@ -60,169 +60,203 @@ final class ShopproOrdersSyncService } $result['checked_integrations'] = (int) $result['checked_integrations'] + 1; - $state = $this->syncState->getState($integrationId); - $this->syncState->markRunStarted($integrationId, new DateTimeImmutable('now')); - - try { - $statusMap = $this->buildStatusMap($integrationId); - $cursorUpdatedAt = StringHelper::nullableString((string) ($state['last_synced_updated_at'] ?? '')); - $cursorOrderId = StringHelper::nullableString((string) ($state['last_synced_source_order_id'] ?? '')); - $startDate = $this->resolveStartDate( - (string) ($integration['orders_fetch_start_date'] ?? ''), - $cursorUpdatedAt - ); - $baseUrl = trim((string) ($integration['base_url'] ?? '')); - $apiKey = $this->integrations->getApiKeyDecrypted($integrationId); - $timeout = max(1, min(120, (int) ($integration['timeout_seconds'] ?? 10))); - $productImageCache = []; - - if ($baseUrl === '' || $apiKey === null || trim($apiKey) === '') { - throw new \RuntimeException('Brak poprawnych danych API dla integracji.'); - } - - $latestUpdatedAt = $cursorUpdatedAt; - $latestOrderId = $cursorOrderId; - $shouldStop = false; - - for ($page = 1; $page <= $maxPages; $page++) { - $orders = $this->apiClient->fetchOrders($baseUrl, $apiKey, $timeout, $page, $pageLimit, $startDate); - if (($orders['ok'] ?? false) !== true) { - throw new \RuntimeException((string) ($orders['message'] ?? 'Blad pobierania listy zamowien.')); - } - - $items = is_array($orders['items'] ?? null) ? $orders['items'] : []; - if ($items === []) { - break; - } - - $candidates = $this->buildCandidates($items, $cursorUpdatedAt, $cursorOrderId); - foreach ($candidates as $candidate) { - if ((int) $result['processed'] >= $maxOrders) { - $shouldStop = true; - break; - } - - $sourceOrderId = (string) ($candidate['source_order_id'] ?? ''); - $sourceUpdatedAt = (string) ($candidate['source_updated_at'] ?? ''); - $rawOrder = is_array($candidate['payload'] ?? null) ? $candidate['payload'] : []; - - $details = $this->apiClient->fetchOrderById($baseUrl, $apiKey, $timeout, $sourceOrderId); - if (($details['ok'] ?? false) === true && is_array($details['order'] ?? null)) { - $detailsOrder = (array) $details['order']; - foreach ([ - 'products', - 'summary', - 'paid', - 'transport_cost', - 'transport', - 'transport_description', - 'client_name', - 'client_surname', - 'client_email', - 'client_phone', - 'client_city', - 'client_street', - 'client_postal_code', - ] as $protectedKey) { - if (array_key_exists($protectedKey, $rawOrder)) { - unset($detailsOrder[$protectedKey]); - } - } - $rawOrder = array_replace($rawOrder, $detailsOrder); - } - - try { - $productImages = $this->resolveProductImagesForOrder( - $baseUrl, - (string) $apiKey, - $timeout, - $rawOrder, - $productImageCache - ); - $aggregate = $this->mapOrderAggregate( - $integrationId, - $rawOrder, - $statusMap, - $sourceOrderId, - $sourceUpdatedAt, - $productImages - ); - $save = $this->orderImportRepository->upsertOrderAggregate( - $aggregate['order'], - $aggregate['addresses'], - $aggregate['items'], - $aggregate['payments'], - $aggregate['shipments'], - $aggregate['notes'], - $aggregate['status_history'] - ); - $result['processed'] = (int) $result['processed'] + 1; - if (!empty($save['created'])) { - $result['imported_created'] = (int) $result['imported_created'] + 1; - } else { - $result['imported_updated'] = (int) $result['imported_updated'] + 1; - } - - $this->orders->recordActivity( - (int) ($save['order_id'] ?? 0), - 'import', - 'Import zamowienia z shopPRO', - [ - 'integration_id' => $integrationId, - 'source_order_id' => $sourceOrderId, - ], - 'import', - 'shopPRO' - ); - } catch (Throwable $exception) { - $result['failed'] = (int) $result['failed'] + 1; - $errors = is_array($result['errors']) ? $result['errors'] : []; - if (count($errors) < 20) { - $errors[] = [ - 'integration_id' => $integrationId, - 'source_order_id' => $sourceOrderId, - 'error' => $exception->getMessage(), - ]; - } - $result['errors'] = $errors; - } - - if ($latestUpdatedAt === null || $sourceUpdatedAt > $latestUpdatedAt) { - $latestUpdatedAt = $sourceUpdatedAt; - $latestOrderId = $sourceOrderId; - } elseif ($latestUpdatedAt === $sourceUpdatedAt && ($latestOrderId === null || strcmp($sourceOrderId, $latestOrderId) > 0)) { - $latestOrderId = $sourceOrderId; - } - } - - if ($shouldStop || count($items) < $pageLimit) { - break; - } - } - - $this->syncState->markRunSuccess( - $integrationId, - new DateTimeImmutable('now'), - $latestUpdatedAt, - $latestOrderId - ); - } catch (Throwable $exception) { - $this->syncState->markRunFailed($integrationId, new DateTimeImmutable('now'), $exception->getMessage()); - $result['failed'] = (int) $result['failed'] + 1; - $errors = is_array($result['errors']) ? $result['errors'] : []; - if (count($errors) < 20) { - $errors[] = [ - 'integration_id' => $integrationId, - 'error' => $exception->getMessage(), - ]; - } - $result['errors'] = $errors; - } + $this->syncOneIntegration($integration, $maxPages, $pageLimit, $maxOrders, $result); } return $result; } + /** + * @param array $integration + * @param array $result + */ + private function syncOneIntegration(array $integration, int $maxPages, int $pageLimit, int $maxOrders, array &$result): void + { + $integrationId = (int) ($integration['id'] ?? 0); + $state = $this->syncState->getState($integrationId); + $this->syncState->markRunStarted($integrationId, new DateTimeImmutable('now')); + + try { + $statusMap = $this->buildStatusMap($integrationId); + $cursorUpdatedAt = StringHelper::nullableString((string) ($state['last_synced_updated_at'] ?? '')); + $cursorOrderId = StringHelper::nullableString((string) ($state['last_synced_source_order_id'] ?? '')); + $startDate = $this->resolveStartDate( + (string) ($integration['orders_fetch_start_date'] ?? ''), + $cursorUpdatedAt + ); + $baseUrl = trim((string) ($integration['base_url'] ?? '')); + $apiKey = $this->integrations->getApiKeyDecrypted($integrationId); + $timeout = max(1, min(120, (int) ($integration['timeout_seconds'] ?? 10))); + $productImageCache = []; + + if ($baseUrl === '' || $apiKey === null || trim($apiKey) === '') { + throw new \RuntimeException('Brak poprawnych danych API dla integracji.'); + } + + $latestUpdatedAt = $cursorUpdatedAt; + $latestOrderId = $cursorOrderId; + $shouldStop = false; + + for ($page = 1; $page <= $maxPages; $page++) { + $items = $this->fetchOrdersPage($baseUrl, (string) $apiKey, $timeout, $page, $pageLimit, $startDate); + if ($items === []) { + break; + } + $candidates = $this->buildCandidates($items, $cursorUpdatedAt, $cursorOrderId); + $this->processPageCandidates( + $candidates, $integrationId, $baseUrl, (string) $apiKey, $timeout, + $statusMap, $maxOrders, $result, $productImageCache, $shouldStop, + $latestUpdatedAt, $latestOrderId + ); + if ($shouldStop || count($items) < $pageLimit) { + break; + } + } + + $this->syncState->markRunSuccess($integrationId, new DateTimeImmutable('now'), $latestUpdatedAt, $latestOrderId); + } catch (Throwable $exception) { + $this->syncState->markRunFailed($integrationId, new DateTimeImmutable('now'), $exception->getMessage()); + $result['failed'] = (int) $result['failed'] + 1; + $errors = is_array($result['errors']) ? $result['errors'] : []; + if (count($errors) < 20) { + $errors[] = ['integration_id' => $integrationId, 'error' => $exception->getMessage()]; + } + $result['errors'] = $errors; + } + } + + /** + * @return array> + */ + private function fetchOrdersPage(string $baseUrl, string $apiKey, int $timeout, int $page, int $pageLimit, ?string $startDate): array + { + $orders = $this->apiClient->fetchOrders($baseUrl, $apiKey, $timeout, $page, $pageLimit, $startDate); + if (($orders['ok'] ?? false) !== true) { + throw new \RuntimeException((string) ($orders['message'] ?? 'Blad pobierania listy zamowien.')); + } + + return is_array($orders['items'] ?? null) ? $orders['items'] : []; + } + + /** + * @param array> $candidates + * @param array $statusMap + * @param array $result + * @param array $productImageCache + */ + private function processPageCandidates( + array $candidates, + int $integrationId, + string $baseUrl, + string $apiKey, + int $timeout, + array $statusMap, + int $maxOrders, + array &$result, + array &$productImageCache, + bool &$shouldStop, + ?string &$latestUpdatedAt, + ?string &$latestOrderId + ): void { + foreach ($candidates as $candidate) { + if ((int) $result['processed'] >= $maxOrders) { + $shouldStop = true; + break; + } + + $sourceOrderId = (string) ($candidate['source_order_id'] ?? ''); + $sourceUpdatedAt = (string) ($candidate['source_updated_at'] ?? ''); + $rawOrder = is_array($candidate['payload'] ?? null) ? $candidate['payload'] : []; + + $details = $this->apiClient->fetchOrderById($baseUrl, $apiKey, $timeout, $sourceOrderId); + if (($details['ok'] ?? false) === true && is_array($details['order'] ?? null)) { + $detailsOrder = (array) $details['order']; + foreach (['products', 'summary', 'paid', 'transport_cost', 'transport', 'transport_description', + 'client_name', 'client_surname', 'client_email', 'client_phone', 'client_city', + 'client_street', 'client_postal_code'] as $protectedKey) { + if (array_key_exists($protectedKey, $rawOrder)) { + unset($detailsOrder[$protectedKey]); + } + } + $rawOrder = array_replace($rawOrder, $detailsOrder); + } + + $this->importOneOrder( + $integrationId, $sourceOrderId, $sourceUpdatedAt, $rawOrder, + $baseUrl, $apiKey, $timeout, $statusMap, $result, $productImageCache + ); + + if ($latestUpdatedAt === null || $sourceUpdatedAt > $latestUpdatedAt) { + $latestUpdatedAt = $sourceUpdatedAt; + $latestOrderId = $sourceOrderId; + } elseif ($latestUpdatedAt === $sourceUpdatedAt && ($latestOrderId === null || strcmp($sourceOrderId, $latestOrderId) > 0)) { + $latestOrderId = $sourceOrderId; + } + } + } + + /** + * @param array $rawOrder + * @param array $statusMap + * @param array $result + * @param array $productImageCache + */ + private function importOneOrder( + int $integrationId, + string $sourceOrderId, + string $sourceUpdatedAt, + array $rawOrder, + string $baseUrl, + string $apiKey, + int $timeout, + array $statusMap, + array &$result, + array &$productImageCache + ): void { + try { + $productImages = $this->resolveProductImagesForOrder( + $baseUrl, $apiKey, $timeout, $rawOrder, $productImageCache + ); + $aggregate = $this->mapOrderAggregate( + $integrationId, $rawOrder, $statusMap, $sourceOrderId, $sourceUpdatedAt, $productImages + ); + $save = $this->orderImportRepository->upsertOrderAggregate( + $aggregate['order'], + $aggregate['addresses'], + $aggregate['items'], + $aggregate['payments'], + $aggregate['shipments'], + $aggregate['notes'], + $aggregate['status_history'] + ); + $result['processed'] = (int) $result['processed'] + 1; + if (!empty($save['created'])) { + $result['imported_created'] = (int) $result['imported_created'] + 1; + } else { + $result['imported_updated'] = (int) $result['imported_updated'] + 1; + } + $this->orders->recordActivity( + (int) ($save['order_id'] ?? 0), + 'import', + 'Import zamowienia z shopPRO', + ['integration_id' => $integrationId, 'source_order_id' => $sourceOrderId], + 'import', + 'shopPRO' + ); + } catch (Throwable $exception) { + $result['failed'] = (int) $result['failed'] + 1; + $errors = is_array($result['errors']) ? $result['errors'] : []; + if (count($errors) < 20) { + $errors[] = [ + 'integration_id' => $integrationId, + 'source_order_id' => $sourceOrderId, + 'error' => $exception->getMessage(), + ]; + } + $result['errors'] = $errors; + } + } + /** * @param mixed $rawIds * @return array @@ -486,6 +520,38 @@ final class ShopproOrdersSyncService { $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 $payload + * @return array{address:array,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', @@ -499,10 +565,7 @@ final class ShopproOrdersSyncService $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', - ])); - if ($customerName === null) { - $customerName = $this->composeName($customerFirstName, $customerLastName, 'Klient'); - } + ])) ?? $this->composeName($customerFirstName, $customerLastName, 'Klient'); $customerEmail = StringHelper::nullableString((string) $this->readPath($payload, [ 'buyer_email', 'buyer.email', 'customer.email', 'client.email', 'billing_address.email', @@ -514,7 +577,7 @@ final class ShopproOrdersSyncService 'phone_number', 'client_phone_number', ])); - $result[] = [ + $address = [ 'address_type' => 'customer', 'name' => $customerName ?? 'Klient', 'phone' => $customerPhone, @@ -550,11 +613,15 @@ final class ShopproOrdersSyncService ], ]; - $invoiceAddress = $this->buildInvoiceAddress($payload, $customerName, $customerEmail, $customerPhone); - if ($invoiceAddress !== null) { - $result[] = $invoiceAddress; - } + return ['address' => $address, 'name' => $customerName, 'email' => $customerEmail, 'phone' => $customerPhone]; + } + /** + * @param array $payload + * @return array|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', @@ -568,14 +635,11 @@ final class ShopproOrdersSyncService $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', - ])); - if ($deliveryName === null) { - $deliveryName = $this->composeName($deliveryFirstName, $deliveryLastName, null); - } + ])) ?? $this->composeName($deliveryFirstName, $deliveryLastName, null); $pickupData = $this->parsePickupPoint((string) $this->readPath($payload, ['inpost_paczkomat', 'orlen_point', 'pickup_point'])); - $deliveryAddress = [ - 'name' => $deliveryName, + $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', @@ -620,29 +684,24 @@ final class ShopproOrdersSyncService ], ]; - if (($deliveryAddress['name'] ?? null) === null) { - $deliveryAddress['name'] = StringHelper::nullableString($this->buildDeliveryMethodLabel($payload)); + if (!$this->hasAddressData($fields)) { + return null; } - $hasDeliveryData = $this->hasAddressData($deliveryAddress); - if ($hasDeliveryData) { - $result[] = [ - 'address_type' => 'delivery', - 'name' => $deliveryAddress['name'] ?? $customerName ?? 'Dostawa', - 'phone' => $deliveryAddress['phone'] ?? null, - 'email' => $deliveryAddress['email'] ?? $customerEmail, - 'street_name' => $deliveryAddress['street_name'] ?? null, - 'street_number' => $deliveryAddress['street_number'] ?? null, - 'city' => $deliveryAddress['city'] ?? null, - 'zip_code' => $deliveryAddress['zip_code'] ?? null, - 'country' => $deliveryAddress['country'] ?? null, - 'parcel_external_id' => $deliveryAddress['parcel_external_id'] ?? null, - 'parcel_name' => $deliveryAddress['parcel_name'] ?? null, - 'payload_json' => is_array($deliveryAddress['payload_json'] ?? null) ? $deliveryAddress['payload_json'] : null, - ]; - } - - return $result; + 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, + ]; } /**