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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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. + + From 2534c84039abb18159b33d1ec63bdb93364cd336 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sat, 31 Jan 2026 22:16:50 +0100 Subject: [PATCH 15/17] Updated dependencies --- composer.lock | 453 +++++++++++++++++++++++++------------------------- yarn.lock | 278 +++++++++++++++---------------- 2 files changed, 366 insertions(+), 365 deletions(-) diff --git a/composer.lock b/composer.lock index 7faff993..2ee826f6 100644 --- a/composer.lock +++ b/composer.lock @@ -968,20 +968,20 @@ }, { "name": "api-platform/doctrine-common", - "version": "v4.2.14", + "version": "v4.2.15", "source": { "type": "git", "url": "https://github.com/api-platform/doctrine-common.git", - "reference": "a29e9015ecf4547485ec7fbce52da4ee95c282a0" + "reference": "4967ed6ba91465d6a6a047119658984d40f89a0e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/doctrine-common/zipball/a29e9015ecf4547485ec7fbce52da4ee95c282a0", - "reference": "a29e9015ecf4547485ec7fbce52da4ee95c282a0", + "url": "https://api.github.com/repos/api-platform/doctrine-common/zipball/4967ed6ba91465d6a6a047119658984d40f89a0e", + "reference": "4967ed6ba91465d6a6a047119658984d40f89a0e", "shasum": "" }, "require": { - "api-platform/metadata": "^4.2", + "api-platform/metadata": "^4.2.6", "api-platform/state": "^4.2.4", "doctrine/collections": "^2.1", "doctrine/common": "^3.2.2", @@ -1052,22 +1052,22 @@ "rest" ], "support": { - "source": "https://github.com/api-platform/doctrine-common/tree/v4.2.14" + "source": "https://github.com/api-platform/doctrine-common/tree/v4.2.15" }, - "time": "2026-01-12T13:36:15+00:00" + "time": "2026-01-27T07:12:16+00:00" }, { "name": "api-platform/doctrine-orm", - "version": "v4.2.14", + "version": "v4.2.15", "source": { "type": "git", "url": "https://github.com/api-platform/doctrine-orm.git", - "reference": "7a7c5cb7261ead50481a9a2d8ef721e21ea97945" + "reference": "cf5c99a209a7be3e508c6f5d0fa4d853d43cff84" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/doctrine-orm/zipball/7a7c5cb7261ead50481a9a2d8ef721e21ea97945", - "reference": "7a7c5cb7261ead50481a9a2d8ef721e21ea97945", + "url": "https://api.github.com/repos/api-platform/doctrine-orm/zipball/cf5c99a209a7be3e508c6f5d0fa4d853d43cff84", + "reference": "cf5c99a209a7be3e508c6f5d0fa4d853d43cff84", "shasum": "" }, "require": { @@ -1139,13 +1139,13 @@ "rest" ], "support": { - "source": "https://github.com/api-platform/doctrine-orm/tree/v4.2.14" + "source": "https://github.com/api-platform/doctrine-orm/tree/v4.2.15" }, - "time": "2026-01-23T14:24:03+00:00" + "time": "2026-01-26T15:38:30+00:00" }, { "name": "api-platform/documentation", - "version": "v4.2.14", + "version": "v4.2.15", "source": { "type": "git", "url": "https://github.com/api-platform/documentation.git", @@ -1202,13 +1202,13 @@ ], "description": "API Platform documentation controller.", "support": { - "source": "https://github.com/api-platform/documentation/tree/v4.2.14" + "source": "https://github.com/api-platform/documentation/tree/v4.2.15" }, "time": "2025-12-27T22:15:57+00:00" }, { "name": "api-platform/http-cache", - "version": "v4.2.14", + "version": "v4.2.15", "source": { "type": "git", "url": "https://github.com/api-platform/http-cache.git", @@ -1282,22 +1282,22 @@ "rest" ], "support": { - "source": "https://github.com/api-platform/http-cache/tree/v4.2.14" + "source": "https://github.com/api-platform/http-cache/tree/v4.2.15" }, "time": "2026-01-12T13:36:15+00:00" }, { "name": "api-platform/hydra", - "version": "v4.2.14", + "version": "v4.2.15", "source": { "type": "git", "url": "https://github.com/api-platform/hydra.git", - "reference": "866611a986f4f52da7807b04a0b2cf64e314ab56" + "reference": "32ca5ff3ac5197d0606a846a6570127239091422" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/hydra/zipball/866611a986f4f52da7807b04a0b2cf64e314ab56", - "reference": "866611a986f4f52da7807b04a0b2cf64e314ab56", + "url": "https://api.github.com/repos/api-platform/hydra/zipball/32ca5ff3ac5197d0606a846a6570127239091422", + "reference": "32ca5ff3ac5197d0606a846a6570127239091422", "shasum": "" }, "require": { @@ -1369,22 +1369,22 @@ "rest" ], "support": { - "source": "https://github.com/api-platform/hydra/tree/v4.2.14" + "source": "https://github.com/api-platform/hydra/tree/v4.2.15" }, - "time": "2025-12-27T22:15:57+00:00" + "time": "2026-01-30T09:06:20+00:00" }, { "name": "api-platform/json-api", - "version": "v4.2.14", + "version": "v4.2.15", "source": { "type": "git", "url": "https://github.com/api-platform/json-api.git", - "reference": "86f93ac31f20faeeca5cacd74d1318dc273e6b93" + "reference": "32ca38f977203f8a59f6efee9637261ae4651c29" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/json-api/zipball/86f93ac31f20faeeca5cacd74d1318dc273e6b93", - "reference": "86f93ac31f20faeeca5cacd74d1318dc273e6b93", + "url": "https://api.github.com/repos/api-platform/json-api/zipball/32ca38f977203f8a59f6efee9637261ae4651c29", + "reference": "32ca38f977203f8a59f6efee9637261ae4651c29", "shasum": "" }, "require": { @@ -1451,22 +1451,22 @@ "rest" ], "support": { - "source": "https://github.com/api-platform/json-api/tree/v4.2.14" + "source": "https://github.com/api-platform/json-api/tree/v4.2.15" }, - "time": "2025-12-27T22:15:57+00:00" + "time": "2026-01-26T15:38:30+00:00" }, { "name": "api-platform/json-schema", - "version": "v4.2.14", + "version": "v4.2.15", "source": { "type": "git", "url": "https://github.com/api-platform/json-schema.git", - "reference": "b69ebff7277655c1eb91bc0092fad4bc80aed4fb" + "reference": "4487398c59a07beefeec870a1213c34ae362cb00" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/json-schema/zipball/b69ebff7277655c1eb91bc0092fad4bc80aed4fb", - "reference": "b69ebff7277655c1eb91bc0092fad4bc80aed4fb", + "url": "https://api.github.com/repos/api-platform/json-schema/zipball/4487398c59a07beefeec870a1213c34ae362cb00", + "reference": "4487398c59a07beefeec870a1213c34ae362cb00", "shasum": "" }, "require": { @@ -1532,13 +1532,13 @@ "swagger" ], "support": { - "source": "https://github.com/api-platform/json-schema/tree/v4.2.14" + "source": "https://github.com/api-platform/json-schema/tree/v4.2.15" }, - "time": "2026-01-23T14:31:09+00:00" + "time": "2026-01-26T15:38:30+00:00" }, { "name": "api-platform/jsonld", - "version": "v4.2.14", + "version": "v4.2.15", "source": { "type": "git", "url": "https://github.com/api-platform/jsonld.git", @@ -1612,22 +1612,22 @@ "rest" ], "support": { - "source": "https://github.com/api-platform/jsonld/tree/v4.2.14" + "source": "https://github.com/api-platform/jsonld/tree/v4.2.15" }, "time": "2026-01-12T13:36:15+00:00" }, { "name": "api-platform/metadata", - "version": "v4.2.14", + "version": "v4.2.15", "source": { "type": "git", "url": "https://github.com/api-platform/metadata.git", - "reference": "590195d1038e66a039f1847b43040b7e6b78475f" + "reference": "4d10dbd7b8f036d24df35eb3ec02c0f0befcf397" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/metadata/zipball/590195d1038e66a039f1847b43040b7e6b78475f", - "reference": "590195d1038e66a039f1847b43040b7e6b78475f", + "url": "https://api.github.com/repos/api-platform/metadata/zipball/4d10dbd7b8f036d24df35eb3ec02c0f0befcf397", + "reference": "4d10dbd7b8f036d24df35eb3ec02c0f0befcf397", "shasum": "" }, "require": { @@ -1710,22 +1710,22 @@ "swagger" ], "support": { - "source": "https://github.com/api-platform/metadata/tree/v4.2.14" + "source": "https://github.com/api-platform/metadata/tree/v4.2.15" }, - "time": "2026-01-12T13:36:15+00:00" + "time": "2026-01-27T07:12:16+00:00" }, { "name": "api-platform/openapi", - "version": "v4.2.14", + "version": "v4.2.15", "source": { "type": "git", "url": "https://github.com/api-platform/openapi.git", - "reference": "39ed78187a4a8e7c1c1fc9b5a3ef3913e3e914e3" + "reference": "59c13717f63e21f98d4ed4e4d7122b0bade72e2e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/openapi/zipball/39ed78187a4a8e7c1c1fc9b5a3ef3913e3e914e3", - "reference": "39ed78187a4a8e7c1c1fc9b5a3ef3913e3e914e3", + "url": "https://api.github.com/repos/api-platform/openapi/zipball/59c13717f63e21f98d4ed4e4d7122b0bade72e2e", + "reference": "59c13717f63e21f98d4ed4e4d7122b0bade72e2e", "shasum": "" }, "require": { @@ -1800,22 +1800,22 @@ "swagger" ], "support": { - "source": "https://github.com/api-platform/openapi/tree/v4.2.14" + "source": "https://github.com/api-platform/openapi/tree/v4.2.15" }, - "time": "2026-01-17T19:34:53+00:00" + "time": "2026-01-26T15:38:30+00:00" }, { "name": "api-platform/serializer", - "version": "v4.2.14", + "version": "v4.2.15", "source": { "type": "git", "url": "https://github.com/api-platform/serializer.git", - "reference": "006df770d82860922c7faee493d5d3c14906f810" + "reference": "4d45483a9911b598a262dd2035166ab2040e430f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/serializer/zipball/006df770d82860922c7faee493d5d3c14906f810", - "reference": "006df770d82860922c7faee493d5d3c14906f810", + "url": "https://api.github.com/repos/api-platform/serializer/zipball/4d45483a9911b598a262dd2035166ab2040e430f", + "reference": "4d45483a9911b598a262dd2035166ab2040e430f", "shasum": "" }, "require": { @@ -1893,22 +1893,22 @@ "serializer" ], "support": { - "source": "https://github.com/api-platform/serializer/tree/v4.2.14" + "source": "https://github.com/api-platform/serializer/tree/v4.2.15" }, - "time": "2026-01-12T13:36:15+00:00" + "time": "2026-01-26T15:38:30+00:00" }, { "name": "api-platform/state", - "version": "v4.2.14", + "version": "v4.2.15", "source": { "type": "git", "url": "https://github.com/api-platform/state.git", - "reference": "fa3e7b41bcb54e7ba6d3078de224620e422d6732" + "reference": "89c0999206b4885c2e55204751b4db07061f3fd3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/state/zipball/fa3e7b41bcb54e7ba6d3078de224620e422d6732", - "reference": "fa3e7b41bcb54e7ba6d3078de224620e422d6732", + "url": "https://api.github.com/repos/api-platform/state/zipball/89c0999206b4885c2e55204751b4db07061f3fd3", + "reference": "89c0999206b4885c2e55204751b4db07061f3fd3", "shasum": "" }, "require": { @@ -1990,22 +1990,22 @@ "swagger" ], "support": { - "source": "https://github.com/api-platform/state/tree/v4.2.14" + "source": "https://github.com/api-platform/state/tree/v4.2.15" }, - "time": "2026-01-12T13:36:15+00:00" + "time": "2026-01-26T15:38:30+00:00" }, { "name": "api-platform/symfony", - "version": "v4.2.14", + "version": "v4.2.15", "source": { "type": "git", "url": "https://github.com/api-platform/symfony.git", - "reference": "31539dc26bd88f54e43d2d8a24613ff988307da1" + "reference": "93fdcbe189a1866412f5da04e26fa5615e99b210" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/symfony/zipball/31539dc26bd88f54e43d2d8a24613ff988307da1", - "reference": "31539dc26bd88f54e43d2d8a24613ff988307da1", + "url": "https://api.github.com/repos/api-platform/symfony/zipball/93fdcbe189a1866412f5da04e26fa5615e99b210", + "reference": "93fdcbe189a1866412f5da04e26fa5615e99b210", "shasum": "" }, "require": { @@ -2118,22 +2118,22 @@ "symfony" ], "support": { - "source": "https://github.com/api-platform/symfony/tree/v4.2.14" + "source": "https://github.com/api-platform/symfony/tree/v4.2.15" }, - "time": "2026-01-23T14:24:03+00:00" + "time": "2026-01-30T13:31:50+00:00" }, { "name": "api-platform/validator", - "version": "v4.2.14", + "version": "v4.2.15", "source": { "type": "git", "url": "https://github.com/api-platform/validator.git", - "reference": "346a5916d9706da9b0981ebec3d6278802e96ca9" + "reference": "22968964145b3fe542b5885f6a2e74d77e7e28c3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/validator/zipball/346a5916d9706da9b0981ebec3d6278802e96ca9", - "reference": "346a5916d9706da9b0981ebec3d6278802e96ca9", + "url": "https://api.github.com/repos/api-platform/validator/zipball/22968964145b3fe542b5885f6a2e74d77e7e28c3", + "reference": "22968964145b3fe542b5885f6a2e74d77e7e28c3", "shasum": "" }, "require": { @@ -2194,9 +2194,9 @@ "validator" ], "support": { - "source": "https://github.com/api-platform/validator/tree/v4.2.14" + "source": "https://github.com/api-platform/validator/tree/v4.2.15" }, - "time": "2026-01-16T13:22:15+00:00" + "time": "2026-01-26T15:45:40+00:00" }, { "name": "beberlei/assert", @@ -3352,16 +3352,16 @@ }, { "name": "doctrine/event-manager", - "version": "2.1.0", + "version": "2.1.1", "source": { "type": "git", "url": "https://github.com/doctrine/event-manager.git", - "reference": "c07799fcf5ad362050960a0fd068dded40b1e312" + "reference": "dda33921b198841ca8dbad2eaa5d4d34769d18cf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/event-manager/zipball/c07799fcf5ad362050960a0fd068dded40b1e312", - "reference": "c07799fcf5ad362050960a0fd068dded40b1e312", + "url": "https://api.github.com/repos/doctrine/event-manager/zipball/dda33921b198841ca8dbad2eaa5d4d34769d18cf", + "reference": "dda33921b198841ca8dbad2eaa5d4d34769d18cf", "shasum": "" }, "require": { @@ -3423,7 +3423,7 @@ ], "support": { "issues": "https://github.com/doctrine/event-manager/issues", - "source": "https://github.com/doctrine/event-manager/tree/2.1.0" + "source": "https://github.com/doctrine/event-manager/tree/2.1.1" }, "funding": [ { @@ -3439,7 +3439,7 @@ "type": "tidelift" } ], - "time": "2026-01-17T22:40:21+00:00" + "time": "2026-01-29T07:11:08+00:00" }, { "name": "doctrine/inflector", @@ -3783,16 +3783,16 @@ }, { "name": "doctrine/orm", - "version": "3.6.1", + "version": "3.6.2", "source": { "type": "git", "url": "https://github.com/doctrine/orm.git", - "reference": "2148940290e4c44b9101095707e71fb590832fa5" + "reference": "4262eb495b4d2a53b45de1ac58881e0091f2970f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/orm/zipball/2148940290e4c44b9101095707e71fb590832fa5", - "reference": "2148940290e4c44b9101095707e71fb590832fa5", + "url": "https://api.github.com/repos/doctrine/orm/zipball/4262eb495b4d2a53b45de1ac58881e0091f2970f", + "reference": "4262eb495b4d2a53b45de1ac58881e0091f2970f", "shasum": "" }, "require": { @@ -3865,9 +3865,9 @@ ], "support": { "issues": "https://github.com/doctrine/orm/issues", - "source": "https://github.com/doctrine/orm/tree/3.6.1" + "source": "https://github.com/doctrine/orm/tree/3.6.2" }, - "time": "2026-01-09T05:28:15+00:00" + "time": "2026-01-30T21:41:41+00:00" }, { "name": "doctrine/persistence", @@ -8593,16 +8593,16 @@ }, { "name": "phpstan/phpdoc-parser", - "version": "2.3.1", + "version": "2.3.2", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "16dbf9937da8d4528ceb2145c9c7c0bd29e26374" + "reference": "a004701b11273a26cd7955a61d67a7f1e525a45a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/16dbf9937da8d4528ceb2145c9c7c0bd29e26374", - "reference": "16dbf9937da8d4528ceb2145c9c7c0bd29e26374", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/a004701b11273a26cd7955a61d67a7f1e525a45a", + "reference": "a004701b11273a26cd7955a61d67a7f1e525a45a", "shasum": "" }, "require": { @@ -8634,9 +8634,9 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.1" + "source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.2" }, - "time": "2026-01-12T11:33:04+00:00" + "time": "2026-01-25T14:56:51+00:00" }, { "name": "psr/cache", @@ -10290,16 +10290,16 @@ }, { "name": "symfony/cache", - "version": "v7.4.4", + "version": "v7.4.5", "source": { "type": "git", "url": "https://github.com/symfony/cache.git", - "reference": "67ca35eaa52dd9c1f07a42d459b5a2544dd29b34" + "reference": "8dde98d5a4123b53877aca493f9be57b333f14bd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/cache/zipball/67ca35eaa52dd9c1f07a42d459b5a2544dd29b34", - "reference": "67ca35eaa52dd9c1f07a42d459b5a2544dd29b34", + "url": "https://api.github.com/repos/symfony/cache/zipball/8dde98d5a4123b53877aca493f9be57b333f14bd", + "reference": "8dde98d5a4123b53877aca493f9be57b333f14bd", "shasum": "" }, "require": { @@ -10370,7 +10370,7 @@ "psr6" ], "support": { - "source": "https://github.com/symfony/cache/tree/v7.4.4" + "source": "https://github.com/symfony/cache/tree/v7.4.5" }, "funding": [ { @@ -10390,7 +10390,7 @@ "type": "tidelift" } ], - "time": "2026-01-23T12:59:19+00:00" + "time": "2026-01-27T16:16:02+00:00" }, { "name": "symfony/cache-contracts", @@ -10794,16 +10794,16 @@ }, { "name": "symfony/dependency-injection", - "version": "v7.4.4", + "version": "v7.4.5", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "dbbaba1cc65ccfa29106e931f68b51cd2f4b32bb" + "reference": "76a02cddca45a5254479ad68f9fa274ead0a7ef2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/dbbaba1cc65ccfa29106e931f68b51cd2f4b32bb", - "reference": "dbbaba1cc65ccfa29106e931f68b51cd2f4b32bb", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/76a02cddca45a5254479ad68f9fa274ead0a7ef2", + "reference": "76a02cddca45a5254479ad68f9fa274ead0a7ef2", "shasum": "" }, "require": { @@ -10854,7 +10854,7 @@ "description": "Allows you to standardize and centralize the way objects are constructed in your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dependency-injection/tree/v7.4.4" + "source": "https://github.com/symfony/dependency-injection/tree/v7.4.5" }, "funding": [ { @@ -10874,7 +10874,7 @@ "type": "tidelift" } ], - "time": "2026-01-23T12:59:19+00:00" + "time": "2026-01-27T16:16:02+00:00" }, { "name": "symfony/deprecation-contracts", @@ -11589,16 +11589,16 @@ }, { "name": "symfony/finder", - "version": "v7.4.4", + "version": "v7.4.5", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "01b24a145bbeaa7141e75887ec904c34a6728a5f" + "reference": "ad4daa7c38668dcb031e63bc99ea9bd42196a2cb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/01b24a145bbeaa7141e75887ec904c34a6728a5f", - "reference": "01b24a145bbeaa7141e75887ec904c34a6728a5f", + "url": "https://api.github.com/repos/symfony/finder/zipball/ad4daa7c38668dcb031e63bc99ea9bd42196a2cb", + "reference": "ad4daa7c38668dcb031e63bc99ea9bd42196a2cb", "shasum": "" }, "require": { @@ -11633,7 +11633,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v7.4.4" + "source": "https://github.com/symfony/finder/tree/v7.4.5" }, "funding": [ { @@ -11653,7 +11653,7 @@ "type": "tidelift" } ], - "time": "2026-01-12T12:19:02+00:00" + "time": "2026-01-26T15:07:59+00:00" }, { "name": "symfony/flex", @@ -11833,16 +11833,16 @@ }, { "name": "symfony/framework-bundle", - "version": "v7.4.4", + "version": "v7.4.5", "source": { "type": "git", "url": "https://github.com/symfony/framework-bundle.git", - "reference": "71fffd9f6cf8df1e2ee311176c85a10eddfdb08c" + "reference": "dcf89ca6712d9e1b5d3f14dea0e1c2685a05d1cd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/71fffd9f6cf8df1e2ee311176c85a10eddfdb08c", - "reference": "71fffd9f6cf8df1e2ee311176c85a10eddfdb08c", + "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/dcf89ca6712d9e1b5d3f14dea0e1c2685a05d1cd", + "reference": "dcf89ca6712d9e1b5d3f14dea0e1c2685a05d1cd", "shasum": "" }, "require": { @@ -11865,8 +11865,8 @@ }, "conflict": { "doctrine/persistence": "<1.3", - "phpdocumentor/reflection-docblock": "<3.2.2", - "phpdocumentor/type-resolver": "<1.4.0", + "phpdocumentor/reflection-docblock": "<5.2|>=6", + "phpdocumentor/type-resolver": "<1.5.1", "symfony/asset": "<6.4", "symfony/asset-mapper": "<6.4", "symfony/clock": "<6.4", @@ -11898,7 +11898,7 @@ "require-dev": { "doctrine/persistence": "^1.3|^2|^3", "dragonmantank/cron-expression": "^3.1", - "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", + "phpdocumentor/reflection-docblock": "^5.2", "seld/jsonlint": "^1.10", "symfony/asset": "^6.4|^7.0|^8.0", "symfony/asset-mapper": "^6.4|^7.0|^8.0", @@ -11967,7 +11967,7 @@ "description": "Provides a tight integration between Symfony components and the Symfony full-stack framework", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/framework-bundle/tree/v7.4.4" + "source": "https://github.com/symfony/framework-bundle/tree/v7.4.5" }, "funding": [ { @@ -11987,20 +11987,20 @@ "type": "tidelift" } ], - "time": "2026-01-12T12:19:02+00:00" + "time": "2026-01-27T08:59:58+00:00" }, { "name": "symfony/http-client", - "version": "v7.4.4", + "version": "v7.4.5", "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "d63c23357d74715a589454c141c843f0172bec6c" + "reference": "84bb634857a893cc146cceb467e31b3f02c5fe9f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/d63c23357d74715a589454c141c843f0172bec6c", - "reference": "d63c23357d74715a589454c141c843f0172bec6c", + "url": "https://api.github.com/repos/symfony/http-client/zipball/84bb634857a893cc146cceb467e31b3f02c5fe9f", + "reference": "84bb634857a893cc146cceb467e31b3f02c5fe9f", "shasum": "" }, "require": { @@ -12068,7 +12068,7 @@ "http" ], "support": { - "source": "https://github.com/symfony/http-client/tree/v7.4.4" + "source": "https://github.com/symfony/http-client/tree/v7.4.5" }, "funding": [ { @@ -12088,7 +12088,7 @@ "type": "tidelift" } ], - "time": "2026-01-23T16:34:22+00:00" + "time": "2026-01-27T16:16:02+00:00" }, { "name": "symfony/http-client-contracts", @@ -12170,16 +12170,16 @@ }, { "name": "symfony/http-foundation", - "version": "v7.4.4", + "version": "v7.4.5", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "977a554a34cf8edc95ca351fbecb1bb1ad05cc94" + "reference": "446d0db2b1f21575f1284b74533e425096abdfb6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/977a554a34cf8edc95ca351fbecb1bb1ad05cc94", - "reference": "977a554a34cf8edc95ca351fbecb1bb1ad05cc94", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/446d0db2b1f21575f1284b74533e425096abdfb6", + "reference": "446d0db2b1f21575f1284b74533e425096abdfb6", "shasum": "" }, "require": { @@ -12228,7 +12228,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v7.4.4" + "source": "https://github.com/symfony/http-foundation/tree/v7.4.5" }, "funding": [ { @@ -12248,20 +12248,20 @@ "type": "tidelift" } ], - "time": "2026-01-09T12:14:21+00:00" + "time": "2026-01-27T16:16:02+00:00" }, { "name": "symfony/http-kernel", - "version": "v7.4.4", + "version": "v7.4.5", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "48b067768859f7b68acf41dfb857a5a4be00acdd" + "reference": "229eda477017f92bd2ce7615d06222ec0c19e82a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/48b067768859f7b68acf41dfb857a5a4be00acdd", - "reference": "48b067768859f7b68acf41dfb857a5a4be00acdd", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/229eda477017f92bd2ce7615d06222ec0c19e82a", + "reference": "229eda477017f92bd2ce7615d06222ec0c19e82a", "shasum": "" }, "require": { @@ -12347,7 +12347,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v7.4.4" + "source": "https://github.com/symfony/http-kernel/tree/v7.4.5" }, "funding": [ { @@ -12367,7 +12367,7 @@ "type": "tidelift" } ], - "time": "2026-01-24T22:13:01+00:00" + "time": "2026-01-28T10:33:42+00:00" }, { "name": "symfony/intl", @@ -12545,16 +12545,16 @@ }, { "name": "symfony/mime", - "version": "v7.4.4", + "version": "v7.4.5", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "40945014c0a9471ccfe19673c54738fa19367a3c" + "reference": "b18c7e6e9eee1e19958138df10412f3c4c316148" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/40945014c0a9471ccfe19673c54738fa19367a3c", - "reference": "40945014c0a9471ccfe19673c54738fa19367a3c", + "url": "https://api.github.com/repos/symfony/mime/zipball/b18c7e6e9eee1e19958138df10412f3c4c316148", + "reference": "b18c7e6e9eee1e19958138df10412f3c4c316148", "shasum": "" }, "require": { @@ -12565,15 +12565,15 @@ }, "conflict": { "egulias/email-validator": "~3.0.0", - "phpdocumentor/reflection-docblock": "<3.2.2", - "phpdocumentor/type-resolver": "<1.4.0", + "phpdocumentor/reflection-docblock": "<5.2|>=6", + "phpdocumentor/type-resolver": "<1.5.1", "symfony/mailer": "<6.4", "symfony/serializer": "<6.4.3|>7.0,<7.0.3" }, "require-dev": { "egulias/email-validator": "^2.1.10|^3.1|^4", "league/html-to-markdown": "^5.0", - "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", + "phpdocumentor/reflection-docblock": "^5.2", "symfony/dependency-injection": "^6.4|^7.0|^8.0", "symfony/process": "^6.4|^7.0|^8.0", "symfony/property-access": "^6.4|^7.0|^8.0", @@ -12610,7 +12610,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v7.4.4" + "source": "https://github.com/symfony/mime/tree/v7.4.5" }, "funding": [ { @@ -12630,7 +12630,7 @@ "type": "tidelift" } ], - "time": "2026-01-08T16:12:55+00:00" + "time": "2026-01-27T08:59:58+00:00" }, { "name": "symfony/monolog-bridge", @@ -13692,16 +13692,16 @@ }, { "name": "symfony/process", - "version": "v7.4.4", + "version": "v7.4.5", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "626f07a53f4b4e2f00e11824cc29f928d797783b" + "reference": "608476f4604102976d687c483ac63a79ba18cc97" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/626f07a53f4b4e2f00e11824cc29f928d797783b", - "reference": "626f07a53f4b4e2f00e11824cc29f928d797783b", + "url": "https://api.github.com/repos/symfony/process/zipball/608476f4604102976d687c483ac63a79ba18cc97", + "reference": "608476f4604102976d687c483ac63a79ba18cc97", "shasum": "" }, "require": { @@ -13733,7 +13733,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v7.4.4" + "source": "https://github.com/symfony/process/tree/v7.4.5" }, "funding": [ { @@ -13753,7 +13753,7 @@ "type": "tidelift" } ], - "time": "2026-01-20T09:23:51+00:00" + "time": "2026-01-26T15:07:59+00:00" }, { "name": "symfony/property-access", @@ -13838,16 +13838,16 @@ }, { "name": "symfony/property-info", - "version": "v7.4.4", + "version": "v7.4.5", "source": { "type": "git", "url": "https://github.com/symfony/property-info.git", - "reference": "b5305f3bc5727d0395e9681237e870ed5a5d21ae" + "reference": "1c9d326bd69602561e2ea467a16c09b5972eee21" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/property-info/zipball/b5305f3bc5727d0395e9681237e870ed5a5d21ae", - "reference": "b5305f3bc5727d0395e9681237e870ed5a5d21ae", + "url": "https://api.github.com/repos/symfony/property-info/zipball/1c9d326bd69602561e2ea467a16c09b5972eee21", + "reference": "1c9d326bd69602561e2ea467a16c09b5972eee21", "shasum": "" }, "require": { @@ -13857,7 +13857,7 @@ "symfony/type-info": "~7.3.10|^7.4.4|^8.0.4" }, "conflict": { - "phpdocumentor/reflection-docblock": "<5.2", + "phpdocumentor/reflection-docblock": "<5.2|>=6", "phpdocumentor/type-resolver": "<1.5.1", "symfony/cache": "<6.4", "symfony/dependency-injection": "<6.4", @@ -13904,7 +13904,7 @@ "validator" ], "support": { - "source": "https://github.com/symfony/property-info/tree/v7.4.4" + "source": "https://github.com/symfony/property-info/tree/v7.4.5" }, "funding": [ { @@ -13924,7 +13924,7 @@ "type": "tidelift" } ], - "time": "2026-01-23T10:51:15+00:00" + "time": "2026-01-27T16:16:02+00:00" }, { "name": "symfony/psr-http-message-bridge", @@ -14016,16 +14016,16 @@ }, { "name": "symfony/rate-limiter", - "version": "v7.4.4", + "version": "v7.4.5", "source": { "type": "git", "url": "https://github.com/symfony/rate-limiter.git", - "reference": "7337fff8629956d9ffed05c3fd241d2a42ddfa20" + "reference": "7e275c57293cd2d894e126cc68855ecd82bcd173" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/rate-limiter/zipball/7337fff8629956d9ffed05c3fd241d2a42ddfa20", - "reference": "7337fff8629956d9ffed05c3fd241d2a42ddfa20", + "url": "https://api.github.com/repos/symfony/rate-limiter/zipball/7e275c57293cd2d894e126cc68855ecd82bcd173", + "reference": "7e275c57293cd2d894e126cc68855ecd82bcd173", "shasum": "" }, "require": { @@ -14066,7 +14066,7 @@ "rate-limiter" ], "support": { - "source": "https://github.com/symfony/rate-limiter/tree/v7.4.4" + "source": "https://github.com/symfony/rate-limiter/tree/v7.4.5" }, "funding": [ { @@ -14086,7 +14086,7 @@ "type": "tidelift" } ], - "time": "2026-01-08T16:12:55+00:00" + "time": "2026-01-27T16:16:02+00:00" }, { "name": "symfony/routing", @@ -14627,16 +14627,16 @@ }, { "name": "symfony/serializer", - "version": "v7.4.4", + "version": "v7.4.5", "source": { "type": "git", "url": "https://github.com/symfony/serializer.git", - "reference": "3b9a5d5c941a2a6e2a7dbe0e63fc3161888a5cd4" + "reference": "480cd1237c98ab1219c20945b92c9d4480a44f47" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/serializer/zipball/3b9a5d5c941a2a6e2a7dbe0e63fc3161888a5cd4", - "reference": "3b9a5d5c941a2a6e2a7dbe0e63fc3161888a5cd4", + "url": "https://api.github.com/repos/symfony/serializer/zipball/480cd1237c98ab1219c20945b92c9d4480a44f47", + "reference": "480cd1237c98ab1219c20945b92c9d4480a44f47", "shasum": "" }, "require": { @@ -14646,8 +14646,8 @@ "symfony/polyfill-php84": "^1.30" }, "conflict": { - "phpdocumentor/reflection-docblock": "<3.2.2", - "phpdocumentor/type-resolver": "<1.4.0", + "phpdocumentor/reflection-docblock": "<5.2|>=6", + "phpdocumentor/type-resolver": "<1.5.1", "symfony/dependency-injection": "<6.4", "symfony/property-access": "<6.4", "symfony/property-info": "<6.4", @@ -14656,7 +14656,7 @@ "symfony/yaml": "<6.4" }, "require-dev": { - "phpdocumentor/reflection-docblock": "^3.2|^4.0|^5.0", + "phpdocumentor/reflection-docblock": "^5.2", "phpstan/phpdoc-parser": "^1.0|^2.0", "seld/jsonlint": "^1.10", "symfony/cache": "^6.4|^7.0|^8.0", @@ -14706,7 +14706,7 @@ "description": "Handles serializing and deserializing data structures, including object graphs, into array structures or other formats like XML and JSON.", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/serializer/tree/v7.4.4" + "source": "https://github.com/symfony/serializer/tree/v7.4.5" }, "funding": [ { @@ -14726,7 +14726,7 @@ "type": "tidelift" } ], - "time": "2026-01-23T10:51:15+00:00" + "time": "2026-01-27T08:59:58+00:00" }, { "name": "symfony/service-contracts", @@ -15229,16 +15229,16 @@ }, { "name": "symfony/twig-bridge", - "version": "v7.4.4", + "version": "v7.4.5", "source": { "type": "git", "url": "https://github.com/symfony/twig-bridge.git", - "reference": "23c337a975c1527a4b91199f795abb62ede5238f" + "reference": "f2dd26b604e856476ef7e0efa4568bc07eb7ddc8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/twig-bridge/zipball/23c337a975c1527a4b91199f795abb62ede5238f", - "reference": "23c337a975c1527a4b91199f795abb62ede5238f", + "url": "https://api.github.com/repos/symfony/twig-bridge/zipball/f2dd26b604e856476ef7e0efa4568bc07eb7ddc8", + "reference": "f2dd26b604e856476ef7e0efa4568bc07eb7ddc8", "shasum": "" }, "require": { @@ -15248,8 +15248,8 @@ "twig/twig": "^3.21" }, "conflict": { - "phpdocumentor/reflection-docblock": "<3.2.2", - "phpdocumentor/type-resolver": "<1.4.0", + "phpdocumentor/reflection-docblock": "<5.2|>=6", + "phpdocumentor/type-resolver": "<1.5.1", "symfony/console": "<6.4", "symfony/form": "<6.4.32|>7,<7.3.10|>7.4,<7.4.4|>8.0,<8.0.4", "symfony/http-foundation": "<6.4", @@ -15262,7 +15262,7 @@ "require-dev": { "egulias/email-validator": "^2.1.10|^3|^4", "league/html-to-markdown": "^5.0", - "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", + "phpdocumentor/reflection-docblock": "^5.2", "symfony/asset": "^6.4|^7.0|^8.0", "symfony/asset-mapper": "^6.4|^7.0|^8.0", "symfony/console": "^6.4|^7.0|^8.0", @@ -15320,7 +15320,7 @@ "description": "Provides integration for Twig with various Symfony components", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/twig-bridge/tree/v7.4.4" + "source": "https://github.com/symfony/twig-bridge/tree/v7.4.5" }, "funding": [ { @@ -15340,7 +15340,7 @@ "type": "tidelift" } ], - "time": "2026-01-07T10:07:42+00:00" + "time": "2026-01-27T08:59:58+00:00" }, { "name": "symfony/twig-bundle", @@ -15779,16 +15779,16 @@ }, { "name": "symfony/validator", - "version": "v7.4.4", + "version": "v7.4.5", "source": { "type": "git", "url": "https://github.com/symfony/validator.git", - "reference": "64d763109518ea5f85ab32efe28eb8278ae5d502" + "reference": "fcec92c40df1c93507857da08226005573b655c6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/validator/zipball/64d763109518ea5f85ab32efe28eb8278ae5d502", - "reference": "64d763109518ea5f85ab32efe28eb8278ae5d502", + "url": "https://api.github.com/repos/symfony/validator/zipball/fcec92c40df1c93507857da08226005573b655c6", + "reference": "fcec92c40df1c93507857da08226005573b655c6", "shasum": "" }, "require": { @@ -15859,7 +15859,7 @@ "description": "Provides tools to validate values", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/validator/tree/v7.4.4" + "source": "https://github.com/symfony/validator/tree/v7.4.5" }, "funding": [ { @@ -15879,7 +15879,7 @@ "type": "tidelift" } ], - "time": "2026-01-08T22:32:07+00:00" + "time": "2026-01-27T08:59:58+00:00" }, { "name": "symfony/var-dumper", @@ -18276,11 +18276,11 @@ }, { "name": "phpstan/phpstan", - "version": "2.1.37", + "version": "2.1.38", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/28cd424c5ea984128c95cfa7ea658808e8954e49", - "reference": "28cd424c5ea984128c95cfa7ea658808e8954e49", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/dfaf1f530e1663aa167bc3e52197adb221582629", + "reference": "dfaf1f530e1663aa167bc3e52197adb221582629", "shasum": "" }, "require": { @@ -18325,20 +18325,20 @@ "type": "github" } ], - "time": "2026-01-24T08:21:55+00:00" + "time": "2026-01-30T17:12:46+00:00" }, { "name": "phpstan/phpstan-doctrine", - "version": "2.0.13", + "version": "2.0.14", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-doctrine.git", - "reference": "2d2ad04a0ac14ac52e21ad47ec67a54a14355c1f" + "reference": "70cd3e82fef49171163ff682a89cfe793d88581c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-doctrine/zipball/2d2ad04a0ac14ac52e21ad47ec67a54a14355c1f", - "reference": "2d2ad04a0ac14ac52e21ad47ec67a54a14355c1f", + "url": "https://api.github.com/repos/phpstan/phpstan-doctrine/zipball/70cd3e82fef49171163ff682a89cfe793d88581c", + "reference": "70cd3e82fef49171163ff682a89cfe793d88581c", "shasum": "" }, "require": { @@ -18396,22 +18396,22 @@ "description": "Doctrine extensions for PHPStan", "support": { "issues": "https://github.com/phpstan/phpstan-doctrine/issues", - "source": "https://github.com/phpstan/phpstan-doctrine/tree/2.0.13" + "source": "https://github.com/phpstan/phpstan-doctrine/tree/2.0.14" }, - "time": "2026-01-18T16:15:40+00:00" + "time": "2026-01-25T14:56:09+00:00" }, { "name": "phpstan/phpstan-strict-rules", - "version": "2.0.7", + "version": "2.0.8", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-strict-rules.git", - "reference": "d6211c46213d4181054b3d77b10a5c5cb0d59538" + "reference": "1ed9e626a37f7067b594422411539aa807190573" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/d6211c46213d4181054b3d77b10a5c5cb0d59538", - "reference": "d6211c46213d4181054b3d77b10a5c5cb0d59538", + "url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/1ed9e626a37f7067b594422411539aa807190573", + "reference": "1ed9e626a37f7067b594422411539aa807190573", "shasum": "" }, "require": { @@ -18444,9 +18444,9 @@ "description": "Extra strict and opinionated rules for PHPStan", "support": { "issues": "https://github.com/phpstan/phpstan-strict-rules/issues", - "source": "https://github.com/phpstan/phpstan-strict-rules/tree/2.0.7" + "source": "https://github.com/phpstan/phpstan-strict-rules/tree/2.0.8" }, - "time": "2025-09-26T11:19:08+00:00" + "time": "2026-01-27T08:10:25+00:00" }, { "name": "phpstan/phpstan-symfony", @@ -18856,16 +18856,16 @@ }, { "name": "phpunit/phpunit", - "version": "11.5.49", + "version": "11.5.50", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "4f1750675ba411dd6c2d5fa8a3cca07f6742020e" + "reference": "fdfc727f0fcacfeb8fcb30c7e5da173125b58be3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/4f1750675ba411dd6c2d5fa8a3cca07f6742020e", - "reference": "4f1750675ba411dd6c2d5fa8a3cca07f6742020e", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/fdfc727f0fcacfeb8fcb30c7e5da173125b58be3", + "reference": "fdfc727f0fcacfeb8fcb30c7e5da173125b58be3", "shasum": "" }, "require": { @@ -18937,7 +18937,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.49" + "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.50" }, "funding": [ { @@ -18961,20 +18961,20 @@ "type": "tidelift" } ], - "time": "2026-01-24T16:09:28+00:00" + "time": "2026-01-27T05:59:18+00:00" }, { "name": "rector/rector", - "version": "2.3.4", + "version": "2.3.5", "source": { "type": "git", "url": "https://github.com/rectorphp/rector.git", - "reference": "9227d7a24b0f23ae941057509364f948d5da9ab2" + "reference": "9442f4037de6a5347ae157fe8e6c7cda9d909070" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/rectorphp/rector/zipball/9227d7a24b0f23ae941057509364f948d5da9ab2", - "reference": "9227d7a24b0f23ae941057509364f948d5da9ab2", + "url": "https://api.github.com/repos/rectorphp/rector/zipball/9442f4037de6a5347ae157fe8e6c7cda9d909070", + "reference": "9442f4037de6a5347ae157fe8e6c7cda9d909070", "shasum": "" }, "require": { @@ -19013,7 +19013,7 @@ ], "support": { "issues": "https://github.com/rectorphp/rector/issues", - "source": "https://github.com/rectorphp/rector/tree/2.3.4" + "source": "https://github.com/rectorphp/rector/tree/2.3.5" }, "funding": [ { @@ -19021,7 +19021,7 @@ "type": "github" } ], - "time": "2026-01-21T14:49:03+00:00" + "time": "2026-01-28T15:22:48+00:00" }, { "name": "roave/security-advisories", @@ -19029,12 +19029,12 @@ "source": { "type": "git", "url": "https://github.com/Roave/SecurityAdvisories.git", - "reference": "8e1e81cec2f088871c624d2adf767eb5e492ecdf" + "reference": "8457f2008fc6396be788162c4e04228028306534" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/8e1e81cec2f088871c624d2adf767eb5e492ecdf", - "reference": "8e1e81cec2f088871c624d2adf767eb5e492ecdf", + "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/8457f2008fc6396be788162c4e04228028306534", + "reference": "8457f2008fc6396be788162c4e04228028306534", "shasum": "" }, "conflict": { @@ -19252,7 +19252,7 @@ "ec-cube/ec-cube": "<2.4.4|>=2.11,<=2.17.1|>=3,<=3.0.18.0-patch4|>=4,<=4.1.2", "ecodev/newsletter": "<=4", "ectouch/ectouch": "<=2.7.2", - "egroupware/egroupware": "<23.1.20240624", + "egroupware/egroupware": "<23.1.20260113|>=26.0.20251208,<26.0.20260113", "elefant/cms": "<2.0.7", "elgg/elgg": "<3.3.24|>=4,<4.0.5", "elijaa/phpmemcacheadmin": "<=1.3", @@ -19522,7 +19522,7 @@ "mongodb/mongodb": ">=1,<1.9.2", "mongodb/mongodb-extension": "<1.21.2", "monolog/monolog": ">=1.8,<1.12", - "moodle/moodle": "<=5.1.1", + "moodle/moodle": "<4.4.12|>=4.5.0.0-beta,<4.5.8|>=5.0.0.0-beta,<5.0.4|>=5.1.0.0-beta,<5.1.1", "moonshine/moonshine": "<=3.12.5", "mos/cimage": "<0.7.19", "movim/moxl": ">=0.8,<=0.10", @@ -19627,7 +19627,7 @@ "phpseclib/phpseclib": "<2.0.47|>=3,<3.0.36", "phpservermon/phpservermon": "<3.6", "phpsysinfo/phpsysinfo": "<3.4.3", - "phpunit/phpunit": ">=4.8.19,<4.8.28|>=5.0.10,<5.6.3", + "phpunit/phpunit": "<8.5.52|>=9,<9.6.33|>=10,<10.5.62|>=11,<11.5.50|>=12,<12.5.8", "phpwhois/phpwhois": "<=4.2.5", "phpxmlrpc/extras": "<0.6.1", "phpxmlrpc/phpxmlrpc": "<4.9.2", @@ -19664,6 +19664,7 @@ "processwire/processwire": "<=3.0.246", "propel/propel": ">=2.0.0.0-alpha1,<=2.0.0.0-alpha7", "propel/propel1": ">=1,<=1.7.1", + "psy/psysh": "<=0.11.22|>=0.12,<=0.12.18", "pterodactyl/panel": "<1.12", "ptheofan/yii2-statemachine": ">=2.0.0.0-RC1-dev,<=2", "ptrofimov/beanstalk_console": "<1.7.14", @@ -19752,7 +19753,7 @@ "snipe/snipe-it": "<=8.3.4", "socalnick/scn-social-auth": "<1.15.2", "socialiteproviders/steam": "<1.1", - "solspace/craft-freeform": "<=5.14.6", + "solspace/craft-freeform": "<4.1.29|>=5,<=5.14.6", "soosyze/soosyze": "<=2", "spatie/browsershot": "<5.0.5", "spatie/image-optimizer": "<1.7.3", @@ -19806,7 +19807,7 @@ "symfony/phpunit-bridge": ">=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7", "symfony/polyfill": ">=1,<1.10", "symfony/polyfill-php55": ">=1,<1.10", - "symfony/process": "<5.4.46|>=6,<6.4.14|>=7,<7.1.7", + "symfony/process": "<5.4.51|>=6,<6.4.33|>=7,<7.1.7|>=7.3,<7.3.11|>=7.4,<7.4.5|>=8,<8.0.5", "symfony/proxy-manager-bridge": ">=2.7,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7", "symfony/routing": ">=2,<2.0.19", "symfony/runtime": ">=5.3,<5.4.46|>=6,<6.4.14|>=7,<7.1.7", @@ -19817,7 +19818,7 @@ "symfony/security-guard": ">=2.8,<3.4.48|>=4,<4.4.23|>=5,<5.2.8", "symfony/security-http": ">=2.3,<2.3.41|>=2.4,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.2.12|>=4.3,<4.3.8|>=4.4,<4.4.7|>=5,<5.0.7|>=5.1,<5.2.8|>=5.3,<5.4.47|>=6,<6.4.15|>=7,<7.1.8", "symfony/serializer": ">=2,<2.0.11|>=4.1,<4.4.35|>=5,<5.3.12", - "symfony/symfony": "<5.4.50|>=6,<6.4.29|>=7,<7.3.7", + "symfony/symfony": "<5.4.51|>=6,<6.4.33|>=7,<7.3.11|>=7.4,<7.4.5|>=8,<8.0.5", "symfony/translation": ">=2,<2.0.17", "symfony/twig-bridge": ">=2,<4.4.51|>=5,<5.4.31|>=6,<6.3.8", "symfony/ux-autocomplete": "<2.11.2", @@ -20036,7 +20037,7 @@ "type": "tidelift" } ], - "time": "2026-01-23T21:05:59+00:00" + "time": "2026-01-30T22:06:58+00:00" }, { "name": "sebastian/cli-parser", diff --git a/yarn.lock b/yarn.lock index 24c8d5be..abbc7d9c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -55,34 +55,34 @@ resolved "https://registry.yarnpkg.com/@algolia/autocomplete-theme-classic/-/autocomplete-theme-classic-1.19.4.tgz#7a0802e7c64dcc3584d5085e23a290a64ade4319" integrity sha512-/qE8BETNFbul4WrrUyBYgaaKcgFPk0Px9FDKADnr3HlIkXquRpcFHTxXK16jdwXb33yrcXaAVSQZRfUUSSnxVA== -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.28.6": - version "7.28.6" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.28.6.tgz#72499312ec58b1e2245ba4a4f550c132be4982f7" - integrity sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q== +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.28.6", "@babel/code-frame@^7.29.0": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.29.0.tgz#7cd7a59f15b3cc0dcd803038f7792712a7d0b15c" + integrity sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw== dependencies: "@babel/helper-validator-identifier" "^7.28.5" js-tokens "^4.0.0" picocolors "^1.1.1" -"@babel/compat-data@^7.28.6": - version "7.28.6" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.28.6.tgz#103f466803fa0f059e82ccac271475470570d74c" - integrity sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg== +"@babel/compat-data@^7.28.6", "@babel/compat-data@^7.29.0": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.29.0.tgz#00d03e8c0ac24dd9be942c5370990cbe1f17d88d" + integrity sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg== "@babel/core@^7.19.6": - version "7.28.6" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.28.6.tgz#531bf883a1126e53501ba46eb3bb414047af507f" - integrity sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw== + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.29.0.tgz#5286ad785df7f79d656e88ce86e650d16ca5f322" + integrity sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA== dependencies: - "@babel/code-frame" "^7.28.6" - "@babel/generator" "^7.28.6" + "@babel/code-frame" "^7.29.0" + "@babel/generator" "^7.29.0" "@babel/helper-compilation-targets" "^7.28.6" "@babel/helper-module-transforms" "^7.28.6" "@babel/helpers" "^7.28.6" - "@babel/parser" "^7.28.6" + "@babel/parser" "^7.29.0" "@babel/template" "^7.28.6" - "@babel/traverse" "^7.28.6" - "@babel/types" "^7.28.6" + "@babel/traverse" "^7.29.0" + "@babel/types" "^7.29.0" "@jridgewell/remapping" "^2.3.5" convert-source-map "^2.0.0" debug "^4.1.0" @@ -90,13 +90,13 @@ json5 "^2.2.3" semver "^6.3.1" -"@babel/generator@^7.28.6": - version "7.28.6" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.28.6.tgz#48dcc65d98fcc8626a48f72b62e263d25fc3c3f1" - integrity sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw== +"@babel/generator@^7.29.0": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.29.0.tgz#4cba5a76b3c71d8be31761b03329d5dc7768447f" + integrity sha512-vSH118/wwM/pLR38g/Sgk05sNtro6TlTJKuiMXDaZqPUfjTFcudpCOt00IhOfj+1BFAX+UFAlzCU+6WXr3GLFQ== dependencies: - "@babel/parser" "^7.28.6" - "@babel/types" "^7.28.6" + "@babel/parser" "^7.29.0" + "@babel/types" "^7.29.0" "@jridgewell/gen-mapping" "^0.3.12" "@jridgewell/trace-mapping" "^0.3.28" jsesc "^3.0.2" @@ -141,7 +141,7 @@ regexpu-core "^6.3.1" semver "^6.3.1" -"@babel/helper-define-polyfill-provider@^0.6.5", "@babel/helper-define-polyfill-provider@^0.6.6": +"@babel/helper-define-polyfill-provider@^0.6.6": version "0.6.6" resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.6.tgz#714dfe33d8bd710f556df59953720f6eeb6c1a14" integrity sha512-mOAsxeeKkUKayvZR3HeTYD/fICpCPLJrU5ZjelT/PA6WHtNDBOE436YiaEUvHN454bRM3CebhDsIpieCc4texA== @@ -173,7 +173,7 @@ "@babel/traverse" "^7.28.6" "@babel/types" "^7.28.6" -"@babel/helper-module-transforms@^7.27.1", "@babel/helper-module-transforms@^7.28.3", "@babel/helper-module-transforms@^7.28.6": +"@babel/helper-module-transforms@^7.27.1", "@babel/helper-module-transforms@^7.28.6": version "7.28.6" resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz#9312d9d9e56edc35aeb6e95c25d4106b50b9eb1e" integrity sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA== @@ -252,12 +252,12 @@ "@babel/template" "^7.28.6" "@babel/types" "^7.28.6" -"@babel/parser@^7.18.9", "@babel/parser@^7.28.6": - version "7.28.6" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.28.6.tgz#f01a8885b7fa1e56dd8a155130226cd698ef13fd" - integrity sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ== +"@babel/parser@^7.18.9", "@babel/parser@^7.28.6", "@babel/parser@^7.29.0": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.29.0.tgz#669ef345add7d057e92b7ed15f0bac07611831b6" + integrity sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww== dependencies: - "@babel/types" "^7.28.6" + "@babel/types" "^7.29.0" "@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.28.5": version "7.28.5" @@ -332,14 +332,14 @@ dependencies: "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-async-generator-functions@^7.28.6": - version "7.28.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.28.6.tgz#80cb86d3eaa2102e18ae90dd05ab87bdcad3877d" - integrity sha512-9knsChgsMzBV5Yh3kkhrZNxH3oCYAfMBkNNaVN4cP2RVlFPe8wYdwwcnOsAbkdDoV9UjFtOXWrWB52M8W4jNeA== +"@babel/plugin-transform-async-generator-functions@^7.29.0": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.29.0.tgz#63ed829820298f0bf143d5a4a68fb8c06ffd742f" + integrity sha512-va0VdWro4zlBr2JsXC+ofCPB2iG12wPtVGTWFx2WLDOM3nYQZZIGP82qku2eW/JR83sD+k2k+CsNtyEbUqhU6w== dependencies: "@babel/helper-plugin-utils" "^7.28.6" "@babel/helper-remap-async-to-generator" "^7.27.1" - "@babel/traverse" "^7.28.6" + "@babel/traverse" "^7.29.0" "@babel/plugin-transform-async-to-generator@^7.28.6": version "7.28.6" @@ -423,10 +423,10 @@ dependencies: "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-duplicate-named-capturing-groups-regex@^7.28.6": - version "7.28.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.28.6.tgz#e0c59ba54f1655dd682f2edf5f101b5910a8f6f3" - integrity sha512-5suVoXjC14lUN6ZL9OLKIHCNVWCrqGqlmEp/ixdXjvgnEl/kauLvvMO/Xw9NyMc95Joj1AeLVPVMvibBgSoFlA== +"@babel/plugin-transform-duplicate-named-capturing-groups-regex@^7.29.0": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.29.0.tgz#8014b8a6cfd0e7b92762724443bf0d2400f26df1" + integrity sha512-zBPcW2lFGxdiD8PUnPwJjag2J9otbcLQzvbiOzDxpYXyCuYX9agOwMPGn1prVH0a4qzhCKu24rlH4c1f7yA8rw== dependencies: "@babel/helper-create-regexp-features-plugin" "^7.28.5" "@babel/helper-plugin-utils" "^7.28.6" @@ -521,15 +521,15 @@ "@babel/helper-module-transforms" "^7.28.6" "@babel/helper-plugin-utils" "^7.28.6" -"@babel/plugin-transform-modules-systemjs@^7.28.5": - version "7.28.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.28.5.tgz#7439e592a92d7670dfcb95d0cbc04bd3e64801d2" - integrity sha512-vn5Jma98LCOeBy/KpeQhXcV2WZgaRUtjwQmjoBuLNlOmkg0fB5pdvYVeWRYI69wWKwK2cD1QbMiUQnoujWvrew== +"@babel/plugin-transform-modules-systemjs@^7.29.0": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.0.tgz#e458a95a17807c415924106a3ff188a3b8dee964" + integrity sha512-PrujnVFbOdUpw4UHiVwKvKRLMMic8+eC0CuNlxjsyZUiBjhFdPsewdXCkveh2KqBA9/waD0W1b4hXSOBQJezpQ== dependencies: - "@babel/helper-module-transforms" "^7.28.3" - "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-module-transforms" "^7.28.6" + "@babel/helper-plugin-utils" "^7.28.6" "@babel/helper-validator-identifier" "^7.28.5" - "@babel/traverse" "^7.28.5" + "@babel/traverse" "^7.29.0" "@babel/plugin-transform-modules-umd@^7.27.1": version "7.27.1" @@ -539,13 +539,13 @@ "@babel/helper-module-transforms" "^7.27.1" "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-named-capturing-groups-regex@^7.27.1": - version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.27.1.tgz#f32b8f7818d8fc0cc46ee20a8ef75f071af976e1" - integrity sha512-SstR5JYy8ddZvD6MhV0tM/j16Qds4mIpJTOd1Yu9J9pJjH93bxHECF7pgtc28XvkzTD6Pxcm/0Z73Hvk7kb3Ng== +"@babel/plugin-transform-named-capturing-groups-regex@^7.29.0": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.29.0.tgz#a26cd51e09c4718588fc4cce1c5d1c0152102d6a" + integrity sha512-1CZQA5KNAD6ZYQLPw7oi5ewtDNxH/2vuCh+6SmvgDfhumForvs8a1o9n0UrEoBD8HU4djO2yWngTQlXl1NDVEQ== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.27.1" - "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-create-regexp-features-plugin" "^7.28.5" + "@babel/helper-plugin-utils" "^7.28.6" "@babel/plugin-transform-new-target@^7.27.1": version "7.27.1" @@ -633,10 +633,10 @@ dependencies: "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-regenerator@^7.28.6": - version "7.28.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.6.tgz#6ca2ed5b76cff87980f96eaacfc2ce833e8e7a1b" - integrity sha512-eZhoEZHYQLL5uc1gS5e9/oTknS0sSSAtd5TkKMUp3J+S/CaUjagc0kOUPsEbDmMeva0nC3WWl4SxVY6+OBuxfw== +"@babel/plugin-transform-regenerator@^7.29.0": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.0.tgz#dec237cec1b93330876d6da9992c4abd42c9d18b" + integrity sha512-FijqlqMA7DmRdg/aINBSs04y8XNTYw/lr1gJ2WsmBnnaNw1iS43EPkJW+zK7z65auG3AWRFXWj+NcTQwYptUog== dependencies: "@babel/helper-plugin-utils" "^7.28.6" @@ -723,11 +723,11 @@ "@babel/helper-plugin-utils" "^7.28.6" "@babel/preset-env@^7.19.4": - version "7.28.6" - resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.28.6.tgz#b4586bb59d8c61be6c58997f4912e7ea6bd17178" - integrity sha512-GaTI4nXDrs7l0qaJ6Rg06dtOXTBCG6TMDB44zbqofCIC4PqC7SEvmFFtpxzCDw9W5aJ7RKVshgXTLvLdBFV/qw== + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.29.0.tgz#c55db400c515a303662faaefd2d87e796efa08d0" + integrity sha512-fNEdfc0yi16lt6IZo2Qxk3knHVdfMYX33czNb4v8yWhemoBhibCpQK/uYHtSKIiO+p/zd3+8fYVXhQdOVV608w== dependencies: - "@babel/compat-data" "^7.28.6" + "@babel/compat-data" "^7.29.0" "@babel/helper-compilation-targets" "^7.28.6" "@babel/helper-plugin-utils" "^7.28.6" "@babel/helper-validator-option" "^7.27.1" @@ -741,7 +741,7 @@ "@babel/plugin-syntax-import-attributes" "^7.28.6" "@babel/plugin-syntax-unicode-sets-regex" "^7.18.6" "@babel/plugin-transform-arrow-functions" "^7.27.1" - "@babel/plugin-transform-async-generator-functions" "^7.28.6" + "@babel/plugin-transform-async-generator-functions" "^7.29.0" "@babel/plugin-transform-async-to-generator" "^7.28.6" "@babel/plugin-transform-block-scoped-functions" "^7.27.1" "@babel/plugin-transform-block-scoping" "^7.28.6" @@ -752,7 +752,7 @@ "@babel/plugin-transform-destructuring" "^7.28.5" "@babel/plugin-transform-dotall-regex" "^7.28.6" "@babel/plugin-transform-duplicate-keys" "^7.27.1" - "@babel/plugin-transform-duplicate-named-capturing-groups-regex" "^7.28.6" + "@babel/plugin-transform-duplicate-named-capturing-groups-regex" "^7.29.0" "@babel/plugin-transform-dynamic-import" "^7.27.1" "@babel/plugin-transform-explicit-resource-management" "^7.28.6" "@babel/plugin-transform-exponentiation-operator" "^7.28.6" @@ -765,9 +765,9 @@ "@babel/plugin-transform-member-expression-literals" "^7.27.1" "@babel/plugin-transform-modules-amd" "^7.27.1" "@babel/plugin-transform-modules-commonjs" "^7.28.6" - "@babel/plugin-transform-modules-systemjs" "^7.28.5" + "@babel/plugin-transform-modules-systemjs" "^7.29.0" "@babel/plugin-transform-modules-umd" "^7.27.1" - "@babel/plugin-transform-named-capturing-groups-regex" "^7.27.1" + "@babel/plugin-transform-named-capturing-groups-regex" "^7.29.0" "@babel/plugin-transform-new-target" "^7.27.1" "@babel/plugin-transform-nullish-coalescing-operator" "^7.28.6" "@babel/plugin-transform-numeric-separator" "^7.28.6" @@ -779,7 +779,7 @@ "@babel/plugin-transform-private-methods" "^7.28.6" "@babel/plugin-transform-private-property-in-object" "^7.28.6" "@babel/plugin-transform-property-literals" "^7.27.1" - "@babel/plugin-transform-regenerator" "^7.28.6" + "@babel/plugin-transform-regenerator" "^7.29.0" "@babel/plugin-transform-regexp-modifiers" "^7.28.6" "@babel/plugin-transform-reserved-words" "^7.27.1" "@babel/plugin-transform-shorthand-properties" "^7.27.1" @@ -792,10 +792,10 @@ "@babel/plugin-transform-unicode-regex" "^7.27.1" "@babel/plugin-transform-unicode-sets-regex" "^7.28.6" "@babel/preset-modules" "0.1.6-no-external-plugins" - babel-plugin-polyfill-corejs2 "^0.4.14" - babel-plugin-polyfill-corejs3 "^0.13.0" - babel-plugin-polyfill-regenerator "^0.6.5" - core-js-compat "^3.43.0" + babel-plugin-polyfill-corejs2 "^0.4.15" + babel-plugin-polyfill-corejs3 "^0.14.0" + babel-plugin-polyfill-regenerator "^0.6.6" + core-js-compat "^3.48.0" semver "^6.3.1" "@babel/preset-modules@0.1.6-no-external-plugins": @@ -816,23 +816,23 @@ "@babel/parser" "^7.28.6" "@babel/types" "^7.28.6" -"@babel/traverse@^7.18.9", "@babel/traverse@^7.27.1", "@babel/traverse@^7.28.5", "@babel/traverse@^7.28.6": - version "7.28.6" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.28.6.tgz#871ddc79a80599a5030c53b1cc48cbe3a5583c2e" - integrity sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg== +"@babel/traverse@^7.18.9", "@babel/traverse@^7.27.1", "@babel/traverse@^7.28.5", "@babel/traverse@^7.28.6", "@babel/traverse@^7.29.0": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.29.0.tgz#f323d05001440253eead3c9c858adbe00b90310a" + integrity sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA== dependencies: - "@babel/code-frame" "^7.28.6" - "@babel/generator" "^7.28.6" + "@babel/code-frame" "^7.29.0" + "@babel/generator" "^7.29.0" "@babel/helper-globals" "^7.28.0" - "@babel/parser" "^7.28.6" + "@babel/parser" "^7.29.0" "@babel/template" "^7.28.6" - "@babel/types" "^7.28.6" + "@babel/types" "^7.29.0" debug "^4.3.1" -"@babel/types@^7.27.1", "@babel/types@^7.27.3", "@babel/types@^7.28.5", "@babel/types@^7.28.6", "@babel/types@^7.4.4": - version "7.28.6" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.28.6.tgz#c3e9377f1b155005bcc4c46020e7e394e13089df" - integrity sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg== +"@babel/types@^7.27.1", "@babel/types@^7.27.3", "@babel/types@^7.28.5", "@babel/types@^7.28.6", "@babel/types@^7.29.0", "@babel/types@^7.4.4": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.29.0.tgz#9f5b1e838c446e72cf3cd4b918152b8c605e37c7" + integrity sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A== dependencies: "@babel/helper-string-parser" "^7.27.1" "@babel/helper-validator-identifier" "^7.28.5" @@ -1850,9 +1850,9 @@ integrity sha512-eGeIqNOQpXoPAIP7tC1+1Yc1yl1xnwYqg+3mzqxyrbE5pg5YFBZcA6YoTiByJB6DKAEsiWtl6tjTJS4IYtbB7A== "@hotwired/turbo@^8.0.1": - version "8.0.21" - resolved "https://registry.yarnpkg.com/@hotwired/turbo/-/turbo-8.0.21.tgz#a3e80c01d70048200f64bbe3582b84f9bfac034e" - integrity sha512-fJTv3JnzFHeDxBb23esZSOhT4r142xf5o3lKMFMvzPC6AllkqbBKk5Yb31UZhtIsKQCwmO/pUQrtTUlYl5CHAQ== + version "8.0.23" + resolved "https://registry.yarnpkg.com/@hotwired/turbo/-/turbo-8.0.23.tgz#a6eebc9ab4a5faadae265a4cbec8cfcb5731e77c" + integrity sha512-GZ7cijxEZ6Ig71u7rD6LHaRv/wcE/hNsc+nEfiWOkLNqUgLOwo5MNGWOy5ZV9ZUDSiQx1no7YxjTNnT4O6//cQ== "@isaacs/balanced-match@^4.0.1": version "4.0.1" @@ -2169,9 +2169,9 @@ integrity sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA== "@types/node@*": - version "25.0.10" - resolved "https://registry.yarnpkg.com/@types/node/-/node-25.0.10.tgz#4864459c3c9459376b8b75fd051315071c8213e7" - integrity sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg== + version "25.1.0" + resolved "https://registry.yarnpkg.com/@types/node/-/node-25.1.0.tgz#95cc584f1f478301efc86de4f1867e5875e83571" + integrity sha512-t7frlewr6+cbx+9Ohpl0NOTKXZNV9xHRmNOvql47BFJKcEG1CxtxlPEEe+gR9uhVWM4DwhnvTF110mIL4yP9RA== dependencies: undici-types "~7.16.0" @@ -2575,7 +2575,7 @@ available-typed-arrays@^1.0.7: dependencies: find-up "^5.0.0" -babel-plugin-polyfill-corejs2@^0.4.14: +babel-plugin-polyfill-corejs2@^0.4.15: version "0.4.15" resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.15.tgz#808fa349686eea4741807cfaaa2aa3aa57ce120a" integrity sha512-hR3GwrRwHUfYwGfrisXPIDP3JcYfBrW7wKE7+Au6wDYl7fm/ka1NEII6kORzxNU556JjfidZeBsO10kYvtV1aw== @@ -2584,15 +2584,15 @@ babel-plugin-polyfill-corejs2@^0.4.14: "@babel/helper-define-polyfill-provider" "^0.6.6" semver "^6.3.1" -babel-plugin-polyfill-corejs3@^0.13.0: - version "0.13.0" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.13.0.tgz#bb7f6aeef7addff17f7602a08a6d19a128c30164" - integrity sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A== +babel-plugin-polyfill-corejs3@^0.14.0: + version "0.14.0" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.14.0.tgz#65b06cda48d6e447e1e926681f5a247c6ae2b9cf" + integrity sha512-AvDcMxJ34W4Wgy4KBIIePQTAOP1Ie2WFwkQp3dB7FQ/f0lI5+nM96zUnYEOE1P9sEg0es5VCP0HxiWu5fUHZAQ== dependencies: - "@babel/helper-define-polyfill-provider" "^0.6.5" - core-js-compat "^3.43.0" + "@babel/helper-define-polyfill-provider" "^0.6.6" + core-js-compat "^3.48.0" -babel-plugin-polyfill-regenerator@^0.6.5: +babel-plugin-polyfill-regenerator@^0.6.6: version "0.6.6" resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.6.tgz#69f5dd263cab933c42fe5ea05e83443b374bd4bf" integrity sha512-hYm+XLYRMvupxiQzrvXUj7YyvFFVfv5gI0R71AJzudg1g2AI2vyCPPIFEBjk162/wFzti3inBHo7isWFuEVS/A== @@ -2627,9 +2627,9 @@ base64-js@^1.1.2, base64-js@^1.3.0: integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== baseline-browser-mapping@^2.9.0: - version "2.9.18" - resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.9.18.tgz#c8281693035a9261b10d662a5379650a6c2d1ff7" - integrity sha512-e23vBV1ZLfjb9apvfPk4rHVu2ry6RIr2Wfs+O324okSidrX7pTAnEJPCh/O5BtRlr7QtZI7ktOP3vsqr7Z5XoA== + version "2.9.19" + resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz#3e508c43c46d961eb4d7d2e5b8d1dd0f9ee4f488" + integrity sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg== big.js@^5.2.2: version "5.2.2" @@ -2865,9 +2865,9 @@ chrome-trace-event@^1.0.2: integrity sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ== ci-info@^4.2.0: - version "4.3.1" - resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-4.3.1.tgz#355ad571920810b5623e11d40232f443f16f1daa" - integrity sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA== + version "4.4.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-4.4.0.tgz#7d54eff9f54b45b62401c26032696eb59c8bd18c" + integrity sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg== ckeditor5@47.4.0, ckeditor5@^47.0.0: version "47.4.0" @@ -3101,7 +3101,7 @@ convert-source-map@^2.0.0: resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== -core-js-compat@^3.43.0: +core-js-compat@^3.48.0: version "3.48.0" resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.48.0.tgz#7efbe1fc1cbad44008190462217cc5558adaeaa6" integrity sha512-OM4cAF3D6VtH/WkLtWvyNC56EZVXsZdU3iqaMG2B4WvYrlqU831pc4UtG5yp0sE9z8Y02wVN7PjW5Zf9Gt0f1Q== @@ -3165,18 +3165,18 @@ css-loader@^5.2.7: semver "^7.3.5" css-loader@^7.1.0: - version "7.1.2" - resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-7.1.2.tgz#64671541c6efe06b0e22e750503106bdd86880f8" - integrity sha512-6WvYYn7l/XEGN8Xu2vWFt9nVzrCn39vKyTEFf/ExEyoksJjjSZV/0/35XPlMbpnr6VGhZIUg5yJrL8tGfes/FA== + version "7.1.3" + resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-7.1.3.tgz#c0de715ceabe39b8531a85fcaf6734a430c4d99a" + integrity sha512-frbERmjT0UC5lMheWpJmMilnt9GEhbZJN/heUb7/zaJYeIzj5St9HvDcfshzzOqbsS+rYpMk++2SD3vGETDSyA== dependencies: icss-utils "^5.1.0" - postcss "^8.4.33" + postcss "^8.4.40" postcss-modules-extract-imports "^3.1.0" postcss-modules-local-by-default "^4.0.5" postcss-modules-scope "^3.2.0" postcss-modules-values "^4.0.0" postcss-value-parser "^4.2.0" - semver "^7.5.4" + semver "^7.6.3" css-minimizer-webpack-plugin@^7.0.0: version "7.0.4" @@ -3379,11 +3379,11 @@ data-view-byte-offset@^1.0.1: is-data-view "^1.0.1" datatables.net-bs5@^2, datatables.net-bs5@^2.0.0: - version "2.3.6" - resolved "https://registry.yarnpkg.com/datatables.net-bs5/-/datatables.net-bs5-2.3.6.tgz#88e9b015cb3d260f3e874f0f9ad16dc566b997da" - integrity sha512-oUNGjZrpNC2fY3l/6V4ijTC9kyVKU4Raons+RFmq2J7590rPn0c+5WAYKBx0evgW/CW7WfhStGBrU7+WJig6Og== + version "2.3.7" + resolved "https://registry.yarnpkg.com/datatables.net-bs5/-/datatables.net-bs5-2.3.7.tgz#ddef957ee23b03c2d4bc1d48735b39c6182e5d53" + integrity sha512-RiCEMpMXDBeMDwjSrMpmcXDU6mibRMuOn7Wk7k3SlOfLEY3FQHO7S2m+K7teXYeaNlCLyjJMU+6BUUwlBCpLFw== dependencies: - datatables.net "2.3.6" + datatables.net "2.3.7" jquery ">=1.7" datatables.net-buttons-bs5@^3.0.0: @@ -3438,18 +3438,18 @@ datatables.net-fixedheader@4.0.5: jquery ">=1.7" datatables.net-responsive-bs5@^3.0.0: - version "3.0.7" - resolved "https://registry.yarnpkg.com/datatables.net-responsive-bs5/-/datatables.net-responsive-bs5-3.0.7.tgz#aa9961d096a7443f59a871d55bf8a19e37a9e60e" - integrity sha512-M5VgAXMF7sa64GxFxVfyhiomYpvH/CRXhwoB+l13LaoDU6qtb6noOupFMtG7AVECrDar6UaKe38Frfqz3Pi0Kg== + version "3.0.8" + resolved "https://registry.yarnpkg.com/datatables.net-responsive-bs5/-/datatables.net-responsive-bs5-3.0.8.tgz#666e9dfbd14f330630660374edca5d645c3697d5" + integrity sha512-f0YTxv/HKWKXkOdutwDe3MmRM3AWf4Lxw7FjrgVc3H5+62emUnHep6cA9VwUcAAMywNqMYVndaKPyhAoeKUCyQ== dependencies: datatables.net-bs5 "^2" - datatables.net-responsive "3.0.7" + datatables.net-responsive "3.0.8" jquery ">=1.7" -datatables.net-responsive@3.0.7: - version "3.0.7" - resolved "https://registry.yarnpkg.com/datatables.net-responsive/-/datatables.net-responsive-3.0.7.tgz#7b57574bcfba105dc0827b77ec75b72b63e461fb" - integrity sha512-MngWU41M1LDDMjKFJ3rAHc4Zb3QhOysDTh+TfKE1ycrh5dpnKa1vobw2MKMMbvbx4q05OXZY9jtLSPIkaJRsuw== +datatables.net-responsive@3.0.8: + version "3.0.8" + resolved "https://registry.yarnpkg.com/datatables.net-responsive/-/datatables.net-responsive-3.0.8.tgz#c41d706c98442122e61a8fb9b02a8b2995cd487d" + integrity sha512-htslaX9g/9HFrJeyFQKEe/XJWpawPxpvy+M6vc/NkKQIrKhbxSoPc3phPqmlnZth6b9hgawqWDT0e0lwf5p+KA== dependencies: datatables.net "^2" jquery ">=1.7" @@ -3471,10 +3471,10 @@ datatables.net-select@3.1.3: datatables.net "^2" jquery ">=1.7" -datatables.net@2.3.6, datatables.net@^2, datatables.net@^2.0.0: - version "2.3.6" - resolved "https://registry.yarnpkg.com/datatables.net/-/datatables.net-2.3.6.tgz#a11be57a2b50d7231cae2980a8ff1df3c18b7b17" - integrity sha512-xQ/dCxrjfxM0XY70wSIzakkTZ6ghERwlLmAPyCnu8Sk5cyt9YvOVyOsFNOa/BZ/lM63Q3i2YSSvp/o7GXZGsbg== +datatables.net@2.3.7, datatables.net@^2, datatables.net@^2.0.0: + version "2.3.7" + resolved "https://registry.yarnpkg.com/datatables.net/-/datatables.net-2.3.7.tgz#3cd34f6f5d1f40a46b5a20a4ba32604bdbcd6738" + integrity sha512-AvsjG/Nkp6OxeyBKYZauemuzQCPogE1kOtKwG4sYjvdqGCSLiGaJagQwXv4YxG+ts5vaJr6qKGG9ec3g6vTo3w== dependencies: jquery ">=1.7" @@ -3671,9 +3671,9 @@ dunder-proto@^1.0.0, dunder-proto@^1.0.1: gopd "^1.2.0" electron-to-chromium@^1.5.263: - version "1.5.278" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.278.tgz#807a5e321f012a41bfd64e653f35993c9af95493" - integrity sha512-dQ0tM1svDRQOwxnXxm+twlGTjr9Upvt8UFWAgmLsxEzFQxhbti4VwxmMjsDxVC51Zo84swW7FVCXEV+VAkhuPw== + version "1.5.283" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.283.tgz#51d492c37c2d845a0dccb113fe594880c8616de8" + integrity sha512-3vifjt1HgrGW/h76UEeny+adYApveS9dH2h3p57JYzBSXJIKUJAvtmIytDKjcSCt9xHfrNCFJ7gts6vkhuq++w== emoji-regex@^7.0.1: version "7.0.3" @@ -4146,9 +4146,9 @@ get-symbol-description@^1.1.0: get-intrinsic "^1.2.6" get-tsconfig@^4.4.0: - version "4.13.0" - resolved "https://registry.yarnpkg.com/get-tsconfig/-/get-tsconfig-4.13.0.tgz#fcdd991e6d22ab9a600f00e91c318707a5d9a0d7" - integrity sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ== + version "4.13.1" + resolved "https://registry.yarnpkg.com/get-tsconfig/-/get-tsconfig-4.13.1.tgz#ff96c0d98967df211c1ebad41f375ccf516c43fa" + integrity sha512-EoY1N2xCn44xU6750Sx7OjOIT59FkmstNc3X6y5xpz7D5cBtZRe/3pSlTkDJgqsOk3WwZPkWfonhhUJfttQo3w== dependencies: resolve-pkg-maps "^1.0.0" @@ -4957,9 +4957,9 @@ jszip@^3.2.0: setimmediate "^1.0.5" katex@^0.16.0: - version "0.16.27" - resolved "https://registry.yarnpkg.com/katex/-/katex-0.16.27.tgz#4ecf6f620e0ca1c1a5de722e85fcdcec49086a48" - integrity sha512-aeQoDkuRWSqQN6nSvVCEFvfXdqo1OQiCmmW1kc9xSdjutPv7BGO7pqY9sQRJpMOGrEdfDgF2TfRXe5eUAD2Waw== + version "0.16.28" + resolved "https://registry.yarnpkg.com/katex/-/katex-0.16.28.tgz#64068425b5a29b41b136aae0d51cbb2c71d64c39" + integrity sha512-YHzO7721WbmAL6Ov1uzN/l5mY5WWWhJBSW+jq4tkfZfsxmo1hu6frS0EOswvjBUnWE6NtjEs48SFn5CQESRLZg== dependencies: commander "^8.3.0" @@ -6503,7 +6503,7 @@ postcss-value-parser@^4.0.0, postcss-value-parser@^4.1.0, postcss-value-parser@^ resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== -postcss@^8.2.14, postcss@^8.2.15, postcss@^8.4.12, postcss@^8.4.33, postcss@^8.4.40: +postcss@^8.2.14, postcss@^8.2.15, postcss@^8.4.12, postcss@^8.4.40: version "8.5.6" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.6.tgz#2825006615a619b4f62a9e7426cc120b349a8f3c" integrity sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg== @@ -6513,9 +6513,9 @@ postcss@^8.2.14, postcss@^8.2.15, postcss@^8.4.12, postcss@^8.4.33, postcss@^8.4 source-map-js "^1.2.1" preact@^10.13.2: - version "10.28.2" - resolved "https://registry.yarnpkg.com/preact/-/preact-10.28.2.tgz#4b668383afa4b4a2546bbe4bd1747e02e2360138" - integrity sha512-lbteaWGzGHdlIuiJ0l2Jq454m6kcpI1zNje6d8MlGAFlYvP2GO4ibnat7P74Esfz4sPTdM6UxtTwh/d3pwM9JA== + version "10.28.3" + resolved "https://registry.yarnpkg.com/preact/-/preact-10.28.3.tgz#3c2171526b3e29628ad1a6c56a9e3ca867bbdee8" + integrity sha512-tCmoRkPQLpBeWzpmbhryairGnhW9tKV6c6gr/w+RhoRoKEJwsjzipwp//1oCpGPOchvSLaAPlpcJi9MwMmoPyA== pretty-error@^4.0.0: version "4.0.0" @@ -6952,7 +6952,7 @@ semver@^6.0.0, semver@^6.3.1: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.5.4: +semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.6.3: version "7.7.3" resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.3.tgz#4b5f4143d007633a8dc671cd0a6ef9147b8bb946" integrity sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q== @@ -7499,9 +7499,9 @@ tslib@^2.8.0: integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== type-fest@^5.2.0: - version "5.4.1" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-5.4.1.tgz#aa9eaadcdc0acb0b5bd52e54f966ee3e38e125d2" - integrity sha512-xygQcmneDyzsEuKZrFbRMne5HDqMs++aFzefrJTgEIKjQ3rekM+RPfFCVq2Gp1VIDqddoYeppCj4Pcb+RZW0GQ== + version "5.4.3" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-5.4.3.tgz#b4c7e028da129098911ee2162a0c30df8a1be904" + integrity sha512-AXSAQJu79WGc79/3e9/CR77I/KQgeY1AhNvcShIH4PTcGYyC4xv6H4R4AUOwkPS5799KlVDAu8zExeCrkGquiA== dependencies: tagged-tag "^1.0.0" From 584643d4cabc4658442db9d8d515104859ebfb3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sat, 31 Jan 2026 22:21:59 +0100 Subject: [PATCH 16/17] Fixed phpstan issue --- 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 044eb7a7..6212f148 100644 --- a/src/Services/InfoProviderSystem/Providers/ConradProvider.php +++ b/src/Services/InfoProviderSystem/Providers/ConradProvider.php @@ -248,7 +248,7 @@ readonly class ConradProvider implements InfoProviderInterface $priceInfo = $result['priceAndAvailabilityFacadeResponse']['priceAndAvailability']['price'] ?? []; $price = $priceInfo['price'] ?? "0.0"; $currency = $priceInfo['currency'] ?? "EUR"; - $includesVat = $priceInfo['isGrossAmount'] === "true" ?? true; + $includesVat = !$priceInfo['isGrossAmount'] || $priceInfo['isGrossAmount'] === "true"; $minOrderAmount = $result['priceAndAvailabilityFacadeResponse']['priceAndAvailability']['availabilityStatus']['minimumOrderQuantity'] ?? 1; $prices = []; From a355bda9da6eb7011036f6aac886932d31a4fbcc Mon Sep 17 00:00:00 2001 From: Niklas <44636701+MayNiklas@users.noreply.github.com> Date: Sat, 31 Jan 2026 22:37:43 +0100 Subject: [PATCH 17/17] add supplier SPN linking for BOM import (#1209) * feat: add supplier SPN lookup for BOM import Add automatic part linking via supplier part numbers (SPNs) in the BOM importer. When a Part-DB ID is not provided, the importer now searches for existing parts by matching supplier SPNs from the CSV with orderdetail records in the database. This allows automatic part linking when KiCad schematic BOMs contain supplier information like LCSC SPN, Mouser SPN, etc., improving the import workflow for users who track parts by supplier part numbers. * add tests for BOM import with supplier SPN handling --- .../ImportExportSystem/BOMImporter.php | 40 +++- .../ImportExportSystem/BOMImporterTest.php | 175 ++++++++++++++++++ 2 files changed, 214 insertions(+), 1 deletion(-) diff --git a/src/Services/ImportExportSystem/BOMImporter.php b/src/Services/ImportExportSystem/BOMImporter.php index 33a402cb..8a91c825 100644 --- a/src/Services/ImportExportSystem/BOMImporter.php +++ b/src/Services/ImportExportSystem/BOMImporter.php @@ -277,8 +277,11 @@ class BOMImporter // Fetch suppliers once for efficiency $suppliers = $this->entityManager->getRepository(\App\Entity\Parts\Supplier::class)->findAll(); $supplierSPNKeys = []; + $suppliersByName = []; // Map supplier names to supplier objects foreach ($suppliers as $supplier) { - $supplierSPNKeys[] = $supplier->getName() . ' SPN'; + $supplierName = $supplier->getName(); + $supplierSPNKeys[] = $supplierName . ' SPN'; + $suppliersByName[$supplierName] = $supplier; } foreach ($csv->getRecords() as $offset => $entry) { @@ -356,6 +359,41 @@ class BOMImporter } } + // Try to link existing part based on supplier part number if no Part-DB ID is given + if ($part === null) { + // Check all available supplier SPN fields + foreach ($suppliersByName as $supplierName => $supplier) { + $supplier_spn = null; + + if (isset($mapped_entry[$supplierName . ' SPN']) && !empty(trim($mapped_entry[$supplierName . ' SPN']))) { + $supplier_spn = trim($mapped_entry[$supplierName . ' SPN']); + } + + if ($supplier_spn !== null) { + // Query for orderdetails with matching supplier and SPN + $orderdetail = $this->entityManager->getRepository(\App\Entity\PriceInformations\Orderdetail::class) + ->findOneBy([ + 'supplier' => $supplier, + 'supplierpartnr' => $supplier_spn, + ]); + + if ($orderdetail !== null && $orderdetail->getPart() !== null) { + $part = $orderdetail->getPart(); + $name = $part->getName(); // Update name with actual part name + + $this->logger->info('Linked BOM entry to existing part via supplier SPN', [ + 'supplier' => $supplierName, + 'supplier_spn' => $supplier_spn, + 'part_id' => $part->getID(), + 'part_name' => $part->getName(), + ]); + + break; // Stop searching once a match is found + } + } + } + } + // Create unique key for this entry (name + part ID) $entry_key = $name . '|' . ($part ? $part->getID() : 'null'); diff --git a/tests/Services/ImportExportSystem/BOMImporterTest.php b/tests/Services/ImportExportSystem/BOMImporterTest.php index 47ddcc24..a8841f17 100644 --- a/tests/Services/ImportExportSystem/BOMImporterTest.php +++ b/tests/Services/ImportExportSystem/BOMImporterTest.php @@ -616,6 +616,181 @@ class BOMImporterTest extends WebTestCase $this->assertEquals('R1,R2', $bom_entries[0]->getMountnames()); } + public function testStringToBOMEntriesKiCADSchematicWithSupplierSPN(): void + { + // Create test supplier + $lcscSupplier = new Supplier(); + $lcscSupplier->setName('LCSC'); + $this->entityManager->persist($lcscSupplier); + + // Create a test part with required fields + $part = new Part(); + $part->setName('Test Resistor 10k 0805'); + $part->setCategory($this->getDefaultCategory($this->entityManager)); + $this->entityManager->persist($part); + + // Create orderdetail linking the part to a supplier SPN + $orderdetail = new \App\Entity\PriceInformations\Orderdetail(); + $orderdetail->setPart($part); + $orderdetail->setSupplier($lcscSupplier); + $orderdetail->setSupplierpartnr('C123456'); + $this->entityManager->persist($orderdetail); + + $this->entityManager->flush(); + + // Import CSV with LCSC SPN matching the orderdetail + $input = << 'Designator', + 'Value' => 'Value', + 'LCSC SPN' => 'LCSC SPN', + 'Quantity' => 'Quantity' + ]; + + $bom_entries = $this->service->stringToBOMEntries($input, [ + 'type' => 'kicad_schematic', + 'field_mapping' => $field_mapping, + 'delimiter' => ',' + ]); + + $this->assertContainsOnlyInstancesOf(ProjectBOMEntry::class, $bom_entries); + $this->assertCount(1, $bom_entries); + + // Verify that the BOM entry is linked to the correct part via supplier SPN + $this->assertSame($part, $bom_entries[0]->getPart()); + $this->assertEquals('Test Resistor 10k 0805', $bom_entries[0]->getName()); + $this->assertEquals('R1,R2', $bom_entries[0]->getMountnames()); + $this->assertEquals(2.0, $bom_entries[0]->getQuantity()); + $this->assertStringContainsString('LCSC SPN: C123456', $bom_entries[0]->getComment()); + $this->assertStringContainsString('Part-DB ID: ' . $part->getID(), $bom_entries[0]->getComment()); + + // Clean up + $this->entityManager->remove($orderdetail); + $this->entityManager->remove($part); + $this->entityManager->remove($lcscSupplier); + $this->entityManager->flush(); + } + + public function testStringToBOMEntriesKiCADSchematicWithMultipleSupplierSPNs(): void + { + // Create test suppliers + $lcscSupplier = new Supplier(); + $lcscSupplier->setName('LCSC'); + $mouserSupplier = new Supplier(); + $mouserSupplier->setName('Mouser'); + $this->entityManager->persist($lcscSupplier); + $this->entityManager->persist($mouserSupplier); + + // Create first part linked via LCSC SPN + $part1 = new Part(); + $part1->setName('Resistor 10k'); + $part1->setCategory($this->getDefaultCategory($this->entityManager)); + $this->entityManager->persist($part1); + + $orderdetail1 = new \App\Entity\PriceInformations\Orderdetail(); + $orderdetail1->setPart($part1); + $orderdetail1->setSupplier($lcscSupplier); + $orderdetail1->setSupplierpartnr('C123456'); + $this->entityManager->persist($orderdetail1); + + // Create second part linked via Mouser SPN + $part2 = new Part(); + $part2->setName('Capacitor 100nF'); + $part2->setCategory($this->getDefaultCategory($this->entityManager)); + $this->entityManager->persist($part2); + + $orderdetail2 = new \App\Entity\PriceInformations\Orderdetail(); + $orderdetail2->setPart($part2); + $orderdetail2->setSupplier($mouserSupplier); + $orderdetail2->setSupplierpartnr('789-CAP100NF'); + $this->entityManager->persist($orderdetail2); + + $this->entityManager->flush(); + + // Import CSV with both LCSC and Mouser SPNs + $input = << 'Designator', + 'Value' => 'Value', + 'LCSC SPN' => 'LCSC SPN', + 'Mouser SPN' => 'Mouser SPN', + 'Quantity' => 'Quantity' + ]; + + $bom_entries = $this->service->stringToBOMEntries($input, [ + 'type' => 'kicad_schematic', + 'field_mapping' => $field_mapping, + 'delimiter' => ',' + ]); + + $this->assertCount(2, $bom_entries); + + // Verify first entry linked via LCSC SPN + $this->assertSame($part1, $bom_entries[0]->getPart()); + $this->assertEquals('Resistor 10k', $bom_entries[0]->getName()); + + // Verify second entry linked via Mouser SPN + $this->assertSame($part2, $bom_entries[1]->getPart()); + $this->assertEquals('Capacitor 100nF', $bom_entries[1]->getName()); + + // Clean up + $this->entityManager->remove($orderdetail1); + $this->entityManager->remove($orderdetail2); + $this->entityManager->remove($part1); + $this->entityManager->remove($part2); + $this->entityManager->remove($lcscSupplier); + $this->entityManager->remove($mouserSupplier); + $this->entityManager->flush(); + } + + public function testStringToBOMEntriesKiCADSchematicWithNonMatchingSPN(): void + { + // Create test supplier + $lcscSupplier = new Supplier(); + $lcscSupplier->setName('LCSC'); + $this->entityManager->persist($lcscSupplier); + $this->entityManager->flush(); + + // Import CSV with LCSC SPN that doesn't match any orderdetail + $input = << 'Designator', + 'Value' => 'Value', + 'LCSC SPN' => 'LCSC SPN', + 'Quantity' => 'Quantity' + ]; + + $bom_entries = $this->service->stringToBOMEntries($input, [ + 'type' => 'kicad_schematic', + 'field_mapping' => $field_mapping, + 'delimiter' => ',' + ]); + + $this->assertCount(1, $bom_entries); + + // Verify that no part is linked (SPN not found) + $this->assertNull($bom_entries[0]->getPart()); + $this->assertEquals('10k', $bom_entries[0]->getName()); // Should use Value as name + $this->assertStringContainsString('LCSC SPN: C999999', $bom_entries[0]->getComment()); + + // Clean up + $this->entityManager->remove($lcscSupplier); + $this->entityManager->flush(); + } + private function getDefaultCategory(EntityManagerInterface $entityManager) { // Get the first available category or create a default one