$options * @return array */ public function sync(array $options = []): array { $maxPages = max(1, min(20, (int) ($options['max_pages'] ?? 3))); $pageLimit = max(1, min(100, (int) ($options['page_limit'] ?? 50))); $maxOrders = max(1, min(1000, (int) ($options['max_orders'] ?? 200))); $ignoreOrdersFetchEnabled = !empty($options['ignore_orders_fetch_enabled']); $allowedIntegrationIds = $this->normalizeIntegrationIds($options['allowed_integration_ids'] ?? []); $result = [ 'processed' => 0, 'imported_created' => 0, 'imported_updated' => 0, 'failed' => 0, 'skipped' => 0, 'checked_integrations' => 0, 'errors' => [], ]; foreach ($this->integrations->listIntegrations() as $integration) { $integrationId = (int) ($integration['id'] ?? 0); if ($integrationId <= 0) { continue; } if ($allowedIntegrationIds !== [] && !isset($allowedIntegrationIds[$integrationId])) { continue; } if (empty($integration['is_active']) || empty($integration['has_api_key'])) { continue; } if (!$ignoreOrdersFetchEnabled && empty($integration['orders_fetch_enabled'])) { continue; } $result['checked_integrations'] = (int) $result['checked_integrations'] + 1; $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->mapper->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->imageResolver->resolveProductImagesForOrder( $baseUrl, $apiKey, $timeout, $rawOrder, $productImageCache ); $aggregate = $this->mapper->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; } $wasCreated = !empty($save['created']); $wasPaymentTransition = !empty($save['payment_transition']); $savedOrderId = (int) ($save['order_id'] ?? 0); if ($wasPaymentTransition) { $summary = 'Platnosc potwierdzona z shopPRO — zmiana statusu na w realizacji'; } elseif ($wasCreated) { $summary = 'Import zamowienia z shopPRO'; } else { $summary = 'Zaktualizowano zamowienie z shopPRO (re-import)'; } $details = [ 'integration_id' => $integrationId, 'source_order_id' => $sourceOrderId, 'source_updated_at' => $sourceUpdatedAt, 'created' => $wasCreated, 'payment_transition' => $wasPaymentTransition, 'trigger' => 'orders_sync', 'trigger_label' => 'Synchronizacja zamowien', ]; if (!$this->orders->shouldSkipDuplicateImportActivity($savedOrderId, $details)) { $this->orders->recordActivity( $savedOrderId, 'import', $summary, $details, 'import', 'shopPRO' ); } if ($savedOrderId > 0 && !$wasPaymentTransition && $this->automationService !== null) { $this->automationService->trigger('order.imported', $savedOrderId, [ 'source' => 'shoppro', 'created' => $wasCreated, 'integration_id' => $integrationId, ]); } } 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 */ private function normalizeIntegrationIds(mixed $rawIds): array { if (!is_array($rawIds)) { return []; } $result = []; foreach ($rawIds as $rawId) { $id = (int) $rawId; if ($id <= 0) { continue; } $result[$id] = true; } return $result; } /** * @return array shoppro_status_code => orderpro_status_code */ private function buildStatusMap(int $integrationId): array { if ($this->pullStatusMappings !== null) { return $this->buildStatusMapFromPullTable($integrationId); } return $this->buildStatusMapFromPushTable($integrationId); } /** * @return array */ private function buildStatusMapFromPullTable(int $integrationId): array { $rows = $this->pullStatusMappings->listByIntegration($integrationId); $map = []; foreach ($rows as $row) { $shopCode = strtolower(trim((string) ($row['shoppro_status_code'] ?? ''))); $orderCode = strtolower(trim((string) ($row['orderpro_status_code'] ?? ''))); if ($shopCode === '' || $orderCode === '') { continue; } $map[$shopCode] = $orderCode; } return $map; } /** * @return array */ private function buildStatusMapFromPushTable(int $integrationId): array { $rows = $this->statusMappings->listByIntegration($integrationId); $map = []; foreach ($rows as $row) { $shopCode = strtolower(trim((string) ($row['shoppro_status_code'] ?? ''))); $orderCode = strtolower(trim((string) ($row['orderpro_status_code'] ?? ''))); if ($shopCode === '' || $orderCode === '') { continue; } if (!isset($map[$shopCode])) { $map[$shopCode] = $orderCode; } } return $map; } private function resolveStartDate(string $settingsDate, ?string $cursorUpdatedAt): ?string { $settings = trim($settingsDate); $cursor = StringHelper::nullableString((string) $cursorUpdatedAt); if ($settings === '' && $cursor === null) { return null; } if ($settings === '') { return $cursor; } if ($cursor === null) { return $settings; } return $cursor > $settings ? $cursor : $settings; } }