187 lines
7.2 KiB
PHP
187 lines
7.2 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
require_once __DIR__ . '/../../../modules/shared/db.php';
|
|
require_once __DIR__ . '/../../../modules/shared/webhook_throttle.php';
|
|
require_once __DIR__ . '/shopify-api.php';
|
|
require_once __DIR__ . '/service.php';
|
|
require_once __DIR__ . '/../bestellungen/shopify-projection.php';
|
|
|
|
function shopify_delta_argument(string $name, array $argv, ?string $default = null): ?string
|
|
{
|
|
$prefix = '--' . $name . '=';
|
|
foreach ($argv as $argument) {
|
|
if (str_starts_with($argument, $prefix)) {
|
|
return substr($argument, strlen($prefix));
|
|
}
|
|
}
|
|
return $default;
|
|
}
|
|
|
|
function shopify_delta_start_run(PDO $pdo, string $cutoff, bool $dryRun): int
|
|
{
|
|
$stmt = $pdo->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);
|
|
}
|
|
}
|