prepare( "INSERT INTO process_runs (process_name, status, scope_json, requested_by) VALUES ('erp.import-integration.shopify_delta_order_import', 'running', :scope_json::jsonb, 'cli') RETURNING id" ); $stmt->execute([':scope_json' => json_encode([ 'cutoff_timestamp' => $cutoff, 'dry_run' => $dryRun, ], JSON_THROW_ON_ERROR)]); return (int) $stmt->fetchColumn(); } function shopify_delta_finish_run(PDO $pdo, int $runId, string $status, array $result, ?Throwable $error = null): void { $stmt = $pdo->prepare( 'UPDATE process_runs SET status = :status, result_json = :result_json::jsonb, finished_at = NOW(), error_code = :error_code, error_message = :error_message WHERE id = :id' ); $stmt->execute([ ':status' => $status, ':result_json' => json_encode($result, JSON_THROW_ON_ERROR), ':error_code' => $error === null ? null : 'shopify_delta_failed', ':error_message' => $error?->getMessage(), ':id' => $runId, ]); } function shopify_delta_load_orders(array $env, string $searchQuery): array { $query = <<<'GQL' query ($query: String!, $after: String) { orders(first: 100, after: $after, query: $query, sortKey: CREATED_AT) { nodes { id name createdAt cancelledAt displayFinancialStatus displayFulfillmentStatus } pageInfo { hasNextPage endCursor } } } GQL; $orders = []; $after = null; do { $data = shopify_graphql_query($env, $query, ['query' => $searchQuery, 'after' => $after]); $connection = $data['orders'] ?? []; foreach (($connection['nodes'] ?? []) as $order) { $orders[] = $order; } $after = ($connection['pageInfo']['hasNextPage'] ?? false) ? ($connection['pageInfo']['endCursor'] ?? null) : null; } while ($after !== null); return $orders; } function shopify_delta_load_order(array $env, string $orderGid): array { $query = <<<'GQL' query ($id: ID!) { order(id: $id) { id name createdAt updatedAt cancelledAt displayFinancialStatus displayFulfillmentStatus currentSubtotalPriceSet { shopMoney { amount currencyCode } } totalShippingPriceSet { shopMoney { amount currencyCode } } totalTaxSet { shopMoney { amount currencyCode } } totalPriceSet { shopMoney { amount currencyCode } } customer { id firstName lastName email phone } billingAddress { firstName lastName company address1 address2 zip city provinceCode country countryCodeV2 phone } shippingAddress { firstName lastName company address1 address2 zip city provinceCode country countryCodeV2 phone } lineItems(first: 50) { nodes { id sku name quantity originalUnitPriceSet { shopMoney { amount currencyCode } } variant { id sku product { id } } } } } } GQL; $data = shopify_graphql_query($env, $query, ['id' => $orderGid]); $order = $data['order'] ?? null; if (!is_array($order)) { throw new RuntimeException("Shopify-Bestellung nicht gefunden: {$orderGid}"); } return $order; } function run_shopify_delta(array $env, PDO $pdo, string $cutoff, bool $dryRun): array { $cutoffDate = new DateTimeImmutable($cutoff); $searchQuery = 'created_at:>' . $cutoffDate->format('Y-m-d'); $counters = [ 'status' => 'done', 'cutoff_timestamp' => $cutoffDate->format(DATE_ATOM), 'dry_run' => $dryRun, 'checked_orders' => 0, 'skipped_cancelled_or_refunded' => 0, 'already_imported' => 0, 'handed_off' => 0, 'clarification_order_gids' => [], ]; foreach (shopify_delta_load_orders($env, $searchQuery) as $summaryOrder) { $createdAt = new DateTimeImmutable((string) ($summaryOrder['createdAt'] ?? '')); if ($createdAt <= $cutoffDate) { continue; } $counters['checked_orders']++; if (($summaryOrder['cancelledAt'] ?? null) !== null || strtoupper((string) ($summaryOrder['displayFinancialStatus'] ?? '')) === 'REFUNDED') { $counters['skipped_cancelled_or_refunded']++; continue; } try { $orderGid = (string) $summaryOrder['id']; $result = project_shopify_order( $pdo, $orderGid, static fn (string $gid): array => shopify_delta_load_order($env, $gid), $dryRun ); if (!$dryRun && ($result['status'] ?? '') === 'imported') { $labelPayload = build_legacy_label_order_payload($pdo, (int) $result['sales_order_id']); $labelResult = trigger_shipping_label_flow($labelPayload, $env); $excelResult = trigger_excel_webhook((string) ($summaryOrder['name'] ?? $orderGid), $env); $result['label_trigger'] = $labelResult; $result['excel_trigger'] = $excelResult; if (!$labelResult['ok'] || !$excelResult['ok']) { throw new RuntimeException('Mindestens ein n8n-Webhook ist fehlgeschlagen'); } } if (($result['status'] ?? '') === 'already_imported') { $counters['already_imported']++; } else { $counters['handed_off']++; } } catch (Throwable $error) { $counters['status'] = 'partial_success'; $counters['clarification_order_gids'][] = (string) $summaryOrder['id']; } } return $counters; } if (PHP_SAPI === 'cli' && realpath($_SERVER['SCRIPT_FILENAME'] ?? '') === __FILE__) { $argv = $_SERVER['argv'] ?? []; $cutoff = shopify_delta_argument('cutoff', $argv); $execute = in_array('--execute', $argv, true); $dryRun = !$execute; if ($cutoff === null || $cutoff === '') { fwrite(STDERR, "Usage: php shopify-delta-run.php --cutoff=ISO-8601 [--execute]\n"); exit(2); } $env = expand_env_values(parse_env_file(__DIR__ . '/../../../.env')); $pdo = connect_database($env); $runId = shopify_delta_start_run($pdo, $cutoff, $dryRun); try { $result = run_shopify_delta($env, $pdo, $cutoff, $dryRun); $result['technical_run_id'] = $runId; shopify_delta_finish_run($pdo, $runId, $result['status'], $result); echo json_encode($result, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL; exit($result['status'] === 'done' ? 0 : 1); } catch (Throwable $error) { shopify_delta_finish_run($pdo, $runId, 'failed', ['status' => 'failed', 'technical_run_id' => $runId], $error); fwrite(STDERR, $error->getMessage() . PHP_EOL); exit(1); } }