Allow to query price infos from conrad

This commit is contained in:
Jan Böhmer 2026-01-31 21:15:35 +01:00
parent 6f4dad98d9
commit 98937974c9
3 changed files with 117 additions and 26 deletions

View file

@ -26,6 +26,8 @@ namespace App\Services\InfoProviderSystem\Providers;
use App\Services\InfoProviderSystem\DTOs\FileDTO; use App\Services\InfoProviderSystem\DTOs\FileDTO;
use App\Services\InfoProviderSystem\DTOs\ParameterDTO; use App\Services\InfoProviderSystem\DTOs\ParameterDTO;
use App\Services\InfoProviderSystem\DTOs\PartDetailDTO; 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\Services\InfoProviderSystem\DTOs\SearchResultDTO;
use App\Settings\InfoProviderSystem\ConradSettings; use App\Settings\InfoProviderSystem\ConradSettings;
use Symfony\Contracts\HttpClient\HttpClientInterface; use Symfony\Contracts\HttpClient\HttpClientInterface;
@ -34,9 +36,18 @@ readonly class ConradProvider implements InfoProviderInterface
{ {
private const SEARCH_ENDPOINT = '/search/1/v3/facetSearch'; 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 public function getProviderInfo(): array
@ -76,6 +87,44 @@ readonly class ConradProvider implements InfoProviderInterface
return null; 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 private function getFootprintFromTechnicalAttributes(array $technicalDetails): ?string
{ {
foreach ($technicalDetails as $detail) { foreach ($technicalDetails as $detail) {
@ -87,6 +136,10 @@ readonly class ConradProvider implements InfoProviderInterface
return null; return null;
} }
/**
* @param array $technicalAttributes
* @return array<ParameterDTO>
*/
private function technicalAttributesToParameters(array $technicalAttributes): array private function technicalAttributesToParameters(array $technicalAttributes): array
{ {
return array_map(static function (array $p) { return array_map(static function (array $p) {
@ -139,6 +192,10 @@ readonly class ConradProvider implements InfoProviderInterface
}, $technicalAttributes); }, $technicalAttributes);
} }
/**
* @param array $productMedia
* @return array<FileDTO>
*/
public function productMediaToDatasheets(array $productMedia): array public function productMediaToDatasheets(array $productMedia): array
{ {
$files = []; $files = [];
@ -149,42 +206,70 @@ readonly class ConradProvider implements InfoProviderInterface
return $files; 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' => [ 'query' => [
'apikey' => $this->settings->apiKey, 'apikey' => $this->settings->apiKey,
'overrideCalculationSchema' => $this->settings->includeVAT ? 'GROSS' : 'NET'
], ],
'json' => [ 'json' => [
'query' => $keyword, 'ns:inputArticleItemList' => [
'size' => 50, "#namespaces" => [
'sort' => [["field"=>"_score","order"=>"desc"]], "ns" => "http://www.conrad.de/ccp/basit/service/article/priceandavailabilityservice/api"
], ],
'articles' => [
[
"articleID" => $productId,
"calculatePrice" => true,
"checkAvailability" => true,
],
]
]
]
]); ]);
$out = []; $result = $response->toArray();
$results = $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( $prices = [];
provider_key: $this->getProviderKey(), foreach ($priceInfo['priceScale'] as $priceScale) {
provider_id: $result['productId'], $prices[] = new PriceDTO(
name: $result['title'], minimum_discount_amount: max($priceScale['scaleFrom'], $minOrderAmount),
description: '', price: (string)$priceScale['pricePerUnit'],
manufacturer: $result['brand']['name'] ?? null, currency_iso_code: $currency,
mpn: $result['manufacturerId'] ?? null, includes_tax: $includesVat
preview_image_url: $result['image'] ?? null, );
provider_url: $this->getProductUrl($result['productId']), }
footprint: $this->getFootprintFromTechnicalDetails($result['technicalDetails'] ?? []), 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 public function getDetails(string $id): PartDetailDTO
@ -212,7 +297,8 @@ readonly class ConradProvider implements InfoProviderInterface
footprint: $this->getFootprintFromTechnicalAttributes($data['productFullInformation']['technicalAttributes'] ?? []), footprint: $this->getFootprintFromTechnicalAttributes($data['productFullInformation']['technicalAttributes'] ?? []),
notes: $data['productFullInformation']['description'] ?? null, notes: $data['productFullInformation']['description'] ?? null,
datasheets: $this->productMediaToDatasheets($data['productMedia'] ?? []), datasheets: $this->productMediaToDatasheets($data['productMedia'] ?? []),
parameters: $this->technicalAttributesToParameters($data['productFullInformation']['technicalAttributes'] ?? []) parameters: $this->technicalAttributesToParameters($data['productFullInformation']['technicalAttributes'] ?? []),
vendor_infos: [$this->queryPrices($data['shortProductNumber'])]
); );
} }

View file

@ -53,5 +53,6 @@ class ConradSettings
)] )]
public ConradShopIDs $shopID = ConradShopIDs::COM_B2B; public ConradShopIDs $shopID = ConradShopIDs::COM_B2B;
#[SettingsParameter(label: new TM("settings.ips.reichelt.include_vat"))]
public bool $includeVAT = true; public bool $includeVAT = true;
} }

View file

@ -100,6 +100,10 @@ enum ConradShopIDs: string implements TranslatableInterface
*/ */
public function getShopID(): string public function getShopID(): string
{ {
if ($this === self::CH_B2C_FR || $this === self::CH_B2C_DE) {
return 'CQ_CH_B2C';
}
return $this->value; return $this->value;
} }