<?php
/**
 * _core.php
 * Shared core for all cron files.
 * Uses config.php only and the legacy $db object loaded from config/global.php.
 */

if (!headers_sent()) {
    header('Content-Type: application/json; charset=utf-8');
}

if (!defined('CLEAN_CRON_ROOT')) {
    define('CLEAN_CRON_ROOT', dirname(__DIR__) . '/');
}

require_once CLEAN_CRON_ROOT . 'config.php';

date_default_timezone_set('Asia/Jerusalem');

function core_json(array $data, int $status = 200): void
{
    http_response_code($status);
    echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
    exit;
}

function core_fail(string $message, array $extra = [], int $status = 400): void
{
    core_json(array_merge(['ok' => false, 'message' => $message], $extra), $status);
}

function core_require_constants(array $names): void
{
    $missing = [];
    foreach ($names as $name) {
        if (!defined($name) || constant($name) === '') {
            $missing[] = $name;
        }
    }
    if ($missing) {
        core_fail('Missing required constants in config.php.', ['missing_constants' => $missing], 500);
    }
}

function core_secret_check(): void
{
    core_require_constants(['NS_INVENTORY_SYNC_SECRET']);
    $secret = $_GET['secret'] ?? $_GET['key'] ?? '';
    if ((string)$secret !== (string)NS_INVENTORY_SYNC_SECRET) {
        core_fail('Invalid secret key.', [], 403);
    }
}

function core_db()
{
    if (!isset($GLOBALS['db']) || !is_object($GLOBALS['db']) || !method_exists($GLOBALS['db'], 'query')) {
        core_fail('Database core $db was not loaded by config.php/global.php.', [], 500);
    }
    return $GLOBALS['db'];
}

function core_db_query(string $sql)
{
    $db = core_db();
    $res = $db->query($sql);
    if ($res === false) {
        throw new RuntimeException('DB query failed: ' . $sql);
    }
    return $res;
}

function core_db_fetch($res): ?array
{
    $db = core_db();
    $row = $db->fetch_array($res);
    return is_array($row) ? $row : null;
}

function core_db_fetch_row(string $sql): ?array
{
    if (method_exists(core_db(), 'fetchRow')) {
        $row = core_db()->fetchRow($sql);
        return is_array($row) ? $row : null;
    }
    $res = core_db_query($sql);
    return core_db_fetch($res);
}

function core_sql($value): string
{
    if ($value === null) {
        return 'NULL';
    }
    return "'" . addslashes((string)$value) . "'";
}

function core_sql_num($value, int $decimals = 4): string
{
    if ($value === null || $value === '') {
        return '0';
    }
    return number_format((float)$value, $decimals, '.', '');
}

function core_json_encode($value): string
{
    return json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
}

function core_now(): string
{
    return date('Y-m-d H:i:s');
}

function core_date(?string $value): ?string
{
    if (!$value) return null;
    $ts = strtotime($value);
    return $ts ? date('Y-m-d', $ts) : null;
}

function core_datetime(?string $value): ?string
{
    if (!$value) return null;
    $ts = strtotime($value);
    return $ts ? date('Y-m-d H:i:s', $ts) : null;
}

function core_money($value): float
{
    if ($value === null || $value === '') return 0.0;
    return round((float)$value, 4);
}

function core_gid_tail(?string $gid): string
{
    if (!$gid) return '';
    $parts = explode('/', $gid);
    return (string)end($parts);
}

function core_array_get(array $arr, array $path, $default = null)
{
    $cur = $arr;
    foreach ($path as $p) {
        if (!is_array($cur) || !array_key_exists($p, $cur)) return $default;
        $cur = $cur[$p];
    }
    return $cur;
}

function core_bool_param(string $name, bool $default = false): bool
{
    if (!isset($_GET[$name])) return $default;
    return in_array(strtolower((string)$_GET[$name]), ['1', 'true', 'yes', 'y'], true);
}

function core_int_param(string $name, int $default, int $min = 1, int $max = 100000): int
{
    $v = isset($_GET[$name]) ? (int)$_GET[$name] : $default;
    return max($min, min($max, $v));
}

function core_table_exists(string $table): bool
{
    $row = core_db_fetch_row("SHOW TABLES LIKE " . core_sql($table));
    return is_array($row);
}

function core_table_columns(string $table): array
{
    static $cache = [];
    if (isset($cache[$table])) return $cache[$table];
    $safe = str_replace('`', '', $table);
    $res = core_db_query("SHOW COLUMNS FROM `{$safe}`");
    $cols = [];
    while ($row = core_db_fetch($res)) {
        $cols[] = $row['Field'];
    }
    $cache[$table] = $cols;
    return $cols;
}

function core_has_column(string $table, string $column): bool
{
    return in_array($column, core_table_columns($table), true);
}

function core_add_column_if_missing(string $table, string $column, string $definition, array &$added): void
{
    if (!core_has_column($table, $column)) {
        core_db_query("ALTER TABLE `{$table}` ADD COLUMN `{$column}` {$definition}");
        $added[] = $table . '.' . $column;
    }
}

function core_ensure_runtime_columns(): array
{
    $added = [];
    if (core_table_exists('orders')) {
        core_add_column_if_missing('orders', 'is_refunded', 'TINYINT(1) NOT NULL DEFAULT 0', $added);
        core_add_column_if_missing('orders', 'refund_status', 'VARCHAR(50) DEFAULT NULL', $added);
        core_add_column_if_missing('orders', 'refunds_count', 'INT(11) NOT NULL DEFAULT 0', $added);
        core_add_column_if_missing('orders', 'refund_total_amount', 'DECIMAL(18,4) NOT NULL DEFAULT 0.0000', $added);
        core_add_column_if_missing('orders', 'refund_currency_code', 'VARCHAR(20) DEFAULT NULL', $added);
        core_add_column_if_missing('orders', 'shopify_refunds_json', 'LONGTEXT DEFAULT NULL', $added);
        core_add_column_if_missing('orders', 'shopify_refunded_at', 'DATETIME DEFAULT NULL', $added);
        core_add_column_if_missing('orders', 'refund_sync_date', 'DATETIME DEFAULT NULL', $added);
        core_add_column_if_missing('orders', 'refund_sync_error', 'LONGTEXT DEFAULT NULL', $added);
        core_add_column_if_missing('orders', 'ns_refund_sync_status', 'INT(11) NOT NULL DEFAULT 0', $added);
        core_add_column_if_missing('orders', 'ns_refund_sync_date', 'DATETIME DEFAULT NULL', $added);
        core_add_column_if_missing('orders', 'ns_refund_error', 'LONGTEXT DEFAULT NULL', $added);
    }
    if (!core_table_exists('shopify_refund_netsuite_sync')) {
        core_db_query("CREATE TABLE `shopify_refund_netsuite_sync` (
            `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
            `shopify_order_id` VARCHAR(64) NOT NULL,
            `shopify_refund_id` VARCHAR(64) NOT NULL,
            `shopify_refund_gid` VARCHAR(120) DEFAULT NULL,
            `ns_sales_order_id` VARCHAR(64) DEFAULT NULL,
            `ns_credit_memo_id` VARCHAR(64) DEFAULT NULL,
            `ns_credit_memo_tranid` VARCHAR(64) DEFAULT NULL,
            `refund_type` ENUM('items_only','money_only','items_and_money','unknown') NOT NULL DEFAULT 'unknown',
            `items_refund_amount` DECIMAL(18,4) NOT NULL DEFAULT 0,
            `shipping_refund_amount` DECIMAL(18,4) NOT NULL DEFAULT 0,
            `money_refund_amount` DECIMAL(18,4) NOT NULL DEFAULT 0,
            `adjustment_refund_amount` DECIMAL(18,4) NOT NULL DEFAULT 0,
            `currency` VARCHAR(8) DEFAULT NULL,
            `status` ENUM('pending','success','error','skipped','dry_run') NOT NULL DEFAULT 'pending',
            `error_message` TEXT DEFAULT NULL,
            `shopify_raw_json` LONGTEXT DEFAULT NULL,
            `netsuite_payload_json` LONGTEXT DEFAULT NULL,
            `netsuite_response_json` LONGTEXT DEFAULT NULL,
            `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
            `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
            PRIMARY KEY (`id`),
            UNIQUE KEY `uniq_shopify_refund` (`shopify_refund_id`),
            KEY `idx_shopify_order_id` (`shopify_order_id`),
            KEY `idx_status` (`status`)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
        $added[] = 'shopify_refund_netsuite_sync';
    }
    return $added;
}

/* ========================= Shopify Core ========================= */

function shopify_token_cache_file(): string
{
    if (defined('SHOPIFY_TOKEN_CACHE_FILE') && SHOPIFY_TOKEN_CACHE_FILE !== '') return (string)SHOPIFY_TOKEN_CACHE_FILE;
    return CLEAN_CRON_ROOT . 'storage/cache/shopify_access_token.json';
}

function shopify_token_refresh_buffer(): int
{
    return defined('SHOPIFY_TOKEN_REFRESH_BUFFER') ? (int)SHOPIFY_TOKEN_REFRESH_BUFFER : 120;
}

function shopify_rest_limit(): int
{
    return defined('SHOPIFY_REST_LIMIT') ? (int)SHOPIFY_REST_LIMIT : 250;
}

function shopify_get_cached_token(): ?string
{
    $file = shopify_token_cache_file();
    if (!is_file($file)) return null;
    $data = json_decode((string)file_get_contents($file), true);
    if (!is_array($data) || empty($data['access_token'])) return null;
    if ((string)($data['store_domain'] ?? '') !== (string)SHOPIFY_STORE_DOMAIN) return null;
    if ((string)($data['client_id'] ?? '') !== (string)SHOPIFY_CLIENT_ID) return null;
    $expiresAt = (int)($data['expires_at'] ?? 0);
    if ($expiresAt > 0 && $expiresAt <= time() + shopify_token_refresh_buffer()) return null;
    return (string)$data['access_token'];
}

function shopify_save_token(array $data): void
{
    $file = shopify_token_cache_file();
    $dir = dirname($file);
    if (!is_dir($dir)) @mkdir($dir, 0775, true);
    @file_put_contents($file, core_json_encode($data), LOCK_EX);
}

function shopify_request_admin_token(): string
{
    core_require_constants(['SHOPIFY_STORE_DOMAIN', 'SHOPIFY_CLIENT_ID', 'SHOPIFY_CLIENT_SECRET']);
    $url = 'https://' . SHOPIFY_STORE_DOMAIN . '/admin/oauth/access_token';
    $payload = [
        'client_id' => (string)SHOPIFY_CLIENT_ID,
        'client_secret' => (string)SHOPIFY_CLIENT_SECRET,
        'grant_type' => 'client_credentials',
    ];
    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST => true,
        CURLOPT_TIMEOUT => 60,
        CURLOPT_CONNECTTIMEOUT => 20,
        CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'Accept: application/json'],
        CURLOPT_POSTFIELDS => core_json_encode($payload),
    ]);
    $raw = curl_exec($ch);
    if ($raw === false) {
        $err = curl_error($ch); $no = curl_errno($ch); curl_close($ch);
        throw new RuntimeException('Shopify token cURL error #' . $no . ': ' . $err);
    }
    $status = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);
    $json = json_decode($raw, true);
    if ($status < 200 || $status >= 300 || !is_array($json) || empty($json['access_token'])) {
        throw new RuntimeException('Shopify token request failed HTTP ' . $status . ': ' . $raw);
    }
    $expiresIn = (int)($json['expires_in'] ?? 86400);
    $cache = [
        'access_token' => (string)$json['access_token'],
        'token_type' => $json['token_type'] ?? 'Bearer',
        'store_domain' => (string)SHOPIFY_STORE_DOMAIN,
        'client_id' => (string)SHOPIFY_CLIENT_ID,
        'expires_in' => $expiresIn,
        'expires_at' => time() + max(60, $expiresIn - shopify_token_refresh_buffer()),
        'saved_at' => time(),
    ];
    shopify_save_token($cache);
    return $cache['access_token'];
}

function shopify_access_token(): string
{
    $cached = shopify_get_cached_token();
    if ($cached) return $cached;
    try {
        return shopify_request_admin_token();
    } catch (Throwable $e) {
        if (defined('SHOPIFY_ADMIN_TOKEN') && SHOPIFY_ADMIN_TOKEN !== '' && strpos((string)SHOPIFY_ADMIN_TOKEN, 'xxxxx') === false) {
            return (string)SHOPIFY_ADMIN_TOKEN;
        }
        throw $e;
    }
}

function shopify_api_version(): string
{
    core_require_constants(['SHOPIFY_API_VERSION']);
    return (string)SHOPIFY_API_VERSION;
}

function shopify_request(string $method, string $path, ?array $payload = null): array
{
    core_require_constants(['SHOPIFY_STORE_DOMAIN', 'SHOPIFY_API_VERSION']);
    $url = 'https://' . SHOPIFY_STORE_DOMAIN . '/admin/api/' . shopify_api_version() . $path;
    $headersText = '';
    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_CUSTOMREQUEST => strtoupper($method),
        CURLOPT_TIMEOUT => 120,
        CURLOPT_CONNECTTIMEOUT => 20,
        CURLOPT_HTTPHEADER => [
            'X-Shopify-Access-Token: ' . shopify_access_token(),
            'Content-Type: application/json',
            'Accept: application/json',
        ],
        CURLOPT_HEADERFUNCTION => function ($curl, $line) use (&$headersText) { $headersText .= $line; return strlen($line); },
    ]);
    if ($payload !== null) curl_setopt($ch, CURLOPT_POSTFIELDS, core_json_encode($payload));
    $body = curl_exec($ch);
    if ($body === false) {
        $err = curl_error($ch); $no = curl_errno($ch); curl_close($ch);
        return ['ok' => false, 'status' => 0, 'body' => null, 'raw' => '', 'headers' => $headersText, 'error' => 'cURL #' . $no . ': ' . $err];
    }
    $status = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);
    $json = json_decode((string)$body, true);
    return [
        'ok' => $status >= 200 && $status < 300,
        'status' => $status,
        'headers' => $headersText,
        'body' => is_array($json) ? $json : null,
        'raw' => (string)$body,
        'error' => '',
    ];
}

function shopify_graphql(string $query, array $variables = []): array
{
    $resp = shopify_request('POST', '/graphql.json', [
        'query' => $query,
        'variables' => $variables,
    ]);
    if (!$resp['ok']) {
        throw new RuntimeException('Shopify GraphQL HTTP ' . $resp['status'] . ': ' . $resp['raw']);
    }
    $body = $resp['body'] ?: [];
    if (!empty($body['errors'])) {
        throw new RuntimeException('Shopify GraphQL errors: ' . core_json_encode($body['errors']));
    }
    return is_array($body['data'] ?? null) ? $body['data'] : [];
}

function shopify_rest_get_paginated(string $path, array $query = [], string $rootKey = ''): array
{
    core_require_constants(['SHOPIFY_STORE_DOMAIN', 'SHOPIFY_API_VERSION']);
    $url = 'https://' . SHOPIFY_STORE_DOMAIN . '/admin/api/' . shopify_api_version() . $path;
    if ($query) $url .= '?' . http_build_query($query);
    $rows = [];
    $pages = 0;
    while ($url) {
        $pages++;
        $headerText = '';
        $ch = curl_init($url);
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_HEADER => false,
            CURLOPT_TIMEOUT => 120,
            CURLOPT_CONNECTTIMEOUT => 20,
            CURLOPT_HTTPHEADER => [
                'X-Shopify-Access-Token: ' . shopify_access_token(),
                'Content-Type: application/json',
                'Accept: application/json',
            ],
            CURLOPT_HEADERFUNCTION => function ($curl, $line) use (&$headerText) { $headerText .= $line; return strlen($line); },
        ]);
        $raw = curl_exec($ch);
        if ($raw === false) {
            $err = curl_error($ch); $no = curl_errno($ch); curl_close($ch);
            throw new RuntimeException('Shopify REST cURL #' . $no . ': ' . $err);
        }
        $status = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);
        if ($status < 200 || $status >= 300) {
            throw new RuntimeException('Shopify REST HTTP ' . $status . ': ' . $raw);
        }
        $json = json_decode((string)$raw, true);
        if (!is_array($json)) throw new RuntimeException('Invalid Shopify JSON: ' . $raw);
        if ($rootKey !== '' && isset($json[$rootKey]) && is_array($json[$rootKey])) {
            $rows = array_merge($rows, $json[$rootKey]);
        } else {
            foreach ($json as $v) {
                if (is_array($v)) { $rows = array_merge($rows, $v); break; }
            }
        }
        $url = shopify_extract_next_link($headerText);
        if ($pages > 200) throw new RuntimeException('Shopify pagination safety limit reached.');
    }
    return ['rows' => $rows, 'pages' => $pages];
}

function shopify_extract_next_link(string $headers): ?string
{
    foreach (preg_split("/\r\n|\n|\r/", $headers) as $line) {
        if (stripos($line, 'Link:') !== 0) continue;
        foreach (explode(',', trim(substr($line, 5))) as $part) {
            if (strpos($part, 'rel="next"') !== false && preg_match('/<([^>]+)>/', $part, $m)) return $m[1];
        }
    }
    return null;
}

/* ========================= Shopify Queries ========================= */

function shopify_order_list_query(): string
{
    return <<<'GQL'
query GetOrders($first: Int!, $after: String, $query: String) {
  orders(first: $first, after: $after, query: $query, sortKey: CREATED_AT, reverse: false) {
    edges { cursor node { id legacyResourceId name createdAt updatedAt displayFinancialStatus displayFulfillmentStatus } }
    pageInfo { hasNextPage endCursor }
  }
}
GQL;
}

function shopify_order_detail_query(): string
{
    return <<<'GQL'
query GetOrder($id: ID!) {
  order(id: $id) {
    id legacyResourceId name confirmationNumber createdAt updatedAt processedAt closedAt cancelledAt cancelReason
    displayFinancialStatus displayFulfillmentStatus currencyCode subtotalLineItemsQuantity email phone note tags poNumber sourceName taxExempt taxesIncluded fullyPaid canMarkAsPaid paymentGatewayNames
    currentSubtotalPriceSet { shopMoney { amount currencyCode } }
    currentTotalPriceSet { shopMoney { amount currencyCode } }
    currentTotalTaxSet { shopMoney { amount currencyCode } }
    currentTotalDiscountsSet { shopMoney { amount currencyCode } }
    totalOutstandingSet { shopMoney { amount currencyCode } }
    totalReceivedSet { shopMoney { amount currencyCode } }
    totalRefundedSet { shopMoney { amount currencyCode } }
    totalShippingPriceSet { shopMoney { amount currencyCode } }
    shippingAddress { firstName lastName company address1 address2 city province provinceCode zip country countryCode phone }
    billingAddress { firstName lastName company address1 address2 city province provinceCode zip country countryCode phone }
    customer { id legacyResourceId firstName lastName email phone note tags taxExempt defaultAddress { address1 address2 city province provinceCode zip country countryCode phone } }
    shippingLines(first: 50) { nodes { title code originalPriceSet { shopMoney { amount currencyCode } } discountedPriceSet { shopMoney { amount currencyCode } } taxLines { rate title priceSet { shopMoney { amount currencyCode } } } } }
    discountApplications(first: 50) { nodes { __typename allocationMethod targetSelection targetType value { __typename ... on MoneyV2 { amount currencyCode } ... on PricingPercentageValue { percentage } } } }
    lineItems(first: 250) { nodes { id sku name title quantity currentQuantity refundableQuantity unfulfilledQuantity vendor requiresShipping taxable isGiftCard variantTitle fulfillableQuantity
      discountedUnitPriceSet { shopMoney { amount currencyCode } } originalUnitPriceSet { shopMoney { amount currencyCode } } discountedTotalSet { shopMoney { amount currencyCode } } originalTotalSet { shopMoney { amount currencyCode } } totalDiscountSet { shopMoney { amount currencyCode } }
      product { id legacyResourceId title handle vendor productType }
      variant { id legacyResourceId title sku barcode selectedOptions { name value } }
      customAttributes { key value }
      discountAllocations { allocatedAmountSet { shopMoney { amount currencyCode } } }
      taxLines { rate title priceSet { shopMoney { amount currencyCode } } }
    } }
    transactions { id kind status gateway formattedGateway processedAt test paymentId authorizationCode errorCode manuallyCapturable multiCapturable manualPaymentGateway
      amountSet { shopMoney { amount currencyCode } presentmentMoney { amount currencyCode } }
      receiptJson parentTransaction { id kind status }
    }
  }
}
GQL;
}

function shopify_product_query(): string
{
    return <<<'GQL'
query GetProduct($id: ID!) {
  product(id: $id) {
    id legacyResourceId title handle vendor productType status tags descriptionHtml createdAt updatedAt
    options { id name position optionValues { id name } }
    featuredMedia { preview { image { id url altText width height } } }
    media(first: 20) { nodes { preview { image { id url altText width height } } } }
    variants(first: 100) { nodes { id legacyResourceId title sku barcode taxable inventoryPolicy inventoryQuantity price compareAtPrice selectedOptions { name value } image { id url altText width height } } }
  }
}
GQL;
}

function shopify_locations_query(): string
{
    return <<<'GQL'
query GetLocations($first: Int!, $after: String, $includeInactive: Boolean!, $includeLegacy: Boolean!) {
  locations(first: $first, after: $after, includeInactive: $includeInactive, includeLegacy: $includeLegacy) {
    edges { cursor node { id legacyResourceId name isActive fulfillsOnlineOrders shipsInventory address { address1 address2 city provinceCode zip countryCode phone } } }
    pageInfo { hasNextPage endCursor }
  }
}
GQL;
}

function shopify_get_order_detail(string $shopifyOrderId): array
{
    $gid = str_starts_with($shopifyOrderId, 'gid://') ? $shopifyOrderId : 'gid://shopify/Order/' . $shopifyOrderId;
    $data = shopify_graphql(shopify_order_detail_query(), ['id' => $gid]);
    return is_array($data['order'] ?? null) ? $data['order'] : [];
}

function shopify_get_product_detail(string $productGidOrId): array
{
    $gid = str_starts_with($productGidOrId, 'gid://') ? $productGidOrId : 'gid://shopify/Product/' . $productGidOrId;
    $data = shopify_graphql(shopify_product_query(), ['id' => $gid]);
    return is_array($data['product'] ?? null) ? $data['product'] : [];
}

function shopify_get_refunds_for_order(string $shopifyOrderId): array
{
    $res = shopify_rest_get_paginated('/orders/' . rawurlencode($shopifyOrderId) . '/refunds.json', ['limit' => shopify_rest_limit()], 'refunds');
    return $res['rows'];
}

/* ========================= Upserts ========================= */

function upsert_order(array $order, array $refunds = []): ?array
{
    $shopifyOrderId = (string)($order['legacyResourceId'] ?? '');
    if ($shopifyOrderId === '') return null;
    $customer = is_array($order['customer'] ?? null) ? $order['customer'] : [];
    $gatewayNames = array_values(array_filter(array_map('strval', $order['paymentGatewayNames'] ?? [])));
    $summary = summarize_refunds($refunds, $order);
    $refundJson = $refunds ? core_json_encode(['shopify_order' => $order, 'summary' => $summary, 'refunds' => $refunds, 'synced_at' => core_now()]) : null;
    $now = core_now();

    $sql = "INSERT INTO `orders` (
        `shopify_order_id`, `shopify_graphql_id`, `order_name`, `order_date`, `created_at_shopify`, `updated_at_shopify`, `processed_at_shopify`,
        `financial_status`, `fulfillment_status`, `currency_code`, `customer_email`, `customer_phone`, `gateway_names_json`, `archived_file_path`, `payload_json`,
        `is_refunded`, `refund_status`, `refunds_count`, `refund_total_amount`, `refund_currency_code`, `shopify_refunds_json`, `shopify_refunded_at`, `refund_sync_date`, `refund_sync_error`,
        `created_at`, `updated_at`
    ) VALUES (
        " . core_sql($shopifyOrderId) . ",
        " . core_sql($order['id'] ?? null) . ",
        " . core_sql($order['name'] ?? null) . ",
        " . core_sql(core_date($order['createdAt'] ?? null)) . ",
        " . core_sql(core_datetime($order['createdAt'] ?? null)) . ",
        " . core_sql(core_datetime($order['updatedAt'] ?? null)) . ",
        " . core_sql(core_datetime($order['processedAt'] ?? null)) . ",
        " . core_sql($summary['is_refunded'] ? $summary['refund_status'] : ($order['displayFinancialStatus'] ?? null)) . ",
        " . core_sql($order['displayFulfillmentStatus'] ?? null) . ",
        " . core_sql($order['currencyCode'] ?? null) . ",
        " . core_sql($order['email'] ?? ($customer['email'] ?? null)) . ",
        " . core_sql($order['phone'] ?? ($customer['phone'] ?? null)) . ",
        " . core_sql(core_json_encode($gatewayNames)) . ",
        '',
        " . core_sql(core_json_encode($order)) . ",
        " . (int)$summary['is_refunded'] . ",
        " . core_sql($summary['refund_status']) . ",
        " . (int)$summary['refunds_count'] . ",
        " . core_sql_num($summary['refund_total_amount']) . ",
        " . core_sql($summary['refund_currency_code']) . ",
        " . core_sql($refundJson) . ",
        " . core_sql($summary['shopify_refunded_at']) . ",
        " . ($summary['is_refunded'] ? 'NOW()' : 'NULL') . ",
        NULL,
        " . core_sql($now) . ",
        " . core_sql($now) . "
    ) ON DUPLICATE KEY UPDATE
        `shopify_graphql_id` = VALUES(`shopify_graphql_id`),
        `order_name` = VALUES(`order_name`),
        `order_date` = VALUES(`order_date`),
        `created_at_shopify` = VALUES(`created_at_shopify`),
        `updated_at_shopify` = VALUES(`updated_at_shopify`),
        `processed_at_shopify` = VALUES(`processed_at_shopify`),
        `financial_status` = VALUES(`financial_status`),
        `fulfillment_status` = VALUES(`fulfillment_status`),
        `currency_code` = VALUES(`currency_code`),
        `customer_email` = VALUES(`customer_email`),
        `customer_phone` = VALUES(`customer_phone`),
        `gateway_names_json` = VALUES(`gateway_names_json`),
        `payload_json` = VALUES(`payload_json`),
        `is_refunded` = VALUES(`is_refunded`),
        `refund_status` = VALUES(`refund_status`),
        `refunds_count` = VALUES(`refunds_count`),
        `refund_total_amount` = VALUES(`refund_total_amount`),
        `refund_currency_code` = VALUES(`refund_currency_code`),
        `shopify_refunds_json` = VALUES(`shopify_refunds_json`),
        `shopify_refunded_at` = VALUES(`shopify_refunded_at`),
        `refund_sync_date` = VALUES(`refund_sync_date`),
        `refund_sync_error` = VALUES(`refund_sync_error`),
        `updated_at` = VALUES(`updated_at`)";
    core_db_query($sql);
    return find_order_by_shopify_id($shopifyOrderId);
}

function find_order_by_shopify_id(string $shopifyOrderId): ?array
{
    return core_db_fetch_row("SELECT * FROM `orders` WHERE `shopify_order_id` = " . core_sql($shopifyOrderId) . " LIMIT 1");
}

function find_order_by_id(int $id): ?array
{
    return core_db_fetch_row("SELECT * FROM `orders` WHERE `id` = " . (int)$id . " LIMIT 1");
}

function upsert_payment_methods_and_transactions(int $orderDbId, string $shopifyOrderId, array $order): array
{
    $savedMethods = 0; $savedPayments = 0;
    $transactions = is_array($order['transactions'] ?? null) ? $order['transactions'] : [];
    $gatewayNames = array_values(array_filter(array_map('strval', $order['paymentGatewayNames'] ?? [])));
    foreach ($gatewayNames as $gateway) {
        $methodKey = strtolower(trim($gateway));
        if ($methodKey === '') continue;
        $now = core_now();
        core_db_query("INSERT INTO `payment_methods` (`method_key`,`gateway_name`,`formatted_gateway`,`method_kind`,`status`,`source_type`,`source_order_id`,`details_json`,`first_seen_at`,`last_seen_at`,`created_at`,`updated_at`) VALUES (
            " . core_sql($methodKey) . ", " . core_sql($gateway) . ", " . core_sql($gateway) . ", 'gateway', NULL, 'order', " . core_sql($shopifyOrderId) . ", " . core_sql(core_json_encode(['gateway' => $gateway])) . ", " . core_sql($now) . ", " . core_sql($now) . ", " . core_sql($now) . ", " . core_sql($now) . ")
            ON DUPLICATE KEY UPDATE `gateway_name`=VALUES(`gateway_name`), `formatted_gateway`=VALUES(`formatted_gateway`), `last_seen_at`=VALUES(`last_seen_at`), `updated_at`=VALUES(`updated_at`)");
        core_db_query("INSERT INTO `payment_method_mappings` (`unique_key`,`method_key`,`gateway_name`,`formatted_gateway`,`source_type`,`source_order_id`,`payload_json`,`first_seen_at`,`last_seen_at`,`created_at`,`updated_at`) VALUES (
            " . core_sql('method:' . $methodKey) . ", " . core_sql($methodKey) . ", " . core_sql($gateway) . ", " . core_sql($gateway) . ", 'order', " . core_sql($shopifyOrderId) . ", " . core_sql(core_json_encode(['gateway' => $gateway])) . ", " . core_sql($now) . ", " . core_sql($now) . ", " . core_sql($now) . ", " . core_sql($now) . ")
            ON DUPLICATE KEY UPDATE `gateway_name`=VALUES(`gateway_name`), `formatted_gateway`=VALUES(`formatted_gateway`), `last_seen_at`=VALUES(`last_seen_at`), `updated_at`=VALUES(`updated_at`)");
        $savedMethods++;
    }
    foreach ($transactions as $trx) {
        if (!is_array($trx)) continue;
        $trxGid = (string)($trx['id'] ?? '');
        $trxId = core_gid_tail($trxGid);
        if ($trxId === '') continue;
        $gateway = (string)($trx['gateway'] ?? ($trx['formattedGateway'] ?? ''));
        $methodKey = strtolower(trim($gateway));
        $methodRow = $methodKey ? core_db_fetch_row("SELECT `id` FROM `payment_methods` WHERE `method_key` = " . core_sql($methodKey) . " LIMIT 1") : null;
        $methodId = is_array($methodRow) ? (int)$methodRow['id'] : 0;
        $shopMoney = core_array_get($trx, ['amountSet','shopMoney'], []);
        $presentMoney = core_array_get($trx, ['amountSet','presentmentMoney'], []);
        $parent = is_array($trx['parentTransaction'] ?? null) ? $trx['parentTransaction'] : [];
        $unique = $shopifyOrderId . ':' . $trxId;
        $now = core_now();
        core_db_query("INSERT INTO `order_payments` (`unique_key`,`order_id`,`shopify_order_id`,`payment_method_id`,`method_key`,`shopify_transaction_id`,`shopify_parent_transaction_id`,`transaction_kind`,`transaction_status`,`gateway_name`,`formatted_gateway`,`amount`,`currency_code`,`presentment_amount`,`presentment_currency_code`,`processed_at_shopify`,`authorization_code`,`payment_reference`,`is_test`,`receipt_json`,`payload_json`,`created_at`,`updated_at`) VALUES (
            " . core_sql($unique) . ", " . (int)$orderDbId . ", " . core_sql($shopifyOrderId) . ", " . ($methodId > 0 ? $methodId : 'NULL') . ", " . core_sql($methodKey) . ", " . core_sql($trxId) . ", " . core_sql(core_gid_tail($parent['id'] ?? '')) . ", " . core_sql($trx['kind'] ?? null) . ", " . core_sql($trx['status'] ?? null) . ", " . core_sql($gateway) . ", " . core_sql($trx['formattedGateway'] ?? $gateway) . ", " . core_sql_num($shopMoney['amount'] ?? 0) . ", " . core_sql($shopMoney['currencyCode'] ?? null) . ", " . core_sql_num($presentMoney['amount'] ?? 0) . ", " . core_sql($presentMoney['currencyCode'] ?? null) . ", " . core_sql(core_datetime($trx['processedAt'] ?? null)) . ", " . core_sql($trx['authorizationCode'] ?? null) . ", " . core_sql($trx['paymentId'] ?? null) . ", " . (!empty($trx['test']) ? 1 : 0) . ", " . core_sql($trx['receiptJson'] ?? null) . ", " . core_sql(core_json_encode($trx)) . ", " . core_sql($now) . ", " . core_sql($now) . ")
            ON DUPLICATE KEY UPDATE `payment_method_id`=VALUES(`payment_method_id`), `transaction_kind`=VALUES(`transaction_kind`), `transaction_status`=VALUES(`transaction_status`), `gateway_name`=VALUES(`gateway_name`), `formatted_gateway`=VALUES(`formatted_gateway`), `amount`=VALUES(`amount`), `currency_code`=VALUES(`currency_code`), `presentment_amount`=VALUES(`presentment_amount`), `presentment_currency_code`=VALUES(`presentment_currency_code`), `processed_at_shopify`=VALUES(`processed_at_shopify`), `receipt_json`=VALUES(`receipt_json`), `payload_json`=VALUES(`payload_json`), `updated_at`=VALUES(`updated_at`)");
        $savedPayments++;
    }
    return ['payment_methods' => $savedMethods, 'order_payments' => $savedPayments];
}

function upsert_order_lines_and_catalog(int $orderDbId, string $shopifyOrderId, array $order, bool $fetchProducts = true): array
{
    $savedLines = 0; $savedProducts = 0; $savedVariants = 0; $savedSkus = 0;
    $nodes = core_array_get($order, ['lineItems','nodes'], []);
    if (!is_array($nodes)) $nodes = [];
    $lineNo = 0;
    $productGids = [];
    foreach ($nodes as $line) {
        if (!is_array($line)) continue;
        $lineNo++;
        $product = is_array($line['product'] ?? null) ? $line['product'] : [];
        $variant = is_array($line['variant'] ?? null) ? $line['variant'] : [];
        $lineGid = (string)($line['id'] ?? '');
        $lineId = core_gid_tail($lineGid);
        if ($lineId === '') $lineId = md5(core_json_encode($line));
        $productId = (string)($product['legacyResourceId'] ?? core_gid_tail($product['id'] ?? ''));
        $variantId = (string)($variant['legacyResourceId'] ?? core_gid_tail($variant['id'] ?? ''));
        $sku = (string)($line['sku'] ?? ($variant['sku'] ?? ''));
        $unique = $shopifyOrderId . ':' . $lineId;
        $now = core_now();
        $unit = core_array_get($line, ['originalUnitPriceSet','shopMoney'], []);
        $discUnit = core_array_get($line, ['discountedUnitPriceSet','shopMoney'], []);
        $origTotal = core_array_get($line, ['originalTotalSet','shopMoney'], []);
        $discTotal = core_array_get($line, ['discountedTotalSet','shopMoney'], []);
        $discount = core_array_get($line, ['totalDiscountSet','shopMoney'], []);
        $taxTotal = 0.0; $taxCurrency = $order['currencyCode'] ?? null;
        foreach (($line['taxLines'] ?? []) as $taxLine) {
            if (is_array($taxLine)) {
                $money = core_array_get($taxLine, ['priceSet','shopMoney'], []);
                $taxTotal += core_money($money['amount'] ?? 0);
                if (!empty($money['currencyCode'])) $taxCurrency = $money['currencyCode'];
            }
        }
        core_db_query("INSERT INTO `orders_info` (`unique_key`,`order_id`,`shopify_order_id`,`shopify_line_item_id`,`line_number`,`shopify_product_id`,`shopify_product_graphql_id`,`shopify_variant_id`,`shopify_variant_graphql_id`,`product_title`,`line_item_name`,`variant_title`,`vendor`,`product_type`,`product_handle`,`line_item_sku`,`variant_sku`,`product_primary_sku`,`barcode`,`quantity`,`current_quantity`,`refundable_quantity`,`unfulfilled_quantity`,`fulfillable_quantity`,`requires_shipping`,`taxable`,`is_gift_card`,`unit_price`,`unit_price_currency`,`discounted_unit_price`,`discounted_unit_currency`,`original_total`,`original_total_currency`,`discounted_total`,`discounted_total_currency`,`total_discount`,`total_discount_currency`,`tax_total`,`tax_currency`,`selected_options_json`,`custom_attributes_json`,`tax_lines_json`,`discount_allocations_json`,`payload_json`,`created_at`,`updated_at`) VALUES (
            " . core_sql($unique) . ", " . (int)$orderDbId . ", " . core_sql($shopifyOrderId) . ", " . core_sql($lineId) . ", " . (int)$lineNo . ", " . core_sql($productId) . ", " . core_sql($product['id'] ?? null) . ", " . core_sql($variantId) . ", " . core_sql($variant['id'] ?? null) . ", " . core_sql($product['title'] ?? null) . ", " . core_sql($line['name'] ?? null) . ", " . core_sql($line['variantTitle'] ?? ($variant['title'] ?? null)) . ", " . core_sql($line['vendor'] ?? ($product['vendor'] ?? null)) . ", " . core_sql($product['productType'] ?? null) . ", " . core_sql($product['handle'] ?? null) . ", " . core_sql($sku) . ", " . core_sql($variant['sku'] ?? null) . ", " . core_sql($sku) . ", " . core_sql($variant['barcode'] ?? null) . ", " . (int)($line['quantity'] ?? 0) . ", " . (int)($line['currentQuantity'] ?? 0) . ", " . (int)($line['refundableQuantity'] ?? 0) . ", " . (int)($line['unfulfilledQuantity'] ?? 0) . ", " . (int)($line['fulfillableQuantity'] ?? 0) . ", " . (!empty($line['requiresShipping']) ? 1 : 0) . ", " . (!empty($line['taxable']) ? 1 : 0) . ", " . (!empty($line['isGiftCard']) ? 1 : 0) . ", " . core_sql_num($unit['amount'] ?? 0) . ", " . core_sql($unit['currencyCode'] ?? null) . ", " . core_sql_num($discUnit['amount'] ?? 0) . ", " . core_sql($discUnit['currencyCode'] ?? null) . ", " . core_sql_num($origTotal['amount'] ?? 0) . ", " . core_sql($origTotal['currencyCode'] ?? null) . ", " . core_sql_num($discTotal['amount'] ?? 0) . ", " . core_sql($discTotal['currencyCode'] ?? null) . ", " . core_sql_num($discount['amount'] ?? 0) . ", " . core_sql($discount['currencyCode'] ?? null) . ", " . core_sql_num($taxTotal) . ", " . core_sql($taxCurrency) . ", " . core_sql(core_json_encode($variant['selectedOptions'] ?? [])) . ", " . core_sql(core_json_encode($line['customAttributes'] ?? [])) . ", " . core_sql(core_json_encode($line['taxLines'] ?? [])) . ", " . core_sql(core_json_encode($line['discountAllocations'] ?? [])) . ", " . core_sql(core_json_encode($line)) . ", " . core_sql($now) . ", " . core_sql($now) . ")
            ON DUPLICATE KEY UPDATE `line_number`=VALUES(`line_number`), `product_title`=VALUES(`product_title`), `line_item_name`=VALUES(`line_item_name`), `variant_title`=VALUES(`variant_title`), `line_item_sku`=VALUES(`line_item_sku`), `quantity`=VALUES(`quantity`), `current_quantity`=VALUES(`current_quantity`), `refundable_quantity`=VALUES(`refundable_quantity`), `unfulfilled_quantity`=VALUES(`unfulfilled_quantity`), `fulfillable_quantity`=VALUES(`fulfillable_quantity`), `unit_price`=VALUES(`unit_price`), `discounted_total`=VALUES(`discounted_total`), `tax_total`=VALUES(`tax_total`), `payload_json`=VALUES(`payload_json`), `updated_at`=VALUES(`updated_at`)");
        $savedLines++;
        if (!empty($product['id'])) $productGids[(string)$product['id']] = true;
        upsert_variant_mapping_from_line($line, $shopifyOrderId);
        $savedVariants++;
        if ($sku !== '') { upsert_sku_mapping_from_line($line, $shopifyOrderId); $savedSkus++; }
    }
    if ($fetchProducts) {
        foreach (array_keys($productGids) as $productGid) {
            try {
                $prod = shopify_get_product_detail($productGid);
                if ($prod) {
                    upsert_product($prod, $shopifyOrderId);
                    foreach (core_array_get($prod, ['variants','nodes'], []) as $variant) {
                        if (is_array($variant)) {
                            upsert_variant_mapping_from_product_variant($prod, $variant, $shopifyOrderId);
                            $savedVariants++;
                            if (!empty($variant['sku'])) { upsert_sku_mapping_from_product_variant($prod, $variant, $shopifyOrderId); $savedSkus++; }
                        }
                    }
                    $savedProducts++;
                }
            } catch (Throwable $e) {
                log_sync('error', 'catalog', 'Failed to fetch product from order.', ['product_gid' => $productGid, 'error' => $e->getMessage()]);
            }
        }
    }
    return ['orders_info' => $savedLines, 'products' => $savedProducts, 'product_variant_mappings' => $savedVariants, 'sku_mappings' => $savedSkus];
}

function upsert_product(array $product, ?string $lastSeenOrderId = null): void
{
    $id = (string)($product['legacyResourceId'] ?? core_gid_tail($product['id'] ?? ''));
    if ($id === '') return;
    $variants = core_array_get($product, ['variants','nodes'], []);
    $primarySku = ''; $primaryBarcode = '';
    if (is_array($variants) && isset($variants[0]) && is_array($variants[0])) {
        $primarySku = (string)($variants[0]['sku'] ?? '');
        $primaryBarcode = (string)($variants[0]['barcode'] ?? '');
    }
    $now = core_now();
    core_db_query("INSERT INTO `products` (`unique_key`,`shopify_product_id`,`shopify_graphql_product_id`,`title`,`handle`,`vendor`,`product_type`,`status`,`tags_json`,`options_json`,`media_json`,`variants_json`,`primary_sku`,`primary_barcode`,`last_seen_order_id`,`payload_json`,`first_seen_at`,`last_seen_at`,`created_at`,`updated_at`) VALUES (
        " . core_sql('product:' . $id) . ", " . core_sql($id) . ", " . core_sql($product['id'] ?? null) . ", " . core_sql($product['title'] ?? '') . ", " . core_sql($product['handle'] ?? null) . ", " . core_sql($product['vendor'] ?? null) . ", " . core_sql($product['productType'] ?? null) . ", " . core_sql($product['status'] ?? null) . ", " . core_sql(core_json_encode($product['tags'] ?? [])) . ", " . core_sql(core_json_encode($product['options'] ?? [])) . ", " . core_sql(core_json_encode($product['media'] ?? [])) . ", " . core_sql(core_json_encode($variants)) . ", " . core_sql($primarySku) . ", " . core_sql($primaryBarcode) . ", " . core_sql($lastSeenOrderId) . ", " . core_sql(core_json_encode($product)) . ", " . core_sql($now) . ", " . core_sql($now) . ", " . core_sql($now) . ", " . core_sql($now) . ")
        ON DUPLICATE KEY UPDATE `title`=VALUES(`title`), `handle`=VALUES(`handle`), `vendor`=VALUES(`vendor`), `product_type`=VALUES(`product_type`), `status`=VALUES(`status`), `tags_json`=VALUES(`tags_json`), `options_json`=VALUES(`options_json`), `media_json`=VALUES(`media_json`), `variants_json`=VALUES(`variants_json`), `primary_sku`=VALUES(`primary_sku`), `primary_barcode`=VALUES(`primary_barcode`), `last_seen_order_id`=VALUES(`last_seen_order_id`), `payload_json`=VALUES(`payload_json`), `last_seen_at`=VALUES(`last_seen_at`), `updated_at`=VALUES(`updated_at`)");
}

function upsert_variant_mapping_from_line(array $line, string $lastSeenOrderId): void
{
    $product = is_array($line['product'] ?? null) ? $line['product'] : [];
    $variant = is_array($line['variant'] ?? null) ? $line['variant'] : [];
    if (!$variant && !$product) return;
    upsert_variant_mapping_from_product_variant($product, array_merge($variant, ['title' => $variant['title'] ?? ($line['variantTitle'] ?? null), 'sku' => $variant['sku'] ?? ($line['sku'] ?? null)]), $lastSeenOrderId);
}

function upsert_variant_mapping_from_product_variant(array $product, array $variant, ?string $lastSeenOrderId = null): void
{
    $productId = (string)($product['legacyResourceId'] ?? core_gid_tail($product['id'] ?? ''));
    $variantId = (string)($variant['legacyResourceId'] ?? core_gid_tail($variant['id'] ?? ''));
    if ($productId === '' && $variantId === '') return;
    $unique = 'variant:' . ($variantId ?: md5(core_json_encode($variant)));
    $options = $variant['selectedOptions'] ?? [];
    $optionSummary = [];
    if (is_array($options)) foreach ($options as $o) if (is_array($o)) $optionSummary[] = ($o['name'] ?? '') . ': ' . ($o['value'] ?? '');
    $now = core_now();
    core_db_query("INSERT INTO `product_variant_mappings` (`unique_key`,`shopify_product_id`,`shopify_variant_id`,`shopify_product_graphql_id`,`shopify_variant_graphql_id`,`shopify_product_title`,`shopify_variant_title`,`option_summary`,`shopify_sku`,`barcode`,`vendor`,`product_type`,`handle`,`options_json`,`payload_json`,`first_seen_at`,`last_seen_at`,`created_at`,`updated_at`) VALUES (
        " . core_sql($unique) . ", " . core_sql($productId) . ", " . core_sql($variantId) . ", " . core_sql($product['id'] ?? null) . ", " . core_sql($variant['id'] ?? null) . ", " . core_sql($product['title'] ?? null) . ", " . core_sql($variant['title'] ?? null) . ", " . core_sql(implode(', ', $optionSummary)) . ", " . core_sql($variant['sku'] ?? null) . ", " . core_sql($variant['barcode'] ?? null) . ", " . core_sql($product['vendor'] ?? null) . ", " . core_sql($product['productType'] ?? null) . ", " . core_sql($product['handle'] ?? null) . ", " . core_sql(core_json_encode($options)) . ", " . core_sql(core_json_encode(['product' => $product, 'variant' => $variant])) . ", " . core_sql($now) . ", " . core_sql($now) . ", " . core_sql($now) . ", " . core_sql($now) . ")
        ON DUPLICATE KEY UPDATE `shopify_product_id`=VALUES(`shopify_product_id`), `shopify_product_graphql_id`=VALUES(`shopify_product_graphql_id`), `shopify_product_title`=VALUES(`shopify_product_title`), `shopify_variant_title`=VALUES(`shopify_variant_title`), `option_summary`=VALUES(`option_summary`), `shopify_sku`=VALUES(`shopify_sku`), `barcode`=VALUES(`barcode`), `vendor`=VALUES(`vendor`), `product_type`=VALUES(`product_type`), `handle`=VALUES(`handle`), `options_json`=VALUES(`options_json`), `payload_json`=VALUES(`payload_json`), `last_seen_at`=VALUES(`last_seen_at`), `updated_at`=VALUES(`updated_at`)");
}

function upsert_sku_mapping_from_line(array $line, string $lastSeenOrderId): void
{
    $product = is_array($line['product'] ?? null) ? $line['product'] : [];
    $variant = is_array($line['variant'] ?? null) ? $line['variant'] : [];
    $variant['sku'] = $variant['sku'] ?? ($line['sku'] ?? '');
    upsert_sku_mapping_from_product_variant($product, $variant, $lastSeenOrderId);
}

function upsert_sku_mapping_from_product_variant(array $product, array $variant, ?string $lastSeenOrderId = null): void
{
    $sku = trim((string)($variant['sku'] ?? ''));
    if ($sku === '') return;
    $productId = (string)($product['legacyResourceId'] ?? core_gid_tail($product['id'] ?? ''));
    $variantId = (string)($variant['legacyResourceId'] ?? core_gid_tail($variant['id'] ?? ''));
    $now = core_now();
    core_db_query("INSERT INTO `sku_mappings` (`shopify_sku`,`shopify_product_id`,`shopify_variant_id`,`shopify_product_title`,`shopify_variant_title`,`barcode`,`vendor`,`product_type`,`handle`,`status`,`payload_json`,`last_seen_order_id`,`is_active`,`created_at`,`updated_at`) VALUES (
        " . core_sql($sku) . ", " . core_sql($productId) . ", " . core_sql($variantId) . ", " . core_sql($product['title'] ?? null) . ", " . core_sql($variant['title'] ?? null) . ", " . core_sql($variant['barcode'] ?? null) . ", " . core_sql($product['vendor'] ?? null) . ", " . core_sql($product['productType'] ?? null) . ", " . core_sql($product['handle'] ?? null) . ", " . core_sql($product['status'] ?? null) . ", " . core_sql(core_json_encode(['product' => $product, 'variant' => $variant])) . ", " . core_sql($lastSeenOrderId) . ", 1, " . core_sql($now) . ", " . core_sql($now) . ")
        ON DUPLICATE KEY UPDATE `shopify_product_id`=VALUES(`shopify_product_id`), `shopify_variant_id`=VALUES(`shopify_variant_id`), `shopify_product_title`=VALUES(`shopify_product_title`), `shopify_variant_title`=VALUES(`shopify_variant_title`), `barcode`=VALUES(`barcode`), `vendor`=VALUES(`vendor`), `product_type`=VALUES(`product_type`), `handle`=VALUES(`handle`), `status`=VALUES(`status`), `payload_json`=VALUES(`payload_json`), `last_seen_order_id`=VALUES(`last_seen_order_id`), `updated_at`=VALUES(`updated_at`)");
}

function upsert_shopify_location(array $loc): void
{
    $id = (string)($loc['legacyResourceId'] ?? core_gid_tail($loc['id'] ?? ''));
    if ($id === '') return;
    $addr = is_array($loc['address'] ?? null) ? $loc['address'] : [];
    $now = core_now();
    core_db_query("INSERT INTO `shopify_locations` (`unique_key`,`shopify_location_id`,`shopify_graphql_location_id`,`name`,`is_active`,`fulfills_online_orders`,`fulfills_subscriptions`,`supports_pickup_in_store`,`ships_inventory`,`address1`,`address2`,`city`,`province_code`,`zip`,`country_code`,`phone`,`legacy`,`metafields_json`,`payload_json`,`first_seen_at`,`last_seen_at`,`created_at`,`updated_at`) VALUES (
        " . core_sql('location:' . $id) . ", " . core_sql($id) . ", " . core_sql($loc['id'] ?? null) . ", " . core_sql($loc['name'] ?? '') . ", " . (!empty($loc['isActive']) ? 1 : 0) . ", " . (!empty($loc['fulfillsOnlineOrders']) ? 1 : 0) . ", 0, 0, " . (!empty($loc['shipsInventory']) ? 1 : 0) . ", " . core_sql($addr['address1'] ?? null) . ", " . core_sql($addr['address2'] ?? null) . ", " . core_sql($addr['city'] ?? null) . ", " . core_sql($addr['provinceCode'] ?? null) . ", " . core_sql($addr['zip'] ?? null) . ", " . core_sql($addr['countryCode'] ?? null) . ", " . core_sql($addr['phone'] ?? null) . ", 0, '[]', " . core_sql(core_json_encode($loc)) . ", " . core_sql($now) . ", " . core_sql($now) . ", " . core_sql($now) . ", " . core_sql($now) . ")
        ON DUPLICATE KEY UPDATE `name`=VALUES(`name`), `is_active`=VALUES(`is_active`), `fulfills_online_orders`=VALUES(`fulfills_online_orders`), `ships_inventory`=VALUES(`ships_inventory`), `address1`=VALUES(`address1`), `address2`=VALUES(`address2`), `city`=VALUES(`city`), `province_code`=VALUES(`province_code`), `zip`=VALUES(`zip`), `country_code`=VALUES(`country_code`), `phone`=VALUES(`phone`), `payload_json`=VALUES(`payload_json`), `last_seen_at`=VALUES(`last_seen_at`), `updated_at`=VALUES(`updated_at`)");
    core_db_query("INSERT INTO `shopify_location_mappings` (`unique_key`,`shopify_location_id`,`shopify_graphql_location_id`,`shopify_location_name`,`city`,`country_code`,`payload_json`,`first_seen_at`,`last_seen_at`,`created_at`,`updated_at`) VALUES (
        " . core_sql('location:' . $id) . ", " . core_sql($id) . ", " . core_sql($loc['id'] ?? null) . ", " . core_sql($loc['name'] ?? '') . ", " . core_sql($addr['city'] ?? null) . ", " . core_sql($addr['countryCode'] ?? null) . ", " . core_sql(core_json_encode($loc)) . ", " . core_sql($now) . ", " . core_sql($now) . ", " . core_sql($now) . ", " . core_sql($now) . ")
        ON DUPLICATE KEY UPDATE `shopify_location_name`=VALUES(`shopify_location_name`), `city`=VALUES(`city`), `country_code`=VALUES(`country_code`), `payload_json`=VALUES(`payload_json`), `last_seen_at`=VALUES(`last_seen_at`), `updated_at`=VALUES(`updated_at`)");
}

function log_sync(string $level, string $channel, string $message, array $context = []): void
{
    if (!core_table_exists('sync_logs')) return;
    core_db_query("INSERT INTO `sync_logs` (`level`,`channel`,`message`,`context_json`,`created_at`) VALUES (" . core_sql($level) . ", " . core_sql($channel) . ", " . core_sql($message) . ", " . core_sql(core_json_encode($context)) . ", " . core_sql(core_now()) . ")");
}

/* ========================= Refund helpers ========================= */

function refund_money_amount(array $refund): float
{
    $total = 0.0;
    foreach (($refund['transactions'] ?? []) as $trx) {
        if (!is_array($trx)) continue;
        $kind = strtolower((string)($trx['kind'] ?? ''));
        $status = strtolower((string)($trx['status'] ?? ''));
        if ($kind === 'refund' && ($status === '' || in_array($status, ['success','pending'], true))) $total += abs(core_money($trx['amount'] ?? 0));
    }
    return round($total, 4);
}

function refund_items_amount(array $refund): float
{
    $total = 0.0;
    foreach (($refund['refund_line_items'] ?? []) as $line) {
        if (!is_array($line)) continue;
        $subtotal = core_money($line['subtotal'] ?? 0);
        $tax = core_money($line['total_tax'] ?? 0);
        if ($subtotal > 0 || $tax > 0) $total += abs($subtotal) + abs($tax);
        else $total += abs(core_money($line['quantity'] ?? 0) * core_money(core_array_get($line, ['line_item','price'], 0)));
    }
    return round($total, 4);
}

function refund_shipping_amount(array $refund): float
{
    $total = 0.0;
    foreach (($refund['order_adjustments'] ?? []) as $adj) {
        if (!is_array($adj)) continue;
        $kind = strtolower((string)($adj['kind'] ?? ''));
        if ($kind === 'shipping_refund' || $kind === 'shipping') $total += abs(core_money($adj['amount'] ?? 0)) + abs(core_money($adj['tax_amount'] ?? 0));
    }
    return round($total, 4);
}

function summarize_refunds(array $refunds, ?array $shopifyOrder = null): array
{
    $ids = []; $gids = []; $last = null; $items = 0.0; $shipping = 0.0; $money = 0.0; $currency = $shopifyOrder['currencyCode'] ?? ($shopifyOrder['currency'] ?? null);
    foreach ($refunds as $refund) {
        if (!is_array($refund)) continue;
        if (!empty($refund['id'])) $ids[] = (string)$refund['id'];
        if (!empty($refund['admin_graphql_api_id'])) $gids[] = (string)$refund['admin_graphql_api_id'];
        $created = core_datetime($refund['created_at'] ?? null);
        if ($created && (!$last || strtotime($created) > strtotime($last))) $last = $created;
        $items += refund_items_amount($refund);
        $shipping += refund_shipping_amount($refund);
        $money += refund_money_amount($refund);
        if (!$currency) $currency = $refund['currency'] ?? core_array_get($refund, ['transactions',0,'currency']);
    }
    $total = $money > 0 ? $money : ($items + $shipping);
    $status = strtoupper((string)($shopifyOrder['displayFinancialStatus'] ?? ($shopifyOrder['financial_status'] ?? '')));
    if ($status === '') $status = count($refunds) ? ($total > 0 ? 'REFUNDED' : 'REFUND_EXISTS') : null;
    return [
        'is_refunded' => count($refunds) > 0 ? 1 : 0,
        'refund_status' => $status,
        'refunds_count' => count($refunds),
        'refund_total_amount' => round($total, 4),
        'refund_items_amount' => round($items, 4),
        'refund_shipping_amount' => round($shipping, 4),
        'refund_money_amount' => round($money, 4),
        'refund_currency_code' => $currency,
        'shopify_refunded_at' => $last,
        'refund_ids' => $ids,
        'refund_gids' => $gids,
    ];
}

function update_order_refund_local(string $shopifyOrderId, array $shopifyOrder, array $refunds, bool $dryRun = false): array
{
    $summary = summarize_refunds($refunds, $shopifyOrder);
    $payload = ['shopify_order' => $shopifyOrder, 'summary' => $summary, 'refunds' => $refunds, 'synced_at' => core_now()];
    if ($dryRun) return ['dry_run' => true, 'summary' => $summary, 'payload' => $payload];
    core_db_query("UPDATE `orders` SET
        `financial_status` = " . core_sql($summary['refund_status']) . ",
        `is_refunded` = " . (int)$summary['is_refunded'] . ",
        `refund_status` = " . core_sql($summary['refund_status']) . ",
        `refunds_count` = " . (int)$summary['refunds_count'] . ",
        `refund_total_amount` = " . core_sql_num($summary['refund_total_amount']) . ",
        `refund_currency_code` = " . core_sql($summary['refund_currency_code']) . ",
        `shopify_refunds_json` = " . core_sql(core_json_encode($payload)) . ",
        `shopify_refunded_at` = " . core_sql($summary['shopify_refunded_at']) . ",
        `refund_sync_date` = NOW(),
        `refund_sync_error` = NULL,
        `updated_at` = NOW()
        WHERE `shopify_order_id` = " . core_sql($shopifyOrderId) . " LIMIT 1");
    return ['dry_run' => false, 'updated' => true, 'summary' => $summary];
}

/* ========================= NetSuite Core ========================= */

function ns_rest_base(): string
{
    if (defined('NS_REST_RECORD_BASE') && NS_REST_RECORD_BASE !== '') return rtrim((string)NS_REST_RECORD_BASE, '/');
    core_require_constants(['NS_REST_ACCOUNT2']);
    return 'https://' . NS_REST_ACCOUNT2 . '.suitetalk.api.netsuite.com/services/rest/record/v1';
}

function ns_oauth_header(string $method, string $url): string
{
    core_require_constants(['NS_REST_CONSUMER_KEY','NS_REST_CONSUMER_SECRET','NS_REST_TOKEN_ID','NS_REST_TOKEN_SECRET','NS_REST_REALM']);
    $oauth = [
        'oauth_consumer_key' => (string)NS_REST_CONSUMER_KEY,
        'oauth_nonce' => bin2hex(random_bytes(16)),
        'oauth_signature_method' => 'HMAC-SHA256',
        'oauth_timestamp' => (string)time(),
        'oauth_token' => (string)NS_REST_TOKEN_ID,
        'oauth_version' => '1.0',
    ];
    $parts = parse_url($url);
    $baseUrl = $parts['scheme'] . '://' . $parts['host'] . ($parts['path'] ?? '');
    $query = [];
    if (!empty($parts['query'])) parse_str($parts['query'], $query);
    $params = array_merge($oauth, $query);
    ksort($params);
    $encoded = [];
    foreach ($params as $k => $v) $encoded[] = rawurlencode((string)$k) . '=' . rawurlencode((string)$v);
    $baseString = strtoupper($method) . '&' . rawurlencode($baseUrl) . '&' . rawurlencode(implode('&', $encoded));
    $key = rawurlencode((string)NS_REST_CONSUMER_SECRET) . '&' . rawurlencode((string)NS_REST_TOKEN_SECRET);
    $oauth['oauth_signature'] = base64_encode(hash_hmac('sha256', $baseString, $key, true));
    $partsOut = [];
    foreach ($oauth as $k => $v) $partsOut[] = rawurlencode($k) . '="' . rawurlencode((string)$v) . '"';
    return 'OAuth realm="' . rawurlencode((string)NS_REST_REALM) . '", ' . implode(', ', $partsOut);
}

function ns_rest_request(string $method, string $path, ?array $payload = null): array
{
    $url = ns_rest_base() . $path;
    $headers = [
        'Authorization: ' . ns_oauth_header($method, $url),
        'Content-Type: application/json',
        'Accept: application/json',
        'Prefer: return=representation',
    ];
    $headerText = '';
    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_CUSTOMREQUEST => strtoupper($method),
        CURLOPT_TIMEOUT => 180,
        CURLOPT_CONNECTTIMEOUT => 30,
        CURLOPT_HTTPHEADER => $headers,
        CURLOPT_HEADERFUNCTION => function ($curl, $line) use (&$headerText) { $headerText .= $line; return strlen($line); },
    ]);
    if ($payload !== null) curl_setopt($ch, CURLOPT_POSTFIELDS, core_json_encode($payload));
    $bodyText = curl_exec($ch);
    if ($bodyText === false) {
        $err = curl_error($ch); $no = curl_errno($ch); curl_close($ch);
        return ['ok' => false, 'status' => 0, 'headers' => $headerText, 'body' => null, 'raw' => '', 'error' => 'cURL #' . $no . ': ' . $err];
    }
    $status = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);
    $decoded = json_decode((string)$bodyText, true);
    $location = null;
    foreach (preg_split("/\r\n|\n|\r/", $headerText) as $line) if (stripos($line, 'Location:') === 0) $location = trim(substr($line, 9));
    return [
        'ok' => $status >= 200 && $status < 300,
        'status' => $status,
        'headers' => $headerText,
        'location' => $location,
        'internalId' => $location ? core_gid_tail($location) : null,
        'body' => is_array($decoded) ? $decoded : null,
        'raw' => (string)$bodyText,
        'error' => '',
    ];
}

function find_netsuite_item_for_refund_line(array $refundLine): string
{
    $lineItem = is_array($refundLine['line_item'] ?? null) ? $refundLine['line_item'] : [];
    $lineId = (string)($refundLine['line_item_id'] ?? ($lineItem['id'] ?? ''));
    $sku = (string)($lineItem['sku'] ?? '');
    $variantId = (string)($lineItem['variant_id'] ?? '');
    $productId = (string)($lineItem['product_id'] ?? '');

    if ($lineId !== '') {
        $row = core_db_fetch_row("SELECT oi.*, pvm.netsuite_item_id AS pvm_ns_item_id, sm.netsuite_item_id AS sm_ns_item_id
            FROM `orders_info` oi
            LEFT JOIN `product_variant_mappings` pvm ON pvm.shopify_variant_id = oi.shopify_variant_id OR pvm.shopify_sku = oi.line_item_sku
            LEFT JOIN `sku_mappings` sm ON sm.shopify_sku = oi.line_item_sku
            WHERE oi.shopify_line_item_id = " . core_sql($lineId) . " LIMIT 1");
        foreach (['pvm_ns_item_id','sm_ns_item_id'] as $col) {
            if (is_array($row) && !empty($row[$col]) && (string)$row[$col] !== '0') return (string)$row[$col];
        }
    }
    if ($sku !== '') {
        $row = core_db_fetch_row("SELECT netsuite_item_id FROM `sku_mappings` WHERE shopify_sku = " . core_sql($sku) . " AND netsuite_item_id IS NOT NULL AND netsuite_item_id <> '' AND netsuite_item_id <> '0' LIMIT 1");
        if (is_array($row) && !empty($row['netsuite_item_id'])) return (string)$row['netsuite_item_id'];
        $row = core_db_fetch_row("SELECT netsuite_item_id FROM `product_variant_mappings` WHERE shopify_sku = " . core_sql($sku) . " AND netsuite_item_id IS NOT NULL AND netsuite_item_id <> '' AND netsuite_item_id <> '0' LIMIT 1");
        if (is_array($row) && !empty($row['netsuite_item_id'])) return (string)$row['netsuite_item_id'];
    }
    if ($variantId !== '') {
        $row = core_db_fetch_row("SELECT netsuite_item_id FROM `product_variant_mappings` WHERE shopify_variant_id = " . core_sql($variantId) . " AND netsuite_item_id IS NOT NULL AND netsuite_item_id <> '' AND netsuite_item_id <> '0' LIMIT 1");
        if (is_array($row) && !empty($row['netsuite_item_id'])) return (string)$row['netsuite_item_id'];
    }
    if ($productId !== '') {
        $row = core_db_fetch_row("SELECT netsuite_item_id FROM `product_variant_mappings` WHERE shopify_product_id = " . core_sql($productId) . " AND netsuite_item_id IS NOT NULL AND netsuite_item_id <> '' AND netsuite_item_id <> '0' LIMIT 1");
        if (is_array($row) && !empty($row['netsuite_item_id'])) return (string)$row['netsuite_item_id'];
    }
    return '';
}

function analyze_single_refund(array $refund): array
{
    $itemsAmount = refund_items_amount($refund);
    $shippingAmount = refund_shipping_amount($refund);
    $moneyAmount = refund_money_amount($refund);
    $explained = round($itemsAmount + $shippingAmount, 4);
    $adjustment = round(max(0, $moneyAmount - $explained), 4);
    $hasItems = !empty($refund['refund_line_items']);
    $hasMoney = $moneyAmount > 0;
    $type = 'unknown';
    if ($hasItems && $hasMoney) $type = 'items_and_money';
    elseif ($hasItems) $type = 'items_only';
    elseif ($hasMoney) $type = 'money_only';
    return [
        'type' => $type,
        'items_refund_amount' => $itemsAmount,
        'shipping_refund_amount' => $shippingAmount,
        'money_refund_amount' => $moneyAmount,
        'adjustment_refund_amount' => $adjustment,
        'currency' => $refund['currency'] ?? core_array_get($refund, ['transactions',0,'currency']),
    ];
}

function build_credit_memo_payload_for_refund(array $orderRow, array $refund, array $analysis): array
{
    $nsSalesOrderId = (string)($orderRow['ns_o_id'] ?? '');
    if ($nsSalesOrderId === '' || $nsSalesOrderId === '0') throw new RuntimeException('Missing orders.ns_o_id.');

    $customerId = '';
    foreach (['ns_customer_id','netsuite_customer_id','customer_ns_id'] as $col) {
        if (!empty($orderRow[$col])) { $customerId = (string)$orderRow[$col]; break; }
    }
    if ($customerId === '' && defined('NS_DEFAULT_CUSTOMER_ID')) $customerId = (string)NS_DEFAULT_CUSTOMER_ID;
    if ($customerId === '') throw new RuntimeException('Missing NetSuite customer ID. Add ns_customer_id to orders or define NS_DEFAULT_CUSTOMER_ID.');

    $refundId = (string)($refund['id'] ?? '');
    $shopifyOrderId = (string)($orderRow['shopify_order_id'] ?? '');
    $lines = [];

    foreach (($refund['refund_line_items'] ?? []) as $refundLine) {
        if (!is_array($refundLine)) continue;
        $qty = (float)($refundLine['quantity'] ?? 0);
        if ($qty <= 0) continue;
        $nsItemId = find_netsuite_item_for_refund_line($refundLine);
        if ($nsItemId === '') {
            $sku = (string)core_array_get($refundLine, ['line_item','sku'], '');
            $lineId = (string)($refundLine['line_item_id'] ?? core_array_get($refundLine, ['line_item','id'], ''));
            throw new RuntimeException('No NetSuite item mapping for refund line. line_item_id=' . $lineId . ', sku=' . $sku);
        }
        $amount = refund_items_amount(['refund_line_items' => [$refundLine]]);
        $rate = $qty > 0 ? round($amount / $qty, 4) : $amount;
        $lines[] = ['item' => ['id' => $nsItemId], 'quantity' => $qty, 'rate' => $rate, 'amount' => $amount, 'description' => 'Shopify refund #' . $refundId . ' item'];
    }

    if ($analysis['shipping_refund_amount'] > 0) {
        if (!defined('NS_SHIPPING_ITEM_ID') || (int)NS_SHIPPING_ITEM_ID === 0) throw new RuntimeException('NS_SHIPPING_ITEM_ID is missing in config.php.');
        $amount = (float)$analysis['shipping_refund_amount'];
        $lines[] = ['item' => ['id' => (string)NS_SHIPPING_ITEM_ID], 'quantity' => 1, 'rate' => $amount, 'amount' => $amount, 'description' => 'Shopify refund #' . $refundId . ' shipping'];
    }

    $adjustment = (float)$analysis['adjustment_refund_amount'];
    if ($analysis['type'] === 'money_only') $adjustment = (float)$analysis['money_refund_amount'];
    if ($adjustment > 0) {
        if (!defined('NS_DISCOUNT_ITEM_ID') || (int)NS_DISCOUNT_ITEM_ID === 0) throw new RuntimeException('NS_DISCOUNT_ITEM_ID is missing in config.php.');
        $lines[] = ['item' => ['id' => (string)NS_DISCOUNT_ITEM_ID], 'quantity' => 1, 'rate' => $adjustment, 'amount' => $adjustment, 'description' => 'Shopify refund #' . $refundId . ' money adjustment'];
    }

    if (!$lines) throw new RuntimeException('Refund has no item/shipping/money lines to create Credit Memo.');

    return [
        'externalId' => 'SHOPIFY-REFUND-' . $refundId,
        'entity' => ['id' => $customerId],
        'createdFrom' => ['id' => $nsSalesOrderId],
        'memo' => 'Shopify refund #' . $refundId . ' for order ' . $shopifyOrderId . ' / type=' . $analysis['type'],
        'tranDate' => core_date($refund['created_at'] ?? null) ?: date('Y-m-d'),
        'item' => ['items' => $lines],
    ];
}

function refund_sync_success_exists(string $refundId): bool
{
    $row = core_db_fetch_row("SELECT id FROM `shopify_refund_netsuite_sync` WHERE `shopify_refund_id` = " . core_sql($refundId) . " AND `status` = 'success' LIMIT 1");
    return is_array($row);
}

function upsert_refund_sync_log(array $data): void
{
    $now = core_now();
    core_db_query("INSERT INTO `shopify_refund_netsuite_sync` (`shopify_order_id`,`shopify_refund_id`,`shopify_refund_gid`,`ns_sales_order_id`,`ns_credit_memo_id`,`ns_credit_memo_tranid`,`refund_type`,`items_refund_amount`,`shipping_refund_amount`,`money_refund_amount`,`adjustment_refund_amount`,`currency`,`status`,`error_message`,`shopify_raw_json`,`netsuite_payload_json`,`netsuite_response_json`,`created_at`,`updated_at`) VALUES (
        " . core_sql($data['shopify_order_id'] ?? '') . ", " . core_sql($data['shopify_refund_id'] ?? '') . ", " . core_sql($data['shopify_refund_gid'] ?? null) . ", " . core_sql($data['ns_sales_order_id'] ?? null) . ", " . core_sql($data['ns_credit_memo_id'] ?? null) . ", " . core_sql($data['ns_credit_memo_tranid'] ?? null) . ", " . core_sql($data['refund_type'] ?? 'unknown') . ", " . core_sql_num($data['items_refund_amount'] ?? 0) . ", " . core_sql_num($data['shipping_refund_amount'] ?? 0) . ", " . core_sql_num($data['money_refund_amount'] ?? 0) . ", " . core_sql_num($data['adjustment_refund_amount'] ?? 0) . ", " . core_sql($data['currency'] ?? null) . ", " . core_sql($data['status'] ?? 'pending') . ", " . core_sql($data['error_message'] ?? null) . ", " . core_sql($data['shopify_raw_json'] ?? null) . ", " . core_sql($data['netsuite_payload_json'] ?? null) . ", " . core_sql($data['netsuite_response_json'] ?? null) . ", " . core_sql($now) . ", " . core_sql($now) . ")
        ON DUPLICATE KEY UPDATE `ns_sales_order_id`=VALUES(`ns_sales_order_id`), `ns_credit_memo_id`=VALUES(`ns_credit_memo_id`), `ns_credit_memo_tranid`=VALUES(`ns_credit_memo_tranid`), `refund_type`=VALUES(`refund_type`), `items_refund_amount`=VALUES(`items_refund_amount`), `shipping_refund_amount`=VALUES(`shipping_refund_amount`), `money_refund_amount`=VALUES(`money_refund_amount`), `adjustment_refund_amount`=VALUES(`adjustment_refund_amount`), `currency`=VALUES(`currency`), `status`=VALUES(`status`), `error_message`=VALUES(`error_message`), `shopify_raw_json`=VALUES(`shopify_raw_json`), `netsuite_payload_json`=VALUES(`netsuite_payload_json`), `netsuite_response_json`=VALUES(`netsuite_response_json`), `updated_at`=VALUES(`updated_at`)");
}
