*/ public function syncProduct(Product $product, array $options = []): array { $dryRun = (bool) ($options['dry_run'] ?? false); $item = $this->resolveItemPayload($product, $options); if (! is_array($item) || $item === []) { throw new RuntimeException('Nessun item Amazon trovato per il prodotto selezionato.'); } $asin = $this->extractAsin($item); if ($asin === null) { throw new RuntimeException('La risposta Amazon non contiene un ASIN utilizzabile.'); } $title = $this->extractTitle($item) ?: (string) ($product->name ?? 'Prodotto Amazon'); $marketplace = trim((string) ($options['marketplace'] ?? $this->apiService->getMarketplace())) ?: $this->apiService->getMarketplace(); $associateTag = trim((string) ($options['associate_tag'] ?? $this->apiService->getAssociateTag() ?? '')) ?: null; $detailUrl = $this->extractDetailPageUrl($item); $referralUrl = $detailUrl !== null ? $this->appendAssociateTag($detailUrl, $associateTag) : $this->productOfferService->buildAmazonReferralUrl($product, $associateTag, $this->mapMarketplaceToLocale($marketplace)); $price = $this->extractPriceAmount($item); $compareAt = $this->extractCompareAtAmount($item); $currency = $this->extractCurrency($item) ?? 'EUR'; $availability = $this->extractAvailability($item) ?? 'catalog_synced'; $externalId = $asin; $ean = $this->extractEan($item); $imageUrl = $this->extractPrimaryImageUrl($item); $payloadSummary = [ 'asin' => $asin, 'title' => $title, 'marketplace' => $marketplace, 'detail_url' => $detailUrl, 'image_url' => $imageUrl, 'price_amount' => $price, 'currency' => $currency, ]; $identifiersCreated = 0; $mediaCreated = 0; $offer = null; if (! $dryRun) { $identifiersCreated += $this->upsertIdentifier($product, 'asin', $asin, 'amazon_creators_api', true) ? 1 : 0; if ($ean !== null) { $identifiersCreated += $this->upsertIdentifier($product, 'ean', $ean, 'amazon_creators_api', false) ? 1 : 0; } if ($imageUrl !== null) { $mediaCreated += $this->upsertRemoteMedia($product, $imageUrl, $title) ? 1 : 0; } $offer = $this->productOfferService->syncOffer($product, [ 'source_type' => 'amazon_creators_api', 'source_name' => 'Amazon Creators API', 'external_product_id' => $externalId, 'title' => $title, 'currency' => $currency, 'price_amount' => $price, 'price_compare_at' => $compareAt, 'availability' => $availability, 'external_url' => $detailUrl, 'referral_url' => $referralUrl, 'referral_code' => $associateTag, 'is_internal' => false, 'is_active' => true, 'meta' => [ 'marketplace' => $marketplace, 'sync_source' => (string) ($options['source'] ?? 'api'), 'payload' => $payloadSummary, ], ]); } return [ 'asin' => $asin, 'title' => $title, 'detail_url' => $detailUrl, 'referral_url' => $referralUrl, 'image_url' => $imageUrl, 'price_amount' => $price, 'price_compare_at' => $compareAt, 'currency' => $currency, 'availability' => $availability, 'ean' => $ean, 'identifiers_created' => $identifiersCreated, 'media_created' => $mediaCreated, 'offer_id' => $offer instanceof ProductOffer ? (int) $offer->id : null, 'source' => (string) ($options['source'] ?? 'api'), 'dry_run' => $dryRun, ]; } /** @return array */ private function resolveItemPayload(Product $product, array $options): array { if (is_array($options['item'] ?? null)) { return $options['item']; } if (is_array($options['response'] ?? null)) { $candidate = $this->pickFirstItem((array) $options['response']); return is_array($candidate) ? $candidate : []; } $asin = trim((string) ($options['asin'] ?? $this->findIdentifierValue($product, ['asin']) ?? '')); if ($asin !== '') { $response = $this->apiService->getItems([$asin], [ 'marketplace' => (string) ($options['marketplace'] ?? $this->apiService->getMarketplace()), ]); return $this->pickFirstItem($response) ?? []; } $query = trim((string) ($options['query'] ?? $this->findIdentifierValue($product, ['ean']) ?? $this->buildSearchQuery($product))); if ($query === '') { return []; } $response = $this->apiService->searchItems($query, [ 'marketplace' => (string) ($options['marketplace'] ?? $this->apiService->getMarketplace()), ]); return $this->pickFirstItem($response) ?? []; } /** @return array|null */ private function pickFirstItem(array $payload): ?array { foreach ([ 'items', 'ItemsResult.Items', 'SearchResult.Items', 'GetItemsResult.Items', 'data.items', 'data.searchItems.items', 'data.getItems.items', ] as $path) { $items = data_get($payload, $path); if (is_array($items) && isset($items[0]) && is_array($items[0])) { return $items[0]; } } return isset($payload['asin']) || isset($payload['ASIN']) ? $payload : null; } private function extractAsin(array $item): ?string { foreach (['asin', 'ASIN', 'itemId', 'ItemId'] as $path) { $value = trim((string) data_get($item, $path, '')); if ($value !== '') { return $value; } } return null; } private function extractEan(array $item): ?string { foreach ([ 'ean', 'EAN', 'ExternalIds.EANs.DisplayValues.0', 'externalIds.eans.0', 'identifiers.ean', ] as $path) { $value = preg_replace('/\s+/', '', trim((string) data_get($item, $path, ''))); if (is_string($value) && $value !== '') { return $value; } } return null; } private function extractTitle(array $item): ?string { foreach (['title', 'Title', 'ItemInfo.Title.DisplayValue', 'itemInfo.title.displayValue'] as $path) { $value = trim((string) data_get($item, $path, '')); if ($value !== '') { return $value; } } return null; } private function extractDetailPageUrl(array $item): ?string { foreach (['detailPageURL', 'DetailPageURL', 'detailPageUrl', 'url'] as $path) { $value = trim((string) data_get($item, $path, '')); if ($value !== '') { return $value; } } return null; } private function extractPrimaryImageUrl(array $item): ?string { foreach ([ 'Images.Primary.Large.URL', 'Images.Primary.Medium.URL', 'Images.Primary.Small.URL', 'images.primary.large.url', 'images.primary.medium.url', 'images.0.url', 'image', ] as $path) { $value = trim((string) data_get($item, $path, '')); if ($value !== '') { return $value; } } return null; } private function extractPriceAmount(array $item): ?float { foreach ([ 'Offers.Listings.0.Price.Amount', 'Offers.Summaries.0.LowestPrice.Amount', 'offers.listings.0.price.amount', 'offers.summaries.0.lowestPrice.amount', 'price.amount', ] as $path) { $value = data_get($item, $path); if (is_numeric($value)) { return round((float) $value, 2); } } return null; } private function extractCompareAtAmount(array $item): ?float { foreach ([ 'Offers.Listings.0.SavingBasis.Amount', 'offers.listings.0.savingBasis.amount', 'price.compareAt.amount', ] as $path) { $value = data_get($item, $path); if (is_numeric($value)) { return round((float) $value, 2); } } return null; } private function extractCurrency(array $item): ?string { foreach ([ 'Offers.Listings.0.Price.Currency', 'Offers.Summaries.0.LowestPrice.Currency', 'offers.listings.0.price.currency', 'offers.summaries.0.lowestPrice.currency', 'price.currency', ] as $path) { $value = trim((string) data_get($item, $path, '')); if ($value !== '') { return strtoupper($value); } } return null; } private function extractAvailability(array $item): ?string { foreach ([ 'Offers.Listings.0.Availability.Message', 'offers.listings.0.availability.message', 'availability', ] as $path) { $value = trim((string) data_get($item, $path, '')); if ($value !== '') { return $value; } } return null; } private function appendAssociateTag(string $url, ?string $associateTag): string { $associateTag = $associateTag !== null ? trim($associateTag) : ''; if ($associateTag === '') { return $url; } $separator = str_contains($url, '?') ? '&' : '?'; return str_contains($url, 'tag=') ? $url : ($url . $separator . 'tag=' . rawurlencode($associateTag)); } private function mapMarketplaceToLocale(string $marketplace): string { return match (Str::lower(trim($marketplace))) { 'www.amazon.de' => 'de', 'www.amazon.fr' => 'fr', 'www.amazon.es' => 'es', 'www.amazon.co.uk' => 'uk', default => 'it', }; } /** @param array $types */ private function findIdentifierValue(Product $product, array $types): ?string { $value = ProductIdentifier::query() ->where('product_id', (int) $product->id) ->whereIn('code_type', $types) ->orderByDesc('is_primary') ->orderBy('id') ->value('code_value'); $value = trim((string) $value); return $value !== '' ? $value : null; } private function buildSearchQuery(Product $product): string { $parts = array_values(array_unique(array_filter([ trim((string) ($product->brand ?? '')), trim((string) ($product->model ?? '')), trim((string) ($product->name ?? '')), ], static fn(string $value): bool => $value !== ''))); return Str::limit(implode(' ', $parts), 120, ''); } private function upsertIdentifier(Product $product, string $type, string $value, string $source, bool $isPrimary): bool { $value = trim($value); if ($value === '') { return false; } $normalized = strtoupper(preg_replace('/[^A-Z0-9]+/', '', $value) ?? ''); if ($normalized === '') { return false; } $identifier = ProductIdentifier::query()->firstOrNew([ 'product_id' => (int) $product->id, 'code_type' => $type, 'normalized_code' => $normalized, ]); $created = ! $identifier->exists; $identifier->code_role = $type; $identifier->code_value = $value; $identifier->source = $source; $identifier->source_reference = $value; $identifier->is_primary = $isPrimary; $identifier->payload = ['provider' => 'amazon']; $identifier->save(); return $created; } private function upsertRemoteMedia(Product $product, string $url, string $title): bool { $url = trim($url); if ($url === '' || ! str_starts_with($url, 'http')) { return false; } $media = ProductMedia::query()->firstOrNew([ 'product_id' => (int) $product->id, 'media_type' => 'image', 'disk' => 'remote-url', 'path' => $url, ]); $created = ! $media->exists; $media->title = $title; $media->meta = [ 'provider' => 'amazon_creators_api', 'remote' => true, ]; $media->save(); return $created; } }