From 705e71f1ebdf6825a375f3de96ee278921ecba9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sun, 25 Jan 2026 20:13:04 +0100 Subject: [PATCH 01/14] Started working on a conrad provider --- .../Providers/ConradProvider.php | 103 ++++++++++++++++++ .../InfoProviderSystem/ConradSettings.php | 69 ++++++++++++ .../InfoProviderSettings.php | 5 +- 3 files changed, 176 insertions(+), 1 deletion(-) create mode 100644 src/Services/InfoProviderSystem/Providers/ConradProvider.php create mode 100644 src/Settings/InfoProviderSystem/ConradSettings.php diff --git a/src/Services/InfoProviderSystem/Providers/ConradProvider.php b/src/Services/InfoProviderSystem/Providers/ConradProvider.php new file mode 100644 index 00000000..b72be0bd --- /dev/null +++ b/src/Services/InfoProviderSystem/Providers/ConradProvider.php @@ -0,0 +1,103 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Services\InfoProviderSystem\Providers; + +use App\Services\InfoProviderSystem\DTOs\PartDetailDTO; +use App\Services\InfoProviderSystem\DTOs\SearchResultDTO; +use App\Settings\InfoProviderSystem\ConradSettings; +use Symfony\Contracts\HttpClient\HttpClientInterface; + +readonly class ConradProvider implements InfoProviderInterface +{ + + private const SEARCH_ENDPOINT = 'https://api.conrad.de/search/1/v3/facetSearch'; + + public function __construct(private HttpClientInterface $httpClient, private ConradSettings $settings) + { + } + + public function getProviderInfo(): array + { + return [ + 'name' => 'Pollin', + 'description' => 'Retrieves part information from conrad.de', + 'url' => 'https://www.conrad.de/', + 'disabled_help' => 'Set API key in settings', + 'settings_class' => ConradSettings::class, + ]; + } + + public function getProviderKey(): string + { + return 'conrad'; + } + + public function isActive(): bool + { + return !empty($this->settings->apiKey); + } + + public function searchByKeyword(string $keyword): array + { + $url = self::SEARCH_ENDPOINT . '/' . $this->settings->country . '/' . $this->settings->language . '/' . $this->settings->customerType; + + $response = $this->httpClient->request('POST', $url, [ + 'query' => [ + 'apikey' => $this->settings->apiKey, + ], + 'json' => [ + 'query' => $keyword, + ], + ]); + + $out = []; + $results = $response->toArray(); + + foreach($results as $result) { + $out[] = new SearchResultDTO( + provider_key: $this->getProviderKey(), + provider_id: $result['productId'], + name: $result['title'], + description: '', + manufacturer: $result['brand']['name'] ?? null, + mpn: $result['manufacturerId'] ?? null, + preview_image_url: $result['image'] ?? null, + ); + } + + return $out; + } + + public function getDetails(string $id): PartDetailDTO + { + // TODO: Implement getDetails() method. + } + + public function getCapabilities(): array + { + return [ProviderCapabilities::BASIC, + ProviderCapabilities::PICTURE, + ProviderCapabilities::PRICE,]; + } +} diff --git a/src/Settings/InfoProviderSystem/ConradSettings.php b/src/Settings/InfoProviderSystem/ConradSettings.php new file mode 100644 index 00000000..2330e729 --- /dev/null +++ b/src/Settings/InfoProviderSystem/ConradSettings.php @@ -0,0 +1,69 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Settings\InfoProviderSystem; + +use App\Form\Type\APIKeyType; +use App\Settings\SettingsIcon; +use Jbtronics\SettingsBundle\Metadata\EnvVarMode; +use Jbtronics\SettingsBundle\Settings\Settings; +use Jbtronics\SettingsBundle\Settings\SettingsParameter; +use Jbtronics\SettingsBundle\Settings\SettingsTrait; +use Symfony\Component\Form\Extension\Core\Type\ChoiceType; +use Symfony\Component\Form\Extension\Core\Type\CountryType; +use Symfony\Component\Form\Extension\Core\Type\LanguageType; +use Symfony\Component\Translation\TranslatableMessage as TM; +use Symfony\Component\Validator\Constraints as Assert; + +#[Settings(label: new TM("settings.ips.conrad"))] +#[SettingsIcon("fa-plug")] +class ConradSettings +{ + use SettingsTrait; + + #[SettingsParameter(label: new TM("settings.ips.element14.apiKey"), + formType: APIKeyType::class, + formOptions: ["help_html" => true], envVar: "PROVIDER_CONRAD_API_KEY", envVarMode: EnvVarMode::OVERWRITE)] + public ?string $apiKey = null; + + #[SettingsParameter(label: new TM("settings.ips.tme.country"), formType: CountryType::class, + envVar: "PROVIDER_CONRAD_COUNTRY", envVarMode: EnvVarMode::OVERWRITE)] + #[Assert\Country] + public string $country = "DE"; + + #[SettingsParameter(label: new TM("settings.ips.tme.language"), formType: LanguageType::class, + envVar: "PROVIDER_CONRAD_LANGUAGE", envVarMode: EnvVarMode::OVERWRITE)] + #[Assert\Language] + public string $language = "en"; + + #[SettingsParameter(label: new TM("settings.ips.conrad.customerType"), formType: ChoiceType::class, + formOptions: [ + "choices" => [ + "settings.ips.conrad.customerType.b2c" => "b2c", + "settings.ips.conrad.customerType.b2b" => "b2b", + ], + ], + envVar: "PROVIDER_CONRAD_LANGUAGE", envVarMode: EnvVarMode::OVERWRITE, )] + #[Assert\Choice(choices: ["b2c", "b2b"])] + public string $customerType = "b2c"; +} diff --git a/src/Settings/InfoProviderSystem/InfoProviderSettings.php b/src/Settings/InfoProviderSystem/InfoProviderSettings.php index d4679e23..fb31bdb9 100644 --- a/src/Settings/InfoProviderSystem/InfoProviderSettings.php +++ b/src/Settings/InfoProviderSystem/InfoProviderSettings.php @@ -63,7 +63,10 @@ class InfoProviderSettings #[EmbeddedSettings] public ?PollinSettings $pollin = null; - + #[EmbeddedSettings] public ?BuerklinSettings $buerklin = null; + + #[EmbeddedSettings] + public ?ConradSettings $conrad = null; } From 7ab33c859bf2ea642d33ffd03c16954c2e13c4b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Mon, 26 Jan 2026 23:07:01 +0100 Subject: [PATCH 02/14] Implemented basic functionality to search and retrieve part details --- .../Providers/ConradProvider.php | 65 +++++++- .../InfoProviderSystem/ConradSettings.php | 26 +-- .../InfoProviderSystem/ConradShopIDs.php | 156 ++++++++++++++++++ 3 files changed, 223 insertions(+), 24 deletions(-) create mode 100644 src/Settings/InfoProviderSystem/ConradShopIDs.php diff --git a/src/Services/InfoProviderSystem/Providers/ConradProvider.php b/src/Services/InfoProviderSystem/Providers/ConradProvider.php index b72be0bd..7212444b 100644 --- a/src/Services/InfoProviderSystem/Providers/ConradProvider.php +++ b/src/Services/InfoProviderSystem/Providers/ConradProvider.php @@ -31,7 +31,7 @@ use Symfony\Contracts\HttpClient\HttpClientInterface; readonly class ConradProvider implements InfoProviderInterface { - private const SEARCH_ENDPOINT = 'https://api.conrad.de/search/1/v3/facetSearch'; + private const SEARCH_ENDPOINT = '/search/1/v3/facetSearch'; public function __construct(private HttpClientInterface $httpClient, private ConradSettings $settings) { @@ -40,7 +40,7 @@ readonly class ConradProvider implements InfoProviderInterface public function getProviderInfo(): array { return [ - 'name' => 'Pollin', + 'name' => 'Conrad', 'description' => 'Retrieves part information from conrad.de', 'url' => 'https://www.conrad.de/', 'disabled_help' => 'Set API key in settings', @@ -58,9 +58,38 @@ readonly class ConradProvider implements InfoProviderInterface return !empty($this->settings->apiKey); } + private function getProductUrl(string $productId): string + { + return 'https://' . $this->settings->shopID->getDomain() . '/' . $this->settings->shopID->getLanguage() . '/p/' . $productId; + } + + private function getFootprintFromTechnicalDetails(array $technicalDetails): ?string + { + foreach ($technicalDetails as $detail) { + if ($detail['name'] === 'ATT_LOV_HOUSING_SEMICONDUCTORS') { + return $detail['values'][0] ?? null; + } + } + + 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; + } + public function searchByKeyword(string $keyword): array { - $url = self::SEARCH_ENDPOINT . '/' . $this->settings->country . '/' . $this->settings->language . '/' . $this->settings->customerType; + $url = $this->settings->shopID->getAPIRoot() . self::SEARCH_ENDPOINT . '/' + . $this->settings->shopID->getDomainEnd() . '/' . $this->settings->shopID->getLanguage() + . '/' . $this->settings->shopID->getCustomerType(); $response = $this->httpClient->request('POST', $url, [ 'query' => [ @@ -68,13 +97,15 @@ readonly class ConradProvider implements InfoProviderInterface ], 'json' => [ 'query' => $keyword, + 'size' => 25, ], ]); $out = []; $results = $response->toArray(); - foreach($results as $result) { + foreach($results['hits'] as $result) { + $out[] = new SearchResultDTO( provider_key: $this->getProviderKey(), provider_id: $result['productId'], @@ -83,6 +114,8 @@ readonly class ConradProvider implements InfoProviderInterface manufacturer: $result['brand']['name'] ?? null, mpn: $result['manufacturerId'] ?? null, preview_image_url: $result['image'] ?? null, + provider_url: $this->getProductUrl($result['productId']), + footprint: $this->getFootprintFromTechnicalDetails($result['technicalDetails'] ?? []), ); } @@ -91,7 +124,29 @@ readonly class ConradProvider implements InfoProviderInterface public function getDetails(string $id): PartDetailDTO { - // TODO: Implement getDetails() method. + $productInfoURL = $this->settings->shopID->getAPIRoot() . '/product/1/service/' . $this->settings->shopID->getShopID() + . '/product/' . $id; + + $response = $this->httpClient->request('GET', $productInfoURL, [ + 'query' => [ + 'apikey' => $this->settings->apiKey, + ] + ]); + + $data = $response->toArray(); + + return new PartDetailDTO( + provider_key: $this->getProviderKey(), + provider_id: $data['shortProductNumber'], + 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, + ); } public function getCapabilities(): array diff --git a/src/Settings/InfoProviderSystem/ConradSettings.php b/src/Settings/InfoProviderSystem/ConradSettings.php index 2330e729..999ebfe0 100644 --- a/src/Settings/InfoProviderSystem/ConradSettings.php +++ b/src/Settings/InfoProviderSystem/ConradSettings.php @@ -31,6 +31,7 @@ use Jbtronics\SettingsBundle\Settings\SettingsParameter; use Jbtronics\SettingsBundle\Settings\SettingsTrait; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\Extension\Core\Type\CountryType; +use Symfony\Component\Form\Extension\Core\Type\EnumType; use Symfony\Component\Form\Extension\Core\Type\LanguageType; use Symfony\Component\Translation\TranslatableMessage as TM; use Symfony\Component\Validator\Constraints as Assert; @@ -46,24 +47,11 @@ class ConradSettings formOptions: ["help_html" => true], envVar: "PROVIDER_CONRAD_API_KEY", envVarMode: EnvVarMode::OVERWRITE)] public ?string $apiKey = null; - #[SettingsParameter(label: new TM("settings.ips.tme.country"), formType: CountryType::class, - envVar: "PROVIDER_CONRAD_COUNTRY", envVarMode: EnvVarMode::OVERWRITE)] - #[Assert\Country] - public string $country = "DE"; + #[SettingsParameter(label: new TM("settings.ips.conrad.shopID"), + formType: EnumType::class, + formOptions: ['class' => ConradShopIDs::class], + )] + public ConradShopIDs $shopID = ConradShopIDs::COM_B2B; - #[SettingsParameter(label: new TM("settings.ips.tme.language"), formType: LanguageType::class, - envVar: "PROVIDER_CONRAD_LANGUAGE", envVarMode: EnvVarMode::OVERWRITE)] - #[Assert\Language] - public string $language = "en"; - - #[SettingsParameter(label: new TM("settings.ips.conrad.customerType"), formType: ChoiceType::class, - formOptions: [ - "choices" => [ - "settings.ips.conrad.customerType.b2c" => "b2c", - "settings.ips.conrad.customerType.b2b" => "b2b", - ], - ], - envVar: "PROVIDER_CONRAD_LANGUAGE", envVarMode: EnvVarMode::OVERWRITE, )] - #[Assert\Choice(choices: ["b2c", "b2b"])] - public string $customerType = "b2c"; + public bool $includeVAT = true; } diff --git a/src/Settings/InfoProviderSystem/ConradShopIDs.php b/src/Settings/InfoProviderSystem/ConradShopIDs.php new file mode 100644 index 00000000..2d8710e7 --- /dev/null +++ b/src/Settings/InfoProviderSystem/ConradShopIDs.php @@ -0,0 +1,156 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Settings\InfoProviderSystem; + +use Symfony\Contracts\Translation\TranslatableInterface; +use Symfony\Contracts\Translation\TranslatorInterface; + +enum ConradShopIDs: string implements TranslatableInterface +{ + case COM_B2B = 'HP_COM_B2B'; + case DE_B2B = 'CQ_DE_B2B'; + case AT_B2C = 'CQ_AT_B2C'; + 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 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'; + + + public function trans(TranslatorInterface $translator, ?string $locale = null): string + { + return match ($this) { + self::DE_B2B => "conrad.de (B2B)", + self::AT_B2C => "conrad.at (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)", + self::SI_B2B => "conrad.si (B2B)", + self::SK_B2B => "conrad.sk (B2B)", + self::BE_B2B => "conrad.be (B2B)", + self::DE_B2C => "conrad.de (B2C)", + self::PL_B2B => "conrad.pl (B2B)", + self::NL_B2B => "conrad.nl (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)", + self::COM_B2B => "conrad.com (B2B)", + self::AT_B2B => "conrad.at (B2B)", + self::HR_B2B => "conrad.hr (B2B)", + }; + } + + public function getDomain(): string + { + return 'conrad.' . $this->getDomainEnd(); + } + + /** + * Retrieves the API root URL for this shop ID. e.g. https://api.conrad.de + * @return string + */ + public function getAPIRoot(): string + { + return 'https://api.' . $this->getDomain(); + } + + /** + * Returns the shop ID value used in the API requests. e.g. 'CQ_DE_B2B' + * @return string + */ + public function getShopID(): string + { + return $this->value; + } + + public function getDomainEnd(): string + { + return match ($this) { + self::DE_B2B, self::DE_B2C => 'de', + self::AT_B2B, self::AT_B2C => 'at', + self::CH_B2C => 'ch', + self::SE_B2B => 'se', + self::HU_B2C => 'hu', + self::CZ_B2B => 'cz', + self::SI_B2B => 'si', + self::SK_B2B => 'sk', + self::BE_B2B => 'be', + self::PL_B2B => 'pl', + self::NL_B2B, self::NL_B2C => 'nl', + self::DK_B2B => 'dk', + self::IT_B2B => 'it', + self::FR_B2B => 'fr', + self::COM_B2B => 'com', + self::HR_B2B => 'hr', + }; + } + + public function getLanguage(): string + { + return match ($this) { + self::DE_B2B, self::DE_B2C, self::AT_B2B, self::AT_B2C => 'de', + self::CH_B2C => 'de', + self::SE_B2B => 'sv', + self::HU_B2C => 'hu', + self::CZ_B2B => 'cs', + self::SI_B2B => 'sl', + self::SK_B2B => 'sk', + self::BE_B2B => 'nl', + self::PL_B2B => 'pl', + self::NL_B2B, self::NL_B2C => 'nl', + self::DK_B2B => 'da', + self::IT_B2B => 'it', + self::FR_B2B => 'fr', + self::COM_B2B => 'en', + self::HR_B2B => 'hr', + }; + } + + /** + * Retrieves the customer type for this shop ID. e.g. 'b2b' or 'b2c' + * @return string 'b2b' or 'b2c' + */ + public function getCustomerType(): string + { + return match ($this) { + 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, self::HU_B2C, self::NL_B2C => 'b2c', + }; + } +} From 3ed62f5cee80ca4dbfa935b27caff50b9af0af1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Mon, 26 Jan 2026 23:18:32 +0100 Subject: [PATCH 03/14] Allow to retrieve parameters from conrad --- .../Providers/ConradProvider.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/Services/InfoProviderSystem/Providers/ConradProvider.php b/src/Services/InfoProviderSystem/Providers/ConradProvider.php index 7212444b..8c343099 100644 --- a/src/Services/InfoProviderSystem/Providers/ConradProvider.php +++ b/src/Services/InfoProviderSystem/Providers/ConradProvider.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace App\Services\InfoProviderSystem\Providers; +use App\Services\InfoProviderSystem\DTOs\ParameterDTO; use App\Services\InfoProviderSystem\DTOs\PartDetailDTO; use App\Services\InfoProviderSystem\DTOs\SearchResultDTO; use App\Settings\InfoProviderSystem\ConradSettings; @@ -85,6 +86,20 @@ readonly class ConradProvider implements InfoProviderInterface 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 . '/' @@ -146,6 +161,7 @@ readonly class ConradProvider implements InfoProviderInterface provider_url: $this->getProductUrl($data['shortProductNumber']), footprint: $this->getFootprintFromTechnicalAttributes($data['productFullInformation']['technicalAttributes'] ?? []), notes: $data['productFullInformation']['description'] ?? null, + parameters: $this->technicalAttributesToParameters($data['productFullInformation']['technicalAttributes'] ?? []), ); } From 6628333675eaeafcba93c69ea392bd9ca9490892 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sat, 31 Jan 2026 18:43:59 +0100 Subject: [PATCH 04/14] Properly handle danish and non-german swiss shop --- .../InfoProviderSystem/ConradShopIDs.php | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/Settings/InfoProviderSystem/ConradShopIDs.php b/src/Settings/InfoProviderSystem/ConradShopIDs.php index 2d8710e7..64480bcd 100644 --- a/src/Settings/InfoProviderSystem/ConradShopIDs.php +++ b/src/Settings/InfoProviderSystem/ConradShopIDs.php @@ -31,7 +31,8 @@ enum ConradShopIDs: string implements TranslatableInterface case COM_B2B = 'HP_COM_B2B'; case DE_B2B = 'CQ_DE_B2B'; case AT_B2C = 'CQ_AT_B2C'; - case CH_B2C = 'CQ_CH_B2C'; + case CH_B2C_DE = 'CQ_CH_B2C_DE'; + case CH_B2C_FR = 'CQ_CH_B2C_FR'; case SE_B2B = 'HP_SE_B2B'; case HU_B2C = 'CQ_HU_B2C'; case CZ_B2B = 'HP_CZ_B2B'; @@ -54,7 +55,8 @@ enum ConradShopIDs: string implements TranslatableInterface return match ($this) { self::DE_B2B => "conrad.de (B2B)", self::AT_B2C => "conrad.at (B2C)", - self::CH_B2C => "conrad.ch (B2C)", + self::CH_B2C_DE => "conrad.ch DE (B2C)", + self::CH_B2C_FR => "conrad.ch FR (B2C)", self::SE_B2B => "conrad.se (B2B)", self::HU_B2C => "conrad.hu (B2C)", self::CZ_B2B => "conrad.cz (B2B)", @@ -64,7 +66,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 => "conrad.dk (B2B)", + self::DK_B2B => "conradelektronik.dk (B2B)", self::IT_B2B => "conrad.it (B2B)", self::NL_B2C => "conrad.nl (B2C)", self::FR_B2B => "conrad.fr (B2B)", @@ -76,6 +78,10 @@ enum ConradShopIDs: string implements TranslatableInterface public function getDomain(): string { + if ($this === self::DK_B2B) { + return 'conradelektronik.dk'; + } + return 'conrad.' . $this->getDomainEnd(); } @@ -102,7 +108,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 => 'ch', + self::CH_B2C_DE => 'ch', self::CH_B2C_FR => 'ch', self::SE_B2B => 'se', self::HU_B2C => 'hu', self::CZ_B2B => 'cz', @@ -123,7 +129,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', + self::CH_B2C_DE => 'de', self::CH_B2C_FR => 'fr', self::SE_B2B => 'sv', self::HU_B2C => 'hu', self::CZ_B2B => 'cs', @@ -150,7 +156,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, self::HU_B2C, self::NL_B2C => 'b2c', + self::DE_B2C, self::AT_B2C, self::CH_B2C_DE, self::CH_B2C_FR, self::HU_B2C, self::NL_B2C => 'b2c', }; } } From 22cf04585b1f4f9204190d2cf943a5aedf55d376 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sat, 31 Jan 2026 18:57:00 +0100 Subject: [PATCH 05/14] Allow to retrieve datasheets from conrad --- .../Providers/ConradProvider.php | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/src/Services/InfoProviderSystem/Providers/ConradProvider.php b/src/Services/InfoProviderSystem/Providers/ConradProvider.php index 8c343099..618fb403 100644 --- a/src/Services/InfoProviderSystem/Providers/ConradProvider.php +++ b/src/Services/InfoProviderSystem/Providers/ConradProvider.php @@ -23,6 +23,7 @@ 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\SearchResultDTO; @@ -100,6 +101,16 @@ readonly class ConradProvider implements InfoProviderInterface return $parameters; } + public function productMediaToDatasheets(array $productMedia): array + { + $files = []; + foreach ($productMedia['manuals'] as $manual) { + $files[] = new FileDTO($manual['fullUrl'], $manual['title'] . ' (' . $manual['language'] . ')'); + } + + return $files; + } + public function searchByKeyword(string $keyword): array { $url = $this->settings->shopID->getAPIRoot() . self::SEARCH_ENDPOINT . '/' @@ -112,7 +123,8 @@ readonly class ConradProvider implements InfoProviderInterface ], 'json' => [ 'query' => $keyword, - 'size' => 25, + 'size' => 50, + 'sort' => [["field"=>"_score","order"=>"desc"]], ], ]); @@ -161,14 +173,18 @@ readonly class ConradProvider implements InfoProviderInterface provider_url: $this->getProductUrl($data['shortProductNumber']), footprint: $this->getFootprintFromTechnicalAttributes($data['productFullInformation']['technicalAttributes'] ?? []), notes: $data['productFullInformation']['description'] ?? null, - parameters: $this->technicalAttributesToParameters($data['productFullInformation']['technicalAttributes'] ?? []), + datasheets: $this->productMediaToDatasheets($data['productMedia'] ?? []), + parameters: $this->technicalAttributesToParameters($data['productFullInformation']['technicalAttributes'] ?? []) ); } public function getCapabilities(): array { - return [ProviderCapabilities::BASIC, + return [ + ProviderCapabilities::BASIC, ProviderCapabilities::PICTURE, - ProviderCapabilities::PRICE,]; + ProviderCapabilities::DATASHEET, + ProviderCapabilities::PRICE, + ]; } } From 6f4dad98d9b0c9941bad660cc1e07e66cd1ae099 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sat, 31 Jan 2026 19:04:25 +0100 Subject: [PATCH 06/14] Use parameter parsing logic from PR #1211 to handle multi parameters fine --- .../Providers/ConradProvider.php | 56 ++++++++++++++++--- 1 file changed, 47 insertions(+), 9 deletions(-) diff --git a/src/Services/InfoProviderSystem/Providers/ConradProvider.php b/src/Services/InfoProviderSystem/Providers/ConradProvider.php index 618fb403..f4d1467f 100644 --- a/src/Services/InfoProviderSystem/Providers/ConradProvider.php +++ b/src/Services/InfoProviderSystem/Providers/ConradProvider.php @@ -89,16 +89,54 @@ readonly class ConradProvider implements InfoProviderInterface 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 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 $parameters; + 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'] : $q['value'] + , $p['values'])); + return ParameterDTO::parseValueIncludingUnit( + name: $p['attributeName'], + value: $values, + ); + }, $technicalAttributes); } public function productMediaToDatasheets(array $productMedia): array From 98937974c99aa0eb8997cefa643050ef5d9f6b31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sat, 31 Jan 2026 21:15:35 +0100 Subject: [PATCH 07/14] Allow to query price infos from conrad --- .../Providers/ConradProvider.php | 138 ++++++++++++++---- .../InfoProviderSystem/ConradSettings.php | 1 + .../InfoProviderSystem/ConradShopIDs.php | 4 + 3 files changed, 117 insertions(+), 26 deletions(-) diff --git a/src/Services/InfoProviderSystem/Providers/ConradProvider.php b/src/Services/InfoProviderSystem/Providers/ConradProvider.php index f4d1467f..857b4135 100644 --- a/src/Services/InfoProviderSystem/Providers/ConradProvider.php +++ b/src/Services/InfoProviderSystem/Providers/ConradProvider.php @@ -26,6 +26,8 @@ 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; @@ -34,9 +36,18 @@ readonly class ConradProvider implements InfoProviderInterface { private const SEARCH_ENDPOINT = '/search/1/v3/facetSearch'; + public const DISTRIBUTOR_NAME = 'Conrad'; - public function __construct(private HttpClientInterface $httpClient, private ConradSettings $settings) + private HttpClientInterface $httpClient; + + public function __construct( HttpClientInterface $httpClient, private ConradSettings $settings) { + //We want everything in JSON + $this->httpClient = $httpClient->withOptions([ + 'headers' => [ + 'Accept' => 'application/json', + ], + ]); } public function getProviderInfo(): array @@ -76,6 +87,44 @@ readonly class ConradProvider implements InfoProviderInterface return null; } + public function searchByKeyword(string $keyword): array + { + $url = $this->settings->shopID->getAPIRoot() . self::SEARCH_ENDPOINT . '/' + . $this->settings->shopID->getDomainEnd() . '/' . $this->settings->shopID->getLanguage() + . '/' . $this->settings->shopID->getCustomerType(); + + $response = $this->httpClient->request('POST', $url, [ + 'query' => [ + 'apikey' => $this->settings->apiKey, + ], + 'json' => [ + 'query' => $keyword, + 'size' => 50, + 'sort' => [["field"=>"_score","order"=>"desc"]], + ], + ]); + + $out = []; + $results = $response->toArray(); + + foreach($results['hits'] as $result) { + + $out[] = new SearchResultDTO( + provider_key: $this->getProviderKey(), + provider_id: $result['productId'], + name: $result['title'], + description: '', + manufacturer: $result['brand']['name'] ?? null, + mpn: $result['manufacturerId'] ?? null, + preview_image_url: $result['image'] ?? null, + provider_url: $this->getProductUrl($result['productId']), + footprint: $this->getFootprintFromTechnicalDetails($result['technicalDetails'] ?? []), + ); + } + + return $out; + } + private function getFootprintFromTechnicalAttributes(array $technicalDetails): ?string { foreach ($technicalDetails as $detail) { @@ -87,6 +136,10 @@ readonly class ConradProvider implements InfoProviderInterface return null; } + /** + * @param array $technicalAttributes + * @return array + */ private function technicalAttributesToParameters(array $technicalAttributes): array { return array_map(static function (array $p) { @@ -139,6 +192,10 @@ readonly class ConradProvider implements InfoProviderInterface }, $technicalAttributes); } + /** + * @param array $productMedia + * @return array + */ public function productMediaToDatasheets(array $productMedia): array { $files = []; @@ -149,42 +206,70 @@ readonly class ConradProvider implements InfoProviderInterface return $files; } - public function searchByKeyword(string $keyword): array - { - $url = $this->settings->shopID->getAPIRoot() . self::SEARCH_ENDPOINT . '/' - . $this->settings->shopID->getDomainEnd() . '/' . $this->settings->shopID->getLanguage() - . '/' . $this->settings->shopID->getCustomerType(); - $response = $this->httpClient->request('POST', $url, [ + /** + * 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' => [ - 'query' => $keyword, - 'size' => 50, - 'sort' => [["field"=>"_score","order"=>"desc"]], - ], + 'ns:inputArticleItemList' => [ + "#namespaces" => [ + "ns" => "http://www.conrad.de/ccp/basit/service/article/priceandavailabilityservice/api" + ], + 'articles' => [ + [ + "articleID" => $productId, + "calculatePrice" => true, + "checkAvailability" => true, + ], + ] + ] + ] ]); - $out = []; - $results = $response->toArray(); + $result = $response->toArray(); - foreach($results['hits'] as $result) { + $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; - $out[] = new SearchResultDTO( - provider_key: $this->getProviderKey(), - provider_id: $result['productId'], - name: $result['title'], - description: '', - manufacturer: $result['brand']['name'] ?? null, - mpn: $result['manufacturerId'] ?? null, - preview_image_url: $result['image'] ?? null, - provider_url: $this->getProductUrl($result['productId']), - footprint: $this->getFootprintFromTechnicalDetails($result['technicalDetails'] ?? []), + $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 $out; + return new PurchaseInfoDTO( + distributor_name: self::DISTRIBUTOR_NAME, + order_number: $productId, + prices: $prices, + product_url: $this->getProductUrl($productId) + ); } public function getDetails(string $id): PartDetailDTO @@ -212,7 +297,8 @@ readonly class ConradProvider implements InfoProviderInterface footprint: $this->getFootprintFromTechnicalAttributes($data['productFullInformation']['technicalAttributes'] ?? []), notes: $data['productFullInformation']['description'] ?? null, datasheets: $this->productMediaToDatasheets($data['productMedia'] ?? []), - parameters: $this->technicalAttributesToParameters($data['productFullInformation']['technicalAttributes'] ?? []) + parameters: $this->technicalAttributesToParameters($data['productFullInformation']['technicalAttributes'] ?? []), + vendor_infos: [$this->queryPrices($data['shortProductNumber'])] ); } diff --git a/src/Settings/InfoProviderSystem/ConradSettings.php b/src/Settings/InfoProviderSystem/ConradSettings.php index 999ebfe0..ddd1b4c0 100644 --- a/src/Settings/InfoProviderSystem/ConradSettings.php +++ b/src/Settings/InfoProviderSystem/ConradSettings.php @@ -53,5 +53,6 @@ class ConradSettings )] public ConradShopIDs $shopID = ConradShopIDs::COM_B2B; + #[SettingsParameter(label: new TM("settings.ips.reichelt.include_vat"))] public bool $includeVAT = true; } diff --git a/src/Settings/InfoProviderSystem/ConradShopIDs.php b/src/Settings/InfoProviderSystem/ConradShopIDs.php index 64480bcd..a72609c7 100644 --- a/src/Settings/InfoProviderSystem/ConradShopIDs.php +++ b/src/Settings/InfoProviderSystem/ConradShopIDs.php @@ -100,6 +100,10 @@ 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; } From f168b2a83cc395a3fab1708b92deddd885117652 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sat, 31 Jan 2026 21:30:15 +0100 Subject: [PATCH 08/14] Reordered ConradShopIDs --- src/Settings/InfoProviderSystem/ConradShopIDs.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Settings/InfoProviderSystem/ConradShopIDs.php b/src/Settings/InfoProviderSystem/ConradShopIDs.php index a72609c7..e39ed7b1 100644 --- a/src/Settings/InfoProviderSystem/ConradShopIDs.php +++ b/src/Settings/InfoProviderSystem/ConradShopIDs.php @@ -30,6 +30,7 @@ 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'; @@ -39,12 +40,12 @@ enum ConradShopIDs: string implements TranslatableInterface 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'; From 2f8553303d5b37758ed565eb9317728468177267 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sat, 31 Jan 2026 21:39:34 +0100 Subject: [PATCH 09/14] Use better fields for determine the product name --- .../InfoProviderSystem/Providers/ConradProvider.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Services/InfoProviderSystem/Providers/ConradProvider.php b/src/Services/InfoProviderSystem/Providers/ConradProvider.php index 857b4135..85f7e648 100644 --- a/src/Services/InfoProviderSystem/Providers/ConradProvider.php +++ b/src/Services/InfoProviderSystem/Providers/ConradProvider.php @@ -112,8 +112,8 @@ readonly class ConradProvider implements InfoProviderInterface $out[] = new SearchResultDTO( provider_key: $this->getProviderKey(), provider_id: $result['productId'], - name: $result['title'], - description: '', + name: $result['manufacturerId'] ?? $result['productId'], + description: $result['title'] ?? '', manufacturer: $result['brand']['name'] ?? null, mpn: $result['manufacturerId'] ?? null, preview_image_url: $result['image'] ?? null, @@ -247,7 +247,7 @@ readonly class ConradProvider implements InfoProviderInterface $minOrderAmount = $result['priceAndAvailabilityFacadeResponse']['priceAndAvailability']['availabilityStatus']['minimumOrderQuantity'] ?? 1; $prices = []; - foreach ($priceInfo['priceScale'] as $priceScale) { + foreach ($priceInfo['priceScale'] ?? [] as $priceScale) { $prices[] = new PriceDTO( minimum_discount_amount: max($priceScale['scaleFrom'], $minOrderAmount), price: (string)$priceScale['pricePerUnit'], @@ -288,9 +288,9 @@ readonly class ConradProvider implements InfoProviderInterface return new PartDetailDTO( provider_key: $this->getProviderKey(), provider_id: $data['shortProductNumber'], - name: $data['productShortInformation']['title'], - description: $data['productShortInformation']['shortDescription'] ?? '', - manufacturer: $data['brand']['displayName'] ?? null, + name: $data['productFullInformation']['manufacturer']['name'] ?? $data['productFullInformation']['manufacturer']['id'] ?? $data['shortProductNumber'], + description: $data['productShortInformation']['title'] ?? '', + manufacturer: $data['brand']['displayName'] !== null ? preg_replace("/[\u{2122}\u{00ae}]/", "", $data['brand']['displayName']) : null, //Replace ™ and ® symbols mpn: $data['productFullInformation']['manufacturer']['id'] ?? null, preview_image_url: $data['productShortInformation']['mainImage']['imageUrl'] ?? null, provider_url: $this->getProductUrl($data['shortProductNumber']), From fa04fface337f6bb9f1c179eaec0afffb5fd7ed5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sat, 31 Jan 2026 21:45:27 +0100 Subject: [PATCH 10/14] Fixed bug with parameter parsing --- src/Services/InfoProviderSystem/Providers/ConradProvider.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Services/InfoProviderSystem/Providers/ConradProvider.php b/src/Services/InfoProviderSystem/Providers/ConradProvider.php index 85f7e648..f73a4b68 100644 --- a/src/Services/InfoProviderSystem/Providers/ConradProvider.php +++ b/src/Services/InfoProviderSystem/Providers/ConradProvider.php @@ -183,7 +183,7 @@ readonly class ConradProvider implements InfoProviderInterface // fallback implementation $values = implode(", ", array_map(fn($q) => - array_key_exists('unit', $q) ? $q['value']." ". $q['unit'] : $q['value'] + array_key_exists('unit', $q) ? $q['value']." ". ($q['unit']['name'] ?? $q['unit']) : $q['value'] , $p['values'])); return ParameterDTO::parseValueIncludingUnit( name: $p['attributeName'], From 6d224a4a9f58a7ba7f657a19f66043927dd3b1a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sat, 31 Jan 2026 21:49:43 +0100 Subject: [PATCH 11/14] Allow to filter for languages in conrad attachments --- .../Providers/ConradProvider.php | 5 +++++ .../InfoProviderSystem/ConradSettings.php | 17 +++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/src/Services/InfoProviderSystem/Providers/ConradProvider.php b/src/Services/InfoProviderSystem/Providers/ConradProvider.php index f73a4b68..819fff52 100644 --- a/src/Services/InfoProviderSystem/Providers/ConradProvider.php +++ b/src/Services/InfoProviderSystem/Providers/ConradProvider.php @@ -200,6 +200,11 @@ readonly class ConradProvider implements InfoProviderInterface { $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'] . ')'); } diff --git a/src/Settings/InfoProviderSystem/ConradSettings.php b/src/Settings/InfoProviderSystem/ConradSettings.php index ddd1b4c0..d0f5d7be 100644 --- a/src/Settings/InfoProviderSystem/ConradSettings.php +++ b/src/Settings/InfoProviderSystem/ConradSettings.php @@ -26,6 +26,8 @@ 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; @@ -55,4 +57,19 @@ class ConradSettings #[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"), 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']; } From cd7cd6cdd33cd9f0d9f97db1b36c0081833160f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sat, 31 Jan 2026 21:57:05 +0100 Subject: [PATCH 12/14] Allow to retrieve (short) category info from Conrad provider --- src/Services/InfoProviderSystem/Providers/ConradProvider.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Services/InfoProviderSystem/Providers/ConradProvider.php b/src/Services/InfoProviderSystem/Providers/ConradProvider.php index 819fff52..044eb7a7 100644 --- a/src/Services/InfoProviderSystem/Providers/ConradProvider.php +++ b/src/Services/InfoProviderSystem/Providers/ConradProvider.php @@ -295,6 +295,7 @@ readonly class ConradProvider implements InfoProviderInterface 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 mpn: $data['productFullInformation']['manufacturer']['id'] ?? null, preview_image_url: $data['productShortInformation']['mainImage']['imageUrl'] ?? null, From c0babfa4016c1e32b4070fa8b06ab261f9b2fc7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sat, 31 Jan 2026 22:03:35 +0100 Subject: [PATCH 13/14] Added docs for the conrad info provider --- docs/usage/information_provider_system.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/usage/information_provider_system.md b/docs/usage/information_provider_system.md index 13df7f10..da8ea32b 100644 --- a/docs/usage/information_provider_system.md +++ b/docs/usage/information_provider_system.md @@ -278,6 +278,16 @@ 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 From df3f069a769ee42224429b04a44c9c0daab9dd88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sat, 31 Jan 2026 22:11:50 +0100 Subject: [PATCH 14/14] Added translations for conrad settings --- .../InfoProviderSystem/ConradSettings.php | 4 ++- translations/messages.en.xlf | 34 +++++++++++++++++-- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/src/Settings/InfoProviderSystem/ConradSettings.php b/src/Settings/InfoProviderSystem/ConradSettings.php index d0f5d7be..dda884c8 100644 --- a/src/Settings/InfoProviderSystem/ConradSettings.php +++ b/src/Settings/InfoProviderSystem/ConradSettings.php @@ -50,6 +50,7 @@ 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], )] @@ -64,7 +65,8 @@ class ConradSettings #[Assert\Unique()] #[Assert\All([new Assert\Language()])] #[SettingsParameter(type: ArrayType::class, - label: new TM("settings.ips.conrad.attachment_language_filter"), options: ['type' => StringType::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, diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index 5c4151d6..b2bd908e 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 - You have enough stocked to build <b>%max_builds%</b> builds of this [project]. + %max_builds% builds of this [project].]]> project.builds.check_project_status - The current [project] status is <b>"%project_status%"</b>. You should check if you really want to build the [project] with this status! + "%project_status%". You should check if you really want to build the [project] with this status!]]> @@ -14286,5 +14286,35 @@ 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. + +