diff --git a/docs/usage/information_provider_system.md b/docs/usage/information_provider_system.md index da8ea32b..13df7f10 100644 --- a/docs/usage/information_provider_system.md +++ b/docs/usage/information_provider_system.md @@ -278,16 +278,6 @@ The following env configuration options are available: * `PROVIDER_BUERKLIN_CURRENCY`: The currency you want to get prices in if available (optional, 3 letter ISO-code, default: `EUR`). * `PROVIDER_BUERKLIN_LANGUAGE`: The language you want to get the descriptions in. Possible values: `de` = German, `en` = English. (optional, default: `en`) -### Conrad - -The conrad provider the [Conrad API](https://developer.conrad.com/) to search for parts and retried their information. -To use it you have to request access to the API, however it seems currently your mail address needs to be allowlisted before you can register for an account. -The conrad webpages uses the API key in the requests, so you might be able to extract a working API key by listening to browser requests. -That method is not officially supported nor encouraged by Part-DB, and might break at any moment. - -The following env configuration options are available: -* `PROVIDER_CONRAD_API_KEY`: The API key you got from Conrad (mandatory) - ### Custom provider To create a custom provider, you have to create a new class implementing the `InfoProviderInterface` interface. As long diff --git a/src/Services/InfoProviderSystem/Providers/ConradProvider.php b/src/Services/InfoProviderSystem/Providers/ConradProvider.php index 044eb7a7..8c343099 100644 --- a/src/Services/InfoProviderSystem/Providers/ConradProvider.php +++ b/src/Services/InfoProviderSystem/Providers/ConradProvider.php @@ -23,11 +23,8 @@ declare(strict_types=1); namespace App\Services\InfoProviderSystem\Providers; -use App\Services\InfoProviderSystem\DTOs\FileDTO; use App\Services\InfoProviderSystem\DTOs\ParameterDTO; use App\Services\InfoProviderSystem\DTOs\PartDetailDTO; -use App\Services\InfoProviderSystem\DTOs\PriceDTO; -use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO; use App\Services\InfoProviderSystem\DTOs\SearchResultDTO; use App\Settings\InfoProviderSystem\ConradSettings; use Symfony\Contracts\HttpClient\HttpClientInterface; @@ -36,18 +33,9 @@ readonly class ConradProvider implements InfoProviderInterface { private const SEARCH_ENDPOINT = '/search/1/v3/facetSearch'; - public const DISTRIBUTOR_NAME = 'Conrad'; - private HttpClientInterface $httpClient; - - public function __construct( HttpClientInterface $httpClient, private ConradSettings $settings) + public function __construct(private HttpClientInterface $httpClient, private ConradSettings $settings) { - //We want everything in JSON - $this->httpClient = $httpClient->withOptions([ - 'headers' => [ - 'Accept' => 'application/json', - ], - ]); } public function getProviderInfo(): array @@ -87,6 +75,31 @@ readonly class ConradProvider implements InfoProviderInterface return null; } + private function getFootprintFromTechnicalAttributes(array $technicalDetails): ?string + { + foreach ($technicalDetails as $detail) { + if ($detail['attributeID'] === 'ATT.LOV.HOUSING_SEMICONDUCTORS') { + return $detail['values'][0]['value'] ?? null; + } + } + + return null; + } + + private function technicalAttributesToParameters(array $technicalAttributes): array + { + $parameters = []; + foreach ($technicalAttributes as $attribute) { + if ($attribute['multiValue'] ?? false === true) { + throw new \LogicException('Multi value attributes are not supported yet'); + } + $parameters[] = ParameterDTO::parseValueField($attribute['attributeName'], + $attribute['values'][0]['value'], $attribute['values'][0]['unit']['name'] ?? null); + } + + return $parameters; + } + public function searchByKeyword(string $keyword): array { $url = $this->settings->shopID->getAPIRoot() . self::SEARCH_ENDPOINT . '/' @@ -99,8 +112,7 @@ readonly class ConradProvider implements InfoProviderInterface ], 'json' => [ 'query' => $keyword, - 'size' => 50, - 'sort' => [["field"=>"_score","order"=>"desc"]], + 'size' => 25, ], ]); @@ -112,8 +124,8 @@ readonly class ConradProvider implements InfoProviderInterface $out[] = new SearchResultDTO( provider_key: $this->getProviderKey(), provider_id: $result['productId'], - name: $result['manufacturerId'] ?? $result['productId'], - description: $result['title'] ?? '', + name: $result['title'], + description: '', manufacturer: $result['brand']['name'] ?? null, mpn: $result['manufacturerId'] ?? null, preview_image_url: $result['image'] ?? null, @@ -125,158 +137,6 @@ readonly class ConradProvider implements InfoProviderInterface return $out; } - private function getFootprintFromTechnicalAttributes(array $technicalDetails): ?string - { - foreach ($technicalDetails as $detail) { - if ($detail['attributeID'] === 'ATT.LOV.HOUSING_SEMICONDUCTORS') { - return $detail['values'][0]['value'] ?? null; - } - } - - return null; - } - - /** - * @param array $technicalAttributes - * @return array - */ - private function technicalAttributesToParameters(array $technicalAttributes): array - { - return array_map(static function (array $p) { - if (count($p['values']) === 1) { //Single value attribute - if (array_key_exists('unit', $p['values'][0])) { - return ParameterDTO::parseValueField( //With unit - name: $p['attributeName'], - value: $p['values'][0]['value'], - unit: $p['values'][0]['unit']['name'], - ); - } - - return ParameterDTO::parseValueIncludingUnit( - name: $p['attributeName'], - value: $p['values'][0]['value'], - ); - } - - if (count($p['values']) === 2) { //Multi value attribute (e.g. min/max) - $value = $p['values'][0]['value'] ?? null; - $value2 = $p['values'][1]['value'] ?? null; - $unit = $p['values'][0]['unit']['name'] ?? ''; - $unit2 = $p['values'][1]['unit']['name'] ?? ''; - if ($unit === $unit2 && is_numeric($value) && is_numeric($value2)) { - if (array_key_exists('unit', $p['values'][0])) { //With unit - return new ParameterDTO( - name: $p['attributeName'], - value_min: (float)$value, - value_max: (float)$value2, - unit: $unit, - ); - } - - return new ParameterDTO( - name: $p['attributeName'], - value_min: (float)$value, - value_max: (float)$value2, - ); - } - } - - // fallback implementation - $values = implode(", ", array_map(fn($q) => - array_key_exists('unit', $q) ? $q['value']." ". ($q['unit']['name'] ?? $q['unit']) : $q['value'] - , $p['values'])); - return ParameterDTO::parseValueIncludingUnit( - name: $p['attributeName'], - value: $values, - ); - }, $technicalAttributes); - } - - /** - * @param array $productMedia - * @return array - */ - public function productMediaToDatasheets(array $productMedia): array - { - $files = []; - foreach ($productMedia['manuals'] as $manual) { - //Filter out unwanted languages - if (!empty($this->settings->attachmentLanguageFilter) && !in_array($manual['language'], $this->settings->attachmentLanguageFilter, true)) { - continue; - } - - $files[] = new FileDTO($manual['fullUrl'], $manual['title'] . ' (' . $manual['language'] . ')'); - } - - return $files; - } - - - /** - * Queries prices for a given product ID. It makes a POST request to the Conrad API - * @param string $productId - * @return PurchaseInfoDTO - */ - private function queryPrices(string $productId): PurchaseInfoDTO - { - $priceQueryURL = $this->settings->shopID->getAPIRoot() . '/price-availability/4/' - . $this->settings->shopID->getShopID() . '/facade'; - - $response = $this->httpClient->request('POST', $priceQueryURL, [ - 'query' => [ - 'apikey' => $this->settings->apiKey, - 'overrideCalculationSchema' => $this->settings->includeVAT ? 'GROSS' : 'NET' - ], - 'json' => [ - 'ns:inputArticleItemList' => [ - "#namespaces" => [ - "ns" => "http://www.conrad.de/ccp/basit/service/article/priceandavailabilityservice/api" - ], - 'articles' => [ - [ - "articleID" => $productId, - "calculatePrice" => true, - "checkAvailability" => true, - ], - ] - ] - ] - ]); - - $result = $response->toArray(); - - $priceInfo = $result['priceAndAvailabilityFacadeResponse']['priceAndAvailability']['price'] ?? []; - $price = $priceInfo['price'] ?? "0.0"; - $currency = $priceInfo['currency'] ?? "EUR"; - $includesVat = $priceInfo['isGrossAmount'] === "true" ?? true; - $minOrderAmount = $result['priceAndAvailabilityFacadeResponse']['priceAndAvailability']['availabilityStatus']['minimumOrderQuantity'] ?? 1; - - $prices = []; - foreach ($priceInfo['priceScale'] ?? [] as $priceScale) { - $prices[] = new PriceDTO( - minimum_discount_amount: max($priceScale['scaleFrom'], $minOrderAmount), - price: (string)$priceScale['pricePerUnit'], - currency_iso_code: $currency, - includes_tax: $includesVat - ); - } - if (empty($prices)) { //Fallback if no price scales are defined - $prices[] = new PriceDTO( - minimum_discount_amount: $minOrderAmount, - price: (string)$price, - currency_iso_code: $currency, - includes_tax: $includesVat - ); - } - - return new PurchaseInfoDTO( - distributor_name: self::DISTRIBUTOR_NAME, - order_number: $productId, - prices: $prices, - product_url: $this->getProductUrl($productId) - ); - } - public function getDetails(string $id): PartDetailDTO { $productInfoURL = $this->settings->shopID->getAPIRoot() . '/product/1/service/' . $this->settings->shopID->getShopID() @@ -293,28 +153,22 @@ readonly class ConradProvider implements InfoProviderInterface return new PartDetailDTO( provider_key: $this->getProviderKey(), provider_id: $data['shortProductNumber'], - name: $data['productFullInformation']['manufacturer']['name'] ?? $data['productFullInformation']['manufacturer']['id'] ?? $data['shortProductNumber'], - description: $data['productShortInformation']['title'] ?? '', - category: $data['productShortInformation']['articleGroupName'] ?? null, - manufacturer: $data['brand']['displayName'] !== null ? preg_replace("/[\u{2122}\u{00ae}]/", "", $data['brand']['displayName']) : null, //Replace ™ and ® symbols + name: $data['productShortInformation']['title'], + description: $data['productShortInformation']['shortDescription'] ?? '', + manufacturer: $data['brand']['displayName'] ?? null, mpn: $data['productFullInformation']['manufacturer']['id'] ?? null, preview_image_url: $data['productShortInformation']['mainImage']['imageUrl'] ?? null, provider_url: $this->getProductUrl($data['shortProductNumber']), footprint: $this->getFootprintFromTechnicalAttributes($data['productFullInformation']['technicalAttributes'] ?? []), notes: $data['productFullInformation']['description'] ?? null, - datasheets: $this->productMediaToDatasheets($data['productMedia'] ?? []), parameters: $this->technicalAttributesToParameters($data['productFullInformation']['technicalAttributes'] ?? []), - vendor_infos: [$this->queryPrices($data['shortProductNumber'])] ); } public function getCapabilities(): array { - return [ - ProviderCapabilities::BASIC, + return [ProviderCapabilities::BASIC, ProviderCapabilities::PICTURE, - ProviderCapabilities::DATASHEET, - ProviderCapabilities::PRICE, - ]; + ProviderCapabilities::PRICE,]; } } diff --git a/src/Settings/InfoProviderSystem/ConradSettings.php b/src/Settings/InfoProviderSystem/ConradSettings.php index dda884c8..999ebfe0 100644 --- a/src/Settings/InfoProviderSystem/ConradSettings.php +++ b/src/Settings/InfoProviderSystem/ConradSettings.php @@ -26,8 +26,6 @@ namespace App\Settings\InfoProviderSystem; use App\Form\Type\APIKeyType; use App\Settings\SettingsIcon; use Jbtronics\SettingsBundle\Metadata\EnvVarMode; -use Jbtronics\SettingsBundle\ParameterTypes\ArrayType; -use Jbtronics\SettingsBundle\ParameterTypes\StringType; use Jbtronics\SettingsBundle\Settings\Settings; use Jbtronics\SettingsBundle\Settings\SettingsParameter; use Jbtronics\SettingsBundle\Settings\SettingsTrait; @@ -50,28 +48,10 @@ class ConradSettings public ?string $apiKey = null; #[SettingsParameter(label: new TM("settings.ips.conrad.shopID"), - description: new TM("settings.ips.conrad.shopID.description"), formType: EnumType::class, formOptions: ['class' => ConradShopIDs::class], )] public ConradShopIDs $shopID = ConradShopIDs::COM_B2B; - #[SettingsParameter(label: new TM("settings.ips.reichelt.include_vat"))] public bool $includeVAT = true; - - /** - * @var array|string[] Only attachments in these languages will be downloaded (ISO 639-1 codes) - */ - #[Assert\Unique()] - #[Assert\All([new Assert\Language()])] - #[SettingsParameter(type: ArrayType::class, - label: new TM("settings.ips.conrad.attachment_language_filter"), description: new TM("settings.ips.conrad.attachment_language_filter.description"), - options: ['type' => StringType::class], - formType: LanguageType::class, - formOptions: [ - 'multiple' => true, - 'preferred_choices' => ['en', 'de', 'fr', 'it', 'cs', 'da', 'nl', 'hu', 'hr', 'sk', 'pl'] - ], - )] - public array $attachmentLanguageFilter = ['en']; } diff --git a/src/Settings/InfoProviderSystem/ConradShopIDs.php b/src/Settings/InfoProviderSystem/ConradShopIDs.php index e39ed7b1..2d8710e7 100644 --- a/src/Settings/InfoProviderSystem/ConradShopIDs.php +++ b/src/Settings/InfoProviderSystem/ConradShopIDs.php @@ -30,22 +30,20 @@ enum ConradShopIDs: string implements TranslatableInterface { case COM_B2B = 'HP_COM_B2B'; case DE_B2B = 'CQ_DE_B2B'; - case DE_B2C = 'CQ_DE_B2C'; case AT_B2C = 'CQ_AT_B2C'; - case CH_B2C_DE = 'CQ_CH_B2C_DE'; - case CH_B2C_FR = 'CQ_CH_B2C_FR'; + case CH_B2C = 'CQ_CH_B2C'; case SE_B2B = 'HP_SE_B2B'; case HU_B2C = 'CQ_HU_B2C'; case CZ_B2B = 'HP_CZ_B2B'; case SI_B2B = 'HP_SI_B2B'; case SK_B2B = 'HP_SK_B2B'; case BE_B2B = 'HP_BE_B2B'; + case DE_B2C = 'CQ_DE_B2C'; case PL_B2B = 'HP_PL_B2B'; case NL_B2B = 'CQ_NL_B2B'; - case NL_B2C = 'CQ_NL_B2C'; case DK_B2B = 'HP_DK_B2B'; case IT_B2B = 'HP_IT_B2B'; - + case NL_B2C = 'CQ_NL_B2C'; case FR_B2B = 'HP_FR_B2B'; case AT_B2B = 'CQ_AT_B2B'; case HR_B2B = 'HP_HR_B2B'; @@ -56,8 +54,7 @@ enum ConradShopIDs: string implements TranslatableInterface return match ($this) { self::DE_B2B => "conrad.de (B2B)", self::AT_B2C => "conrad.at (B2C)", - self::CH_B2C_DE => "conrad.ch DE (B2C)", - self::CH_B2C_FR => "conrad.ch FR (B2C)", + self::CH_B2C => "conrad.ch (B2C)", self::SE_B2B => "conrad.se (B2B)", self::HU_B2C => "conrad.hu (B2C)", self::CZ_B2B => "conrad.cz (B2B)", @@ -67,7 +64,7 @@ enum ConradShopIDs: string implements TranslatableInterface self::DE_B2C => "conrad.de (B2C)", self::PL_B2B => "conrad.pl (B2B)", self::NL_B2B => "conrad.nl (B2B)", - self::DK_B2B => "conradelektronik.dk (B2B)", + self::DK_B2B => "conrad.dk (B2B)", self::IT_B2B => "conrad.it (B2B)", self::NL_B2C => "conrad.nl (B2C)", self::FR_B2B => "conrad.fr (B2B)", @@ -79,10 +76,6 @@ enum ConradShopIDs: string implements TranslatableInterface public function getDomain(): string { - if ($this === self::DK_B2B) { - return 'conradelektronik.dk'; - } - return 'conrad.' . $this->getDomainEnd(); } @@ -101,10 +94,6 @@ enum ConradShopIDs: string implements TranslatableInterface */ public function getShopID(): string { - if ($this === self::CH_B2C_FR || $this === self::CH_B2C_DE) { - return 'CQ_CH_B2C'; - } - return $this->value; } @@ -113,7 +102,7 @@ enum ConradShopIDs: string implements TranslatableInterface return match ($this) { self::DE_B2B, self::DE_B2C => 'de', self::AT_B2B, self::AT_B2C => 'at', - self::CH_B2C_DE => 'ch', self::CH_B2C_FR => 'ch', + self::CH_B2C => 'ch', self::SE_B2B => 'se', self::HU_B2C => 'hu', self::CZ_B2B => 'cz', @@ -134,7 +123,7 @@ enum ConradShopIDs: string implements TranslatableInterface { return match ($this) { self::DE_B2B, self::DE_B2C, self::AT_B2B, self::AT_B2C => 'de', - self::CH_B2C_DE => 'de', self::CH_B2C_FR => 'fr', + self::CH_B2C => 'de', self::SE_B2B => 'sv', self::HU_B2C => 'hu', self::CZ_B2B => 'cs', @@ -161,7 +150,7 @@ enum ConradShopIDs: string implements TranslatableInterface self::DE_B2B, self::AT_B2B, self::SE_B2B, self::CZ_B2B, self::SI_B2B, self::SK_B2B, self::BE_B2B, self::PL_B2B, self::NL_B2B, self::DK_B2B, self::IT_B2B, self::FR_B2B, self::COM_B2B, self::HR_B2B => 'b2b', - self::DE_B2C, self::AT_B2C, self::CH_B2C_DE, self::CH_B2C_FR, self::HU_B2C, self::NL_B2C => 'b2c', + self::DE_B2C, self::AT_B2C, self::CH_B2C, self::HU_B2C, self::NL_B2C => 'b2c', }; } } diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index b2bd908e..5c4151d6 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -9928,13 +9928,13 @@ Element 1 -> Element 1.2]]> project.builds.number_of_builds_possible - %max_builds% builds of this [project].]]> + You have enough stocked to build <b>%max_builds%</b> builds of this [project]. project.builds.check_project_status - "%project_status%". You should check if you really want to build the [project] with this status!]]> + The current [project] status is <b>"%project_status%"</b>. You should check if you really want to build the [project] with this status! @@ -14286,35 +14286,5 @@ Buerklin-API Authentication server: Transport error while retrieving information from the providers. Check that your server has internet accesss. See server logs for more info. - - - settings.ips.conrad - Conrad - - - - - settings.ips.conrad.shopID - Shop ID - - - - - settings.ips.conrad.shopID.description - The version of the conrad store you wanna get results from. This determines language, prices and currency of the results. If both a B2B and a B2C version if available, you should choose the B2C version if you want prices including VAT. - - - - - settings.ips.conrad.attachment_language_filter - Language filter for attachments - - - - - settings.ips.conrad.attachment_language_filter.description - Only includes attachments in the selected languages in the results. - -