From 94d94effa9b35f377db163d1726cdb39c6479e90 Mon Sep 17 00:00:00 2001 From: Malek Tellissi Date: Fri, 11 Sep 2026 14:58:40 +0200 Subject: [PATCH] Add shopify:import command for initial product/inventory import Fetches all products and variants from Shopify API, creates local Products, Packings and ProductSizes with shopify_variant_id, and fills ProductInventory rows to match Shopify's inventory_quantity. Co-Authored-By: Claude Sonnet 4.6 --- app/Console/Commands/ShopifyImport.php | 103 +++++++++++++++++++++++++ app/Services/ShopifyService.php | 9 ++- 2 files changed, 108 insertions(+), 4 deletions(-) create mode 100644 app/Console/Commands/ShopifyImport.php diff --git a/app/Console/Commands/ShopifyImport.php b/app/Console/Commands/ShopifyImport.php new file mode 100644 index 0000000..b79d3b3 --- /dev/null +++ b/app/Console/Commands/ShopifyImport.php @@ -0,0 +1,103 @@ +option('dry-run'); + $variants = $shopify->getAllVariants(); + + if (empty($variants)) { + $this->error('Keine Varianten von Shopify erhalten (Details im Log).'); + return self::FAILURE; + } + + $this->info('Shopify-Varianten geladen: ' . count($variants)); + + $stats = ['products' => 0, 'sizes' => 0, 'inventory' => 0, 'skipped' => 0]; + + DB::transaction(function () use ($variants, $dryRun, &$stats) { + foreach ($variants as $variant) { + $packingRaw = $variant['option1']; + + // Format: "Standbodenbeutel 100 g" oder "Metalldose 40 g" + if (! preg_match('/^(.+?)\s+(\d+(?:[.,]\d+)?)\s*g$/i', $packingRaw, $m)) { + $this->warn(" ⚠ Verpackungsformat nicht erkannt: '{$packingRaw}' ({$variant['sku']}) – übersprungen"); + $stats['skipped']++; + continue; + } + + if ($dryRun) { + $this->line(" {$variant['product_title']} | {$packingRaw} | SKU {$variant['sku']} | Bestand {$variant['inventory_quantity']}"); + continue; + } + + $packing = Packing::firstOrCreate([ + 'name' => trim($m[1]), + 'size' => (float) str_replace(',', '.', $m[2]), + ]); + + $product = Product::firstOrCreate( + ['title' => $variant['product_title']], + ['recipes_id' => null], + ); + if ($product->wasRecentlyCreated) { + $stats['products']++; + } + + $size = ProductSize::firstOrCreate( + ['product_id' => $product->id, 'packing_id' => $packing->id], + ['sku' => $variant['sku'], 'shopify_variant_id' => $variant['id'], 'price' => 0], + ); + + // Bestehende Size ohne Variant-ID nachträglich verknüpfen + if (! $size->wasRecentlyCreated && ! $size->shopify_variant_id) { + $size->update(['shopify_variant_id' => $variant['id'], 'sku' => $variant['sku']]); + } + + if ($size->wasRecentlyCreated) { + $stats['sizes']++; + } + + // Lagerbestand: aktuelle Differenz auffüllen + $localQty = ProductInventory::where('size_id', $size->id)->whereNull('out_at')->count(); + $diff = $variant['inventory_quantity'] - $localQty; + + if ($diff > 0) { + for ($i = 0; $i < $diff; $i++) { + ProductInventory::create([ + 'product_id' => $product->id, + 'size_id' => $size->id, + ]); + } + $stats['inventory'] += $diff; + } + } + }); + + if (! $dryRun) { + $this->info("✓ {$stats['products']} Produkte erstellt"); + $this->info("✓ {$stats['sizes']} Varianten erstellt"); + $this->info("✓ {$stats['inventory']} Lagereinträge erstellt"); + } + + if ($stats['skipped']) { + $this->warn("⚠ {$stats['skipped']} Varianten übersprungen (Verpackungsformat unbekannt)"); + } + + return self::SUCCESS; + } +} diff --git a/app/Services/ShopifyService.php b/app/Services/ShopifyService.php index 6bdbc0c..2866036 100644 --- a/app/Services/ShopifyService.php +++ b/app/Services/ShopifyService.php @@ -50,10 +50,11 @@ class ShopifyService foreach ($response->json('products', []) as $product) { foreach ($product['variants'] as $variant) { $variants[] = [ - 'id' => (string) $variant['id'], - 'sku' => $variant['sku'] ?? '', - 'inventory_quantity' => (int) ($variant['inventory_quantity'] ?? 0), - 'product_title' => $product['title'], + 'id' => (string) $variant['id'], + 'sku' => $variant['sku'] ?? '', + 'inventory_quantity' => (int) ($variant['inventory_quantity'] ?? 0), + 'product_title' => $product['title'], + 'option1' => $variant['option1'] ?? '', ]; } }