Files
erp_naurua/modules/erp/import-integration/shopify-delta-run.php
T

175 lines
6.4 KiB
PHP

<?php
declare(strict_types=1);
require_once __DIR__ . '/../../../modules/shared/db.php';
require_once __DIR__ . '/shopify-api.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 (($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);
}
}