From ba796f0a3f31cff9c08e043d0a7e8a5b2f66709d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathias=20Gla=CC=88ser?= Date: Sun, 6 Sep 2026 15:36:58 +0200 Subject: [PATCH] Recover pending Shopify orders by stable order ID --- ...-integration.shopify_delta_order_import.md | 4 +- .../import-integration/shopify-delta-run.php | 50 ++++++++++++++++++- modules/system/naurua-erp-cron.php | 3 +- 3 files changed, 52 insertions(+), 5 deletions(-) diff --git a/docs/modules/erp/import-integration/processes/main/erp.import-integration.shopify_delta_order_import.md b/docs/modules/erp/import-integration/processes/main/erp.import-integration.shopify_delta_order_import.md index b58ff0b..0c2a10f 100644 --- a/docs/modules/erp/import-integration/processes/main/erp.import-integration.shopify_delta_order_import.md +++ b/docs/modules/erp/import-integration/processes/main/erp.import-integration.shopify_delta_order_import.md @@ -20,7 +20,7 @@ Es werden keine vorbereiteten Shopify-Geschäftsdaten an nachgelagerte Module ü ## 4. Exakter Prozessablauf -1. `cutoff_timestamp` und `dry_run` validieren; beim Cron den kleineren Wert aus Wasserstand und ältestem offenen Webhook-Eingang verwenden. +1. `cutoff_timestamp` und `dry_run` validieren; beim Cron den kleineren Wert aus Wasserstand und ältestem offenen Webhook-Eingang verwenden und offene Webhook-Order-GIDs zusätzlich direkt zur Recovery aufnehmen. 2. Read-only Shopify-Bestellungen mit `createdAt` nach dem Stichtag in stabiler Reihenfolge laden. 3. Stornierte oder vollständig erstattete Bestellungen als nicht lagerwirksam klassifizieren und nur technisch protokollieren. 4. Für jede qualifizierte Bestellung die Shopify Order-GID idempotent gegen `sales_order.shopify_order_gid` prüfen. @@ -43,7 +43,7 @@ Write targets: ## 5. Batch, Betriebsmodell und Einbettung -Der Initiallauf verarbeitet den festen Zeitraum nach dem bestehenden ERP-Stichtag seriell. Spätere tägliche Reconciliation-Läufe nutzen den persistierten Wasserstand und ziehen offene oder fehlgeschlagene Webhook-Eingänge wieder in den Prüfbereich. Der Wasserstand wird nur nach einem vollständig erfolgreichen Lauf fortgeschrieben. Keine Parallelisierung. +Der Initiallauf verarbeitet den festen Zeitraum nach dem bestehenden ERP-Stichtag seriell. Spätere tägliche Reconciliation-Läufe nutzen den persistierten Wasserstand und ziehen offene oder fehlgeschlagene Webhook-Eingänge sowohl über den Prüfbereich als auch über ihre stabile Order-GID wieder in die Verarbeitung. Ein technisches Event wird erst nach erfolgreicher Projektion als verarbeitet markiert. Der Wasserstand wird nur nach einem vollständig erfolgreichen Lauf fortgeschrieben. Keine Parallelisierung. ## 6. Output diff --git a/modules/erp/import-integration/shopify-delta-run.php b/modules/erp/import-integration/shopify-delta-run.php index fde3ec8..9746d6e 100644 --- a/modules/erp/import-integration/shopify-delta-run.php +++ b/modules/erp/import-integration/shopify-delta-run.php @@ -62,6 +62,32 @@ function shopify_delta_reconciliation_cutoff(PDO $pdo): string return ($pendingDate < $stateDate ? $pendingDate : $stateDate)->format(DATE_ATOM); } +function shopify_delta_load_pending_order_gids(PDO $pdo): array +{ + $stmt = $pdo->query( + "SELECT DISTINCT shopify_order_gid + FROM shopify_webhook_event + WHERE status IN ('received', 'processing', 'failed') + AND shopify_order_gid IS NOT NULL + ORDER BY shopify_order_gid" + ); + return array_values(array_filter(array_map( + static fn (mixed $value): string => trim((string) $value), + $stmt->fetchAll(PDO::FETCH_COLUMN) + ), static fn (string $value): bool => $value !== '')); +} + +function shopify_delta_mark_order_events_processed(PDO $pdo, string $orderGid): void +{ + $stmt = $pdo->prepare( + "UPDATE shopify_webhook_event + SET status = 'processed', processed_at = NOW(), error_code = NULL, error_message = NULL + WHERE shopify_order_gid = :shopify_order_gid + AND status IN ('received', 'processing', 'failed')" + ); + $stmt->execute([':shopify_order_gid' => $orderGid]); +} + function shopify_delta_store_watermark(PDO $pdo, string $cutoff): void { $stmt = $pdo->prepare( @@ -120,7 +146,7 @@ GQL; return $orders; } -function run_shopify_delta(array $env, PDO $pdo, string $cutoff, bool $dryRun): array +function run_shopify_delta(array $env, PDO $pdo, string $cutoff, bool $dryRun, array $recoveryOrderGids = []): array { $cutoffDate = new DateTimeImmutable($cutoff); $searchQuery = 'created_at:>' . $cutoffDate->format('Y-m-d'); @@ -135,14 +161,31 @@ function run_shopify_delta(array $env, PDO $pdo, string $cutoff, bool $dryRun): 'clarification_order_gids' => [], ]; + $ordersByGid = []; foreach (shopify_delta_load_orders($env, $searchQuery) as $summaryOrder) { + $orderGid = trim((string) ($summaryOrder['id'] ?? '')); + if ($orderGid !== '') { + $ordersByGid[$orderGid] = $summaryOrder; + } + } + foreach ($recoveryOrderGids as $recoveryOrderGid) { + if (!isset($ordersByGid[$recoveryOrderGid])) { + $ordersByGid[$recoveryOrderGid] = shopify_load_order_by_gid($env, $recoveryOrderGid); + } + } + + $recoverySet = array_fill_keys($recoveryOrderGids, true); + foreach ($ordersByGid as $orderGid => $summaryOrder) { $createdAt = new DateTimeImmutable((string) ($summaryOrder['createdAt'] ?? '')); - if ($createdAt <= $cutoffDate) { + if (!isset($recoverySet[$orderGid]) && $createdAt <= $cutoffDate) { continue; } $counters['checked_orders']++; if (($summaryOrder['cancelledAt'] ?? null) !== null || strtoupper((string) ($summaryOrder['displayFinancialStatus'] ?? '')) === 'REFUNDED') { $counters['skipped_cancelled_or_refunded']++; + if (!$dryRun) { + shopify_delta_mark_order_events_processed($pdo, (string) $summaryOrder['id']); + } continue; } @@ -168,6 +211,9 @@ function run_shopify_delta(array $env, PDO $pdo, string $cutoff, bool $dryRun): $counters['already_imported']++; } else { $counters['handed_off']++; + if (!$dryRun) { + shopify_delta_mark_order_events_processed($pdo, (string) $summaryOrder['id']); + } } } catch (Throwable $error) { $counters['status'] = 'partial_success'; diff --git a/modules/system/naurua-erp-cron.php b/modules/system/naurua-erp-cron.php index ca46fbd..3dd2235 100644 --- a/modules/system/naurua-erp-cron.php +++ b/modules/system/naurua-erp-cron.php @@ -26,9 +26,10 @@ try { 'erp.import-integration.shopify_webhook_worker' => static fn (): array => shopify_webhook_worker_run($pdo, $env, 100), 'erp.import-integration.shopify_delta_order_import' => static function () use ($pdo, $env, $reconciliationStartedAt): array { $cutoff = shopify_delta_reconciliation_cutoff($pdo); + $recoveryOrderGids = shopify_delta_load_pending_order_gids($pdo); $runId = shopify_delta_start_run($pdo, $cutoff, false); try { - $result = run_shopify_delta($env, $pdo, $cutoff, false); + $result = run_shopify_delta($env, $pdo, $cutoff, false, $recoveryOrderGids); $result['technical_run_id'] = $runId; if (($result['status'] ?? '') === 'done') { shopify_delta_store_watermark($pdo, $reconciliationStartedAt->format(DATE_ATOM));