Add Shopify delta order import
This commit is contained in:
@@ -0,0 +1,306 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../kontakte/service.php';
|
||||
require_once __DIR__ . '/../artikel-mapping/service.php';
|
||||
require_once __DIR__ . '/../lager/service.php';
|
||||
|
||||
function shopify_projection_payment_status(string $financialStatus): string
|
||||
{
|
||||
return match (strtoupper(trim($financialStatus))) {
|
||||
'PAID', 'PARTIALLY_PAID' => 'paid',
|
||||
'AUTHORIZED' => 'authorized',
|
||||
'PARTIALLY_REFUNDED' => 'partially_refunded',
|
||||
'REFUNDED' => 'refunded',
|
||||
'VOIDED' => 'voided',
|
||||
default => 'unknown',
|
||||
};
|
||||
}
|
||||
|
||||
function shopify_projection_order_status(array $order): string
|
||||
{
|
||||
if (($order['cancelledAt'] ?? null) !== null) {
|
||||
return 'cancelled';
|
||||
}
|
||||
|
||||
return strtoupper((string) ($order['displayFulfillmentStatus'] ?? '')) === 'FULFILLED'
|
||||
? 'fulfilled'
|
||||
: 'imported';
|
||||
}
|
||||
|
||||
function shopify_projection_money(array $order, string $field): ?float
|
||||
{
|
||||
$amount = $order[$field]['shopMoney']['amount'] ?? null;
|
||||
return $amount === null ? null : parse_number($amount);
|
||||
}
|
||||
|
||||
function shopify_projection_customer(PDO $pdo, array $customer, bool $dryRun): ?int
|
||||
{
|
||||
$customerGid = trim((string) ($customer['id'] ?? ''));
|
||||
if ($customerGid === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$identity = $pdo->prepare(
|
||||
'SELECT party_id FROM party_external_identity
|
||||
WHERE source_system = \'shopify\' AND external_id = :external_id LIMIT 1'
|
||||
);
|
||||
$identity->execute([':external_id' => $customerGid]);
|
||||
$partyId = $identity->fetchColumn();
|
||||
if ($partyId !== false) {
|
||||
return (int) $partyId;
|
||||
}
|
||||
|
||||
$email = trim((string) ($customer['email'] ?? ''));
|
||||
if ($dryRun) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$name = trim((string) ($customer['firstName'] ?? '') . ' ' . (string) ($customer['lastName'] ?? ''));
|
||||
if ($name === '') {
|
||||
$name = 'Shopify-Kunde';
|
||||
}
|
||||
|
||||
$partyStmt = $pdo->prepare(
|
||||
"INSERT INTO party (type, name, email, status, created_at, updated_at)
|
||||
VALUES ('customer', :name, :email, 'active', NOW(), NOW())
|
||||
RETURNING id"
|
||||
);
|
||||
$partyStmt->execute([':name' => $name, ':email' => $email !== '' ? $email : null]);
|
||||
$partyId = $partyStmt->fetchColumn();
|
||||
if ($partyId === false) {
|
||||
throw new RuntimeException('Shopify-Kunde konnte nicht angelegt werden');
|
||||
}
|
||||
|
||||
$identityStmt = $pdo->prepare(
|
||||
"INSERT INTO party_external_identity (party_id, source_system, external_id, observed_at, created_at, updated_at)
|
||||
VALUES (:party_id, 'shopify', :external_id, NOW(), NOW(), NOW())"
|
||||
);
|
||||
$identityStmt->execute([':party_id' => (int) $partyId, ':external_id' => $customerGid]);
|
||||
|
||||
return (int) $partyId;
|
||||
}
|
||||
|
||||
function shopify_projection_address(PDO $pdo, ?int $partyId, array $address, string $type, bool $dryRun): void
|
||||
{
|
||||
if ($partyId === null || $dryRun || $address === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare(
|
||||
'INSERT INTO address (
|
||||
party_id, type, first_name, last_name, company_name, street, house_number,
|
||||
zip, city, state_code, country_name, country_iso2, created_at, updated_at
|
||||
) VALUES (
|
||||
:party_id, :type, :first_name, :last_name, :company_name, :street, :house_number,
|
||||
:zip, :city, :state_code, :country_name, :country_iso2, NOW(), NOW()
|
||||
)'
|
||||
);
|
||||
$street = trim((string) ($address['address1'] ?? ''));
|
||||
$address2 = trim((string) ($address['address2'] ?? ''));
|
||||
if ($address2 !== '') {
|
||||
$street .= ($street !== '' ? ', ' : '') . $address2;
|
||||
}
|
||||
$stmt->execute([
|
||||
':party_id' => $partyId,
|
||||
':type' => $type,
|
||||
':first_name' => trim((string) ($address['firstName'] ?? '')),
|
||||
':last_name' => trim((string) ($address['lastName'] ?? '')),
|
||||
':company_name' => trim((string) ($address['company'] ?? '')) ?: null,
|
||||
':street' => $street,
|
||||
':house_number' => null,
|
||||
':zip' => trim((string) ($address['zip'] ?? '')),
|
||||
':city' => trim((string) ($address['city'] ?? '')),
|
||||
':state_code' => trim((string) ($address['provinceCode'] ?? '')) ?: null,
|
||||
':country_name' => trim((string) ($address['country'] ?? '')) ?: null,
|
||||
':country_iso2' => trim((string) ($address['countryCodeV2'] ?? '')) ?: null,
|
||||
]);
|
||||
}
|
||||
|
||||
function shopify_projection_line(PDO $pdo, array $line, bool $dryRun): array
|
||||
{
|
||||
$sku = trim((string) ($line['sku'] ?? ($line['variant']['sku'] ?? '')));
|
||||
$variantGid = trim((string) ($line['variant']['id'] ?? ''));
|
||||
$productGid = trim((string) ($line['variant']['product']['id'] ?? ''));
|
||||
$sellableItemId = find_shopify_sellable_item_id($pdo, $sku, $variantGid);
|
||||
if ($sellableItemId === null) {
|
||||
throw new RuntimeException("Kein Shopify-Artikel-Mapping fuer SKU '{$sku}'");
|
||||
}
|
||||
|
||||
if (!$dryRun) {
|
||||
bind_shopify_item_identity($pdo, $sellableItemId, $sku, $productGid, $variantGid);
|
||||
}
|
||||
|
||||
$qty = parse_number($line['quantity'] ?? null);
|
||||
if ($qty === null || $qty <= 0) {
|
||||
throw new RuntimeException("Ungueltige Shopify-Menge fuer SKU '{$sku}'");
|
||||
}
|
||||
|
||||
return [
|
||||
'line' => $line,
|
||||
'sku' => $sku,
|
||||
'variant_gid' => $variantGid,
|
||||
'product_gid' => $productGid,
|
||||
'sellable_item_id' => $sellableItemId,
|
||||
'qty' => $qty,
|
||||
'unit_price' => parse_number($line['originalUnitPriceSet']['shopMoney']['amount'] ?? null),
|
||||
];
|
||||
}
|
||||
|
||||
function shopify_projection_stock_check(PDO $pdo, array $resolvedLines): void
|
||||
{
|
||||
foreach ($resolvedLines as $resolvedLine) {
|
||||
$components = get_item_components($pdo, (int) $resolvedLine['sellable_item_id']);
|
||||
if ($components === []) {
|
||||
throw new RuntimeException("Keine Komponenten fuer SKU '{$resolvedLine['sku']}'");
|
||||
}
|
||||
foreach ($components as $component) {
|
||||
$required = (float) $resolvedLine['qty'] * (float) $component['qty_per_item'];
|
||||
$stmt = $pdo->prepare(
|
||||
"SELECT COALESCE(SUM(v.qty_net), 0)
|
||||
FROM v_stock_lot_balance v
|
||||
JOIN stock_lot sl ON sl.id = v.stock_lot_id
|
||||
WHERE sl.product_id = :product_id AND sl.status = 'current'"
|
||||
);
|
||||
$stmt->execute([':product_id' => (int) $component['product_id']]);
|
||||
$available = (float) ($stmt->fetchColumn() ?: 0);
|
||||
if ($available + 0.0000001 < $required) {
|
||||
throw new RuntimeException("Nicht genuegend aktueller Bestand fuer SKU '{$resolvedLine['sku']}'");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function project_shopify_order(PDO $pdo, array $order, array $env, bool $dryRun = true): array
|
||||
{
|
||||
$orderGid = trim((string) ($order['id'] ?? ''));
|
||||
if ($orderGid === '') {
|
||||
throw new RuntimeException('Shopify Order-GID fehlt');
|
||||
}
|
||||
|
||||
$existingStmt = $pdo->prepare('SELECT id FROM sales_order WHERE shopify_order_gid = :shopify_order_gid LIMIT 1');
|
||||
$existingStmt->execute([':shopify_order_gid' => $orderGid]);
|
||||
$existingId = $existingStmt->fetchColumn();
|
||||
if ($existingId !== false) {
|
||||
return ['status' => 'already_imported', 'shopify_order_gid' => $orderGid, 'sales_order_id' => (int) $existingId];
|
||||
}
|
||||
|
||||
$resolvedLines = [];
|
||||
foreach (($order['lineItems']['nodes'] ?? []) as $line) {
|
||||
$resolvedLines[] = shopify_projection_line($pdo, $line, $dryRun);
|
||||
}
|
||||
if ($resolvedLines === []) {
|
||||
throw new RuntimeException('Shopify-Bestellung enthaelt keine Positionen');
|
||||
}
|
||||
shopify_projection_stock_check($pdo, $resolvedLines);
|
||||
|
||||
if ($dryRun) {
|
||||
$customer = is_array($order['customer'] ?? null) ? $order['customer'] : [];
|
||||
shopify_projection_customer($pdo, $customer, true);
|
||||
return [
|
||||
'status' => 'dry_run_ready',
|
||||
'shopify_order_gid' => $orderGid,
|
||||
'sales_order_id' => null,
|
||||
'line_count' => count($resolvedLines),
|
||||
'allocation_count' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
$pdo->beginTransaction();
|
||||
try {
|
||||
$customer = is_array($order['customer'] ?? null) ? $order['customer'] : [];
|
||||
$partyId = shopify_projection_customer($pdo, $customer, false);
|
||||
$orderStatus = shopify_projection_order_status($order);
|
||||
$paymentStatus = shopify_projection_payment_status((string) ($order['displayFinancialStatus'] ?? ''));
|
||||
$orderDate = new DateTimeImmutable((string) $order['createdAt']);
|
||||
$externalRef = (string) ($order['name'] ?? $orderGid);
|
||||
$orderStmt = $pdo->prepare(
|
||||
'INSERT INTO sales_order (
|
||||
external_ref, party_id, order_date, order_status, payment_status, amount_net,
|
||||
amount_shipping, amount_tax, total_amount, currency, webhook_payload, imported_at,
|
||||
created_at, updated_at, order_source, shopify_order_gid, shopify_order_name,
|
||||
shopify_created_at, shopify_updated_at, shopify_financial_status, shopify_fulfillment_status
|
||||
) VALUES (
|
||||
:external_ref, :party_id, :order_date, :order_status, :payment_status, :amount_net,
|
||||
:amount_shipping, :amount_tax, :total_amount, :currency, :webhook_payload::jsonb, NOW(),
|
||||
NOW(), NOW(), \'shopify\', :shopify_order_gid, :shopify_order_name,
|
||||
:shopify_created_at, :shopify_updated_at, :shopify_financial_status, :shopify_fulfillment_status
|
||||
) RETURNING id'
|
||||
);
|
||||
$orderStmt->execute([
|
||||
':external_ref' => $externalRef,
|
||||
':party_id' => $partyId,
|
||||
':order_date' => $orderDate->format('Y-m-d H:i:s'),
|
||||
':order_status' => $orderStatus,
|
||||
':payment_status' => $paymentStatus,
|
||||
':amount_net' => shopify_projection_money($order, 'currentSubtotalPriceSet'),
|
||||
':amount_shipping' => shopify_projection_money($order, 'totalShippingPriceSet'),
|
||||
':amount_tax' => shopify_projection_money($order, 'totalTaxSet'),
|
||||
':total_amount' => shopify_projection_money($order, 'totalPriceSet'),
|
||||
':currency' => (string) (($order['totalPriceSet']['shopMoney']['currencyCode'] ?? 'CHF')),
|
||||
':webhook_payload' => json_encode($order, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
||||
':shopify_order_gid' => $orderGid,
|
||||
':shopify_order_name' => (string) ($order['name'] ?? ''),
|
||||
':shopify_created_at' => $orderDate->format('Y-m-d H:i:s'),
|
||||
':shopify_updated_at' => (new DateTimeImmutable((string) $order['updatedAt']))->format('Y-m-d H:i:s'),
|
||||
':shopify_financial_status' => (string) ($order['displayFinancialStatus'] ?? ''),
|
||||
':shopify_fulfillment_status' => (string) ($order['displayFulfillmentStatus'] ?? ''),
|
||||
]);
|
||||
$orderId = (int) $orderStmt->fetchColumn();
|
||||
|
||||
$locations = get_default_location_ids($pdo);
|
||||
$lineNo = 0;
|
||||
$allocationCount = 0;
|
||||
foreach ($resolvedLines as $resolvedLine) {
|
||||
$lineNo++;
|
||||
$line = $resolvedLine['line'];
|
||||
$lineStmt = $pdo->prepare(
|
||||
'INSERT INTO sales_order_line (
|
||||
sales_order_id, line_no, sellable_item_id, raw_external_article_number,
|
||||
raw_external_title, qty, unit_price, line_total, shopify_line_item_gid,
|
||||
shopify_variant_gid, shopify_product_gid, sku, created_at, updated_at
|
||||
) VALUES (
|
||||
:sales_order_id, :line_no, :sellable_item_id, :article_number,
|
||||
:title, :qty, :unit_price, :line_total, :line_gid,
|
||||
:variant_gid, :product_gid, :sku, NOW(), NOW()
|
||||
) RETURNING id'
|
||||
);
|
||||
$qty = (float) $resolvedLine['qty'];
|
||||
$unitPrice = $resolvedLine['unit_price'];
|
||||
$lineStmt->execute([
|
||||
':sales_order_id' => $orderId,
|
||||
':line_no' => $lineNo,
|
||||
':sellable_item_id' => $resolvedLine['sellable_item_id'],
|
||||
':article_number' => $resolvedLine['sku'],
|
||||
':title' => (string) ($line['name'] ?? $resolvedLine['sku']),
|
||||
':qty' => $qty,
|
||||
':unit_price' => $unitPrice,
|
||||
':line_total' => $unitPrice === null ? null : round($qty * $unitPrice, 2),
|
||||
':line_gid' => $resolvedLine['line']['id'],
|
||||
':variant_gid' => $resolvedLine['variant_gid'],
|
||||
':product_gid' => $resolvedLine['product_gid'],
|
||||
':sku' => $resolvedLine['sku'],
|
||||
]);
|
||||
$lineId = (int) $lineStmt->fetchColumn();
|
||||
$allocation = allocate_line_inventory($pdo, $orderId, $lineId, $lineNo, $qty, (int) $resolvedLine['sellable_item_id'], $locations, 'shopify-order');
|
||||
$allocationCount += (int) ($allocation['allocationCount'] ?? 0);
|
||||
}
|
||||
|
||||
shopify_projection_address($pdo, $partyId, (array) ($order['billingAddress'] ?? []), 'billing', false);
|
||||
shopify_projection_address($pdo, $partyId, (array) ($order['shippingAddress'] ?? []), 'shipping', false);
|
||||
$pdo->commit();
|
||||
} catch (Throwable $exception) {
|
||||
if ($pdo->inTransaction()) {
|
||||
$pdo->rollBack();
|
||||
}
|
||||
throw $exception;
|
||||
}
|
||||
|
||||
return [
|
||||
'status' => 'imported',
|
||||
'shopify_order_gid' => $orderGid,
|
||||
'sales_order_id' => $orderId,
|
||||
'line_count' => count($resolvedLines),
|
||||
'allocation_count' => $allocationCount,
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
<?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 {
|
||||
$order = shopify_delta_load_order($env, (string) $summaryOrder['id']);
|
||||
$result = project_shopify_order($pdo, $order, $env, $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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user