baseUrl = "https://{$domain}/admin/api/{$version}"; $this->token = config('services.shopify.access_token'); } /** * Returns all product variants from Shopify. * Each entry: ['id', 'sku', 'inventory_quantity', 'product_title'] */ public function getAllVariants(): array { $variants = []; $url = "{$this->baseUrl}/products.json?limit=250&fields=id,title,body_html,variants,images"; while ($url) { $response = Http::withHeader('X-Shopify-Access-Token', $this->token)->get($url); if ($response->failed()) { $status = $response->status(); $hint = match (true) { $status === 401 => 'Access Token ungültig oder fehlt', $status === 403 => 'IP-Whitelist: VPS-IP ist in Shopify nicht freigeschalten', $status === 429 => 'Rate Limit überschritten', $status >= 500 => 'Shopify-seitiger Fehler', default => 'Unbekannter Fehler', }; Log::error("Shopify Sync fehlgeschlagen: {$hint}", [ 'status' => $status, 'url' => $url, 'body' => $response->body(), ]); break; } foreach ($response->json('products', []) as $product) { $imageUrls = collect($product['images'] ?? [])->pluck('src')->all(); foreach ($product['variants'] as $variant) { $variants[] = [ 'id' => (string) $variant['id'], 'sku' => $variant['sku'] ?? '', 'price' => (float) ($variant['price'] ?? 0), 'inventory_quantity' => (int) ($variant['inventory_quantity'] ?? 0), 'product_title' => $product['title'], 'shopify_product_id' => (string) $product['id'], 'product_description' => strip_tags($product['body_html'] ?? ''), 'option1' => $variant['option1'] ?? '', 'image_urls' => $imageUrls, ]; } } // Follow Shopify's Link header pagination $url = $this->nextPageUrl($response->header('Link')); if ($url) { usleep(500_000); // 0.5s — stay well within 2 req/s limit } } return $variants; } public function getProduct(string $shopifyProductId): array { $response = Http::withHeader('X-Shopify-Access-Token', $this->token) ->get("{$this->baseUrl}/products/{$shopifyProductId}.json"); if ($response->failed()) { throw new \RuntimeException( 'Shopify-Fehler ' . $response->status() . ': ' . $response->body() ); } $p = $response->json('product'); return [ 'description' => strip_tags($p['body_html'] ?? ''), 'variants' => collect($p['variants'] ?? [])->map(fn ($v) => [ 'shopify_variant_id' => (string) $v['id'], 'price' => (float) ($v['price'] ?? 0), 'sku' => $v['sku'] ?? null, 'gtin' => $v['barcode'] ?? null, ])->all(), ]; } public function createProduct(Product $product, \Illuminate\Support\Collection $sizes): array { $variants = $sizes->map(fn (ProductSize $s) => array_filter([ 'option1' => $s->packing->name . ' ' . $s->packing->size . 'g', 'price' => number_format((float) $s->price, 2, '.', ''), 'sku' => $s->sku ?: null, 'barcode' => $s->gtin ?: null, ], fn ($v) => $v !== null))->values()->toArray(); $body = [ 'product' => [ 'title' => $product->title, 'body_html' => $product->description ? '
' . nl2br(e($product->description)) . '
' : '', 'options' => [['name' => 'Größe']], ], ]; if (! empty($variants)) { $body['product']['variants'] = $variants; } Log::info('Shopify createProduct request', [ 'product_id' => $product->id, 'sizes' => $sizes->count(), 'body' => $body, ]); $response = Http::withHeader('X-Shopify-Access-Token', $this->token) ->post("{$this->baseUrl}/products.json", $body); Log::info('Shopify createProduct response', [ 'status' => $response->status(), 'body' => $response->json(), ]); if ($response->failed()) { throw new \RuntimeException( 'Shopify-Fehler ' . $response->status() . ': ' . $response->body() ); } $shopifyProduct = $response->json('product'); $shopifyVariants = $shopifyProduct['variants']; // Match returned variants back to local sizes by index (same order as sent) $variantMap = []; foreach ($sizes as $index => $size) { if (isset($shopifyVariants[$index])) { $variantMap[$size->packing_id] = (string) $shopifyVariants[$index]['id']; } } return [ 'shopify_product_id' => (string) $shopifyProduct['id'], 'variants' => $variantMap, ]; } public function createVariant(string $shopifyProductId, ProductSize $size): string { $packing = $size->packing; $option1 = $packing->name . ' ' . $packing->size . 'g'; $response = Http::withHeader('X-Shopify-Access-Token', $this->token) ->post("{$this->baseUrl}/products/{$shopifyProductId}/variants.json", [ 'variant' => array_filter([ 'option1' => $option1, 'price' => number_format((float) $size->price, 2, '.', ''), 'sku' => $size->sku ?: null, 'barcode' => $size->gtin ?: null, ]), ]); if ($response->failed()) { throw new \RuntimeException( 'Shopify-Fehler ' . $response->status() . ': ' . $response->body() ); } return (string) $response->json('variant.id'); } public function updateVariant(ProductSize $size): void { $response = Http::withHeader('X-Shopify-Access-Token', $this->token) ->put("{$this->baseUrl}/variants/{$size->shopify_variant_id}.json", [ 'variant' => array_filter([ 'id' => $size->shopify_variant_id, 'price' => number_format((float) $size->price, 2, '.', ''), 'sku' => $size->sku ?: null, 'barcode' => $size->gtin ?: null, ], fn ($v) => $v !== null), ]); if ($response->failed()) { throw new \RuntimeException( 'Shopify-Fehler ' . $response->status() . ': ' . $response->body() ); } } private function nextPageUrl(?string $linkHeader): ?string { if (!$linkHeader) { return null; } // Link: