mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2026-01-14 22:29:33 +00:00
Merge tag 'v2.1.2' into Buerklin-provider
# Conflicts: # .docker/symfony.conf # VERSION
This commit is contained in:
commit
5b2fc7ef4b
366 changed files with 32347 additions and 19045 deletions
|
|
@ -31,6 +31,7 @@ use App\Services\InfoProviderSystem\DTOs\PriceDTO;
|
|||
use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO;
|
||||
use App\Services\InfoProviderSystem\DTOs\SearchResultDTO;
|
||||
use App\Services\OAuth\OAuthTokenManager;
|
||||
use App\Settings\InfoProviderSystem\DigikeySettings;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
||||
class DigikeyProvider implements InfoProviderInterface
|
||||
|
|
@ -55,17 +56,16 @@ class DigikeyProvider implements InfoProviderInterface
|
|||
];
|
||||
|
||||
public function __construct(HttpClientInterface $httpClient, private readonly OAuthTokenManager $authTokenManager,
|
||||
private readonly string $currency, private readonly string $clientId,
|
||||
private readonly string $language, private readonly string $country)
|
||||
private readonly DigikeySettings $settings,)
|
||||
{
|
||||
//Create the HTTP client with some default options
|
||||
$this->digikeyClient = $httpClient->withOptions([
|
||||
"base_uri" => self::BASE_URI,
|
||||
"headers" => [
|
||||
"X-DIGIKEY-Client-Id" => $clientId,
|
||||
"X-DIGIKEY-Locale-Site" => $this->country,
|
||||
"X-DIGIKEY-Locale-Language" => $this->language,
|
||||
"X-DIGIKEY-Locale-Currency" => $this->currency,
|
||||
"X-DIGIKEY-Client-Id" => $this->settings->clientId,
|
||||
"X-DIGIKEY-Locale-Site" => $this->settings->country,
|
||||
"X-DIGIKEY-Locale-Language" => $this->settings->language,
|
||||
"X-DIGIKEY-Locale-Currency" => $this->settings->currency,
|
||||
"X-DIGIKEY-Customer-Id" => 0,
|
||||
]
|
||||
]);
|
||||
|
|
@ -78,7 +78,8 @@ class DigikeyProvider implements InfoProviderInterface
|
|||
'description' => 'This provider uses the DigiKey API to search for parts.',
|
||||
'url' => 'https://www.digikey.com/',
|
||||
'oauth_app_name' => self::OAUTH_APP_NAME,
|
||||
'disabled_help' => 'Set the PROVIDER_DIGIKEY_CLIENT_ID and PROVIDER_DIGIKEY_SECRET env option and connect OAuth to enable.'
|
||||
'disabled_help' => 'Set the Client ID and Secret in provider settings and connect OAuth to enable.',
|
||||
'settings_class' => DigikeySettings::class,
|
||||
];
|
||||
}
|
||||
|
||||
|
|
@ -101,19 +102,22 @@ class DigikeyProvider implements InfoProviderInterface
|
|||
public function isActive(): bool
|
||||
{
|
||||
//The client ID has to be set and a token has to be available (user clicked connect)
|
||||
return $this->clientId !== '' && $this->authTokenManager->hasToken(self::OAUTH_APP_NAME);
|
||||
return $this->settings->clientId !== null && $this->settings->clientId !== '' && $this->authTokenManager->hasToken(self::OAUTH_APP_NAME);
|
||||
}
|
||||
|
||||
public function searchByKeyword(string $keyword): array
|
||||
{
|
||||
$request = [
|
||||
'Keywords' => $keyword,
|
||||
'RecordCount' => 50,
|
||||
'RecordStartPosition' => 0,
|
||||
'ExcludeMarketPlaceProducts' => 'true',
|
||||
'Limit' => 50,
|
||||
'Offset' => 0,
|
||||
'FilterOptionsRequest' => [
|
||||
'MarketPlaceFilter' => 'ExcludeMarketPlace',
|
||||
],
|
||||
];
|
||||
|
||||
$response = $this->digikeyClient->request('POST', '/Search/v3/Products/Keyword', [
|
||||
//$response = $this->digikeyClient->request('POST', '/Search/v3/Products/Keyword', [
|
||||
$response = $this->digikeyClient->request('POST', '/products/v4/search/keyword', [
|
||||
'json' => $request,
|
||||
'auth_bearer' => $this->authTokenManager->getAlwaysValidTokenString(self::OAUTH_APP_NAME)
|
||||
]);
|
||||
|
|
@ -124,18 +128,21 @@ class DigikeyProvider implements InfoProviderInterface
|
|||
$result = [];
|
||||
$products = $response_array['Products'];
|
||||
foreach ($products as $product) {
|
||||
$result[] = new SearchResultDTO(
|
||||
provider_key: $this->getProviderKey(),
|
||||
provider_id: $product['DigiKeyPartNumber'],
|
||||
name: $product['ManufacturerPartNumber'],
|
||||
description: $product['DetailedDescription'] ?? $product['ProductDescription'],
|
||||
category: $this->getCategoryString($product),
|
||||
manufacturer: $product['Manufacturer']['Value'] ?? null,
|
||||
mpn: $product['ManufacturerPartNumber'],
|
||||
preview_image_url: $product['PrimaryPhoto'] ?? null,
|
||||
manufacturing_status: $this->productStatusToManufacturingStatus($product['ProductStatus']),
|
||||
provider_url: $product['ProductUrl'],
|
||||
);
|
||||
foreach ($product['ProductVariations'] as $variation) {
|
||||
$result[] = new SearchResultDTO(
|
||||
provider_key: $this->getProviderKey(),
|
||||
provider_id: $variation['DigiKeyProductNumber'],
|
||||
name: $product['ManufacturerProductNumber'],
|
||||
description: $product['Description']['DetailedDescription'] ?? $product['Description']['ProductDescription'],
|
||||
category: $this->getCategoryString($product),
|
||||
manufacturer: $product['Manufacturer']['Name'] ?? null,
|
||||
mpn: $product['ManufacturerProductNumber'],
|
||||
preview_image_url: $product['PhotoUrl'] ?? null,
|
||||
manufacturing_status: $this->productStatusToManufacturingStatus($product['ProductStatus']['Id']),
|
||||
provider_url: $product['ProductUrl'],
|
||||
footprint: $variation['PackageType']['Name'], //Use the footprint field, to show the user the package type (Tape & Reel, etc., as digikey has many different package types)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
|
|
@ -143,62 +150,79 @@ class DigikeyProvider implements InfoProviderInterface
|
|||
|
||||
public function getDetails(string $id): PartDetailDTO
|
||||
{
|
||||
$response = $this->digikeyClient->request('GET', '/Search/v3/Products/' . urlencode($id), [
|
||||
$response = $this->digikeyClient->request('GET', '/products/v4/search/' . urlencode($id) . '/productdetails', [
|
||||
'auth_bearer' => $this->authTokenManager->getAlwaysValidTokenString(self::OAUTH_APP_NAME)
|
||||
]);
|
||||
|
||||
$product = $response->toArray();
|
||||
$response_array = $response->toArray();
|
||||
$product = $response_array['Product'];
|
||||
|
||||
$footprint = null;
|
||||
$parameters = $this->parametersToDTOs($product['Parameters'] ?? [], $footprint);
|
||||
$media = $this->mediaToDTOs($product['MediaLinks']);
|
||||
$media = $this->mediaToDTOs($id);
|
||||
|
||||
// Get the price_breaks of the selected variation
|
||||
$price_breaks = [];
|
||||
foreach ($product['ProductVariations'] as $variation) {
|
||||
if ($variation['DigiKeyProductNumber'] == $id) {
|
||||
$price_breaks = $variation['StandardPricing'] ?? [];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return new PartDetailDTO(
|
||||
provider_key: $this->getProviderKey(),
|
||||
provider_id: $product['DigiKeyPartNumber'],
|
||||
name: $product['ManufacturerPartNumber'],
|
||||
description: $product['DetailedDescription'] ?? $product['ProductDescription'],
|
||||
provider_id: $id,
|
||||
name: $product['ManufacturerProductNumber'],
|
||||
description: $product['Description']['DetailedDescription'] ?? $product['Description']['ProductDescription'],
|
||||
category: $this->getCategoryString($product),
|
||||
manufacturer: $product['Manufacturer']['Value'] ?? null,
|
||||
mpn: $product['ManufacturerPartNumber'],
|
||||
preview_image_url: $product['PrimaryPhoto'] ?? null,
|
||||
manufacturing_status: $this->productStatusToManufacturingStatus($product['ProductStatus']),
|
||||
manufacturer: $product['Manufacturer']['Name'] ?? null,
|
||||
mpn: $product['ManufacturerProductNumber'],
|
||||
preview_image_url: $product['PhotoUrl'] ?? null,
|
||||
manufacturing_status: $this->productStatusToManufacturingStatus($product['ProductStatus']['Id']),
|
||||
provider_url: $product['ProductUrl'],
|
||||
footprint: $footprint,
|
||||
datasheets: $media['datasheets'],
|
||||
images: $media['images'],
|
||||
parameters: $parameters,
|
||||
vendor_infos: $this->pricingToDTOs($product['StandardPricing'] ?? [], $product['DigiKeyPartNumber'], $product['ProductUrl']),
|
||||
vendor_infos: $this->pricingToDTOs($price_breaks, $id, $product['ProductUrl']),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the product status from the Digikey API to the manufacturing status used in Part-DB
|
||||
* @param string|null $dk_status
|
||||
* @param int|null $dk_status
|
||||
* @return ManufacturingStatus|null
|
||||
*/
|
||||
private function productStatusToManufacturingStatus(?string $dk_status): ?ManufacturingStatus
|
||||
private function productStatusToManufacturingStatus(?int $dk_status): ?ManufacturingStatus
|
||||
{
|
||||
// The V4 can use strings to get the status, but if you have changed the PROVIDER_DIGIKEY_LANGUAGE it will not match.
|
||||
// Using the Id instead which should be fixed.
|
||||
//
|
||||
// The API is not well documented and the ID are not there yet, so were extracted using "trial and error".
|
||||
// The 'Preliminary' id was not found in several categories so I was unable to extract it. Disabled for now.
|
||||
return match ($dk_status) {
|
||||
null => null,
|
||||
'Active' => ManufacturingStatus::ACTIVE,
|
||||
'Obsolete' => ManufacturingStatus::DISCONTINUED,
|
||||
'Discontinued at Digi-Key', 'Last Time Buy' => ManufacturingStatus::EOL,
|
||||
'Not For New Designs' => ManufacturingStatus::NRFND,
|
||||
'Preliminary' => ManufacturingStatus::ANNOUNCED,
|
||||
0 => ManufacturingStatus::ACTIVE,
|
||||
1 => ManufacturingStatus::DISCONTINUED,
|
||||
2, 4 => ManufacturingStatus::EOL,
|
||||
7 => ManufacturingStatus::NRFND,
|
||||
//'Preliminary' => ManufacturingStatus::ANNOUNCED,
|
||||
default => ManufacturingStatus::NOT_SET,
|
||||
};
|
||||
}
|
||||
|
||||
private function getCategoryString(array $product): string
|
||||
{
|
||||
$category = $product['Category']['Value'];
|
||||
$sub_category = $product['Family']['Value'];
|
||||
$category = $product['Category']['Name'];
|
||||
$sub_category = current($product['Category']['ChildCategories']);
|
||||
|
||||
//Replace the ' - ' category separator with ' -> '
|
||||
$sub_category = str_replace(' - ', ' -> ', $sub_category);
|
||||
if ($sub_category) {
|
||||
//Replace the ' - ' category separator with ' -> '
|
||||
$category = $category . ' -> ' . str_replace(' - ', ' -> ', $sub_category["Name"]);
|
||||
}
|
||||
|
||||
return $category . ' -> ' . $sub_category;
|
||||
return $category;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -215,18 +239,18 @@ class DigikeyProvider implements InfoProviderInterface
|
|||
|
||||
foreach ($parameters as $parameter) {
|
||||
if ($parameter['ParameterId'] === 1291) { //Meaning "Manufacturer given footprint"
|
||||
$footprint_name = $parameter['Value'];
|
||||
$footprint_name = $parameter['ValueText'];
|
||||
}
|
||||
|
||||
if (in_array(trim((string) $parameter['Value']), ['', '-'], true)) {
|
||||
if (in_array(trim((string) $parameter['ValueText']), ['', '-'], true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
//If the parameter was marked as text only, then we do not try to parse it as a numerical value
|
||||
if (in_array($parameter['ParameterId'], self::TEXT_ONLY_PARAMETERS, true)) {
|
||||
$results[] = new ParameterDTO(name: $parameter['Parameter'], value_text: $parameter['Value']);
|
||||
$results[] = new ParameterDTO(name: $parameter['ParameterText'], value_text: $parameter['ValueText']);
|
||||
} else { //Otherwise try to parse it as a numerical value
|
||||
$results[] = ParameterDTO::parseValueIncludingUnit($parameter['Parameter'], $parameter['Value']);
|
||||
$results[] = ParameterDTO::parseValueIncludingUnit($parameter['ParameterText'], $parameter['ValueText']);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -245,7 +269,7 @@ class DigikeyProvider implements InfoProviderInterface
|
|||
$prices = [];
|
||||
|
||||
foreach ($price_breaks as $price_break) {
|
||||
$prices[] = new PriceDTO(minimum_discount_amount: $price_break['BreakQuantity'], price: (string) $price_break['UnitPrice'], currency_iso_code: $this->currency);
|
||||
$prices[] = new PriceDTO(minimum_discount_amount: $price_break['BreakQuantity'], price: (string) $price_break['UnitPrice'], currency_iso_code: $this->settings->currency);
|
||||
}
|
||||
|
||||
return [
|
||||
|
|
@ -254,16 +278,22 @@ class DigikeyProvider implements InfoProviderInterface
|
|||
}
|
||||
|
||||
/**
|
||||
* @param array $media_links
|
||||
* @param string $id The Digikey product number, to get the media for
|
||||
* @return FileDTO[][]
|
||||
* @phpstan-return array<string, FileDTO[]>
|
||||
*/
|
||||
private function mediaToDTOs(array $media_links): array
|
||||
private function mediaToDTOs(string $id): array
|
||||
{
|
||||
$datasheets = [];
|
||||
$images = [];
|
||||
|
||||
foreach ($media_links as $media_link) {
|
||||
$response = $this->digikeyClient->request('GET', '/products/v4/search/' . urlencode($id) . '/media', [
|
||||
'auth_bearer' => $this->authTokenManager->getAlwaysValidTokenString(self::OAUTH_APP_NAME)
|
||||
]);
|
||||
|
||||
$media_array = $response->toArray();
|
||||
|
||||
foreach ($media_array['MediaLinks'] as $media_link) {
|
||||
$file = new FileDTO(url: $media_link['Url'], name: $media_link['Title']);
|
||||
|
||||
switch ($media_link['MediaType']) {
|
||||
|
|
|
|||
|
|
@ -29,6 +29,8 @@ 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\Settings\InfoProviderSystem\Element14Settings;
|
||||
use Composer\CaBundle\CaBundle;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
||||
class Element14Provider implements InfoProviderInterface
|
||||
|
|
@ -43,9 +45,19 @@ class Element14Provider implements InfoProviderInterface
|
|||
private const COMPLIANCE_ATTRIBUTES = ['euEccn', 'hazardous', 'MSL', 'productTraceability', 'rohsCompliant',
|
||||
'rohsPhthalatesCompliant', 'SVHC', 'tariffCode', 'usEccn', 'hazardCode'];
|
||||
|
||||
public function __construct(private readonly HttpClientInterface $element14Client, private readonly string $api_key, private readonly string $store_id)
|
||||
{
|
||||
private readonly HttpClientInterface $element14Client;
|
||||
|
||||
public function __construct(HttpClientInterface $element14Client, private readonly Element14Settings $settings)
|
||||
{
|
||||
/* We use the mozilla CA from the composer ca bundle directly, as some debian systems seems to have problems
|
||||
* with the SSL.COM CA, element14 uses. See https://github.com/Part-DB/Part-DB-server/issues/866
|
||||
*
|
||||
* This is a workaround until the issue is resolved in debian (or never).
|
||||
* As this only affects this provider, this should have no negative impact and the CA bundle is still secure.
|
||||
*/
|
||||
$this->element14Client = $element14Client->withOptions([
|
||||
'cafile' => CaBundle::getBundledCaBundlePath(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function getProviderInfo(): array
|
||||
|
|
@ -54,7 +66,8 @@ class Element14Provider implements InfoProviderInterface
|
|||
'name' => 'Farnell element14',
|
||||
'description' => 'This provider uses the Farnell element14 API to search for parts.',
|
||||
'url' => 'https://www.element14.com/',
|
||||
'disabled_help' => 'Configure the API key in the PROVIDER_ELEMENT14_KEY environment variable to enable.'
|
||||
'disabled_help' => 'Configure the API key in the provider settings to enable.',
|
||||
'settings_class' => Element14Settings::class,
|
||||
];
|
||||
}
|
||||
|
||||
|
|
@ -65,7 +78,7 @@ class Element14Provider implements InfoProviderInterface
|
|||
|
||||
public function isActive(): bool
|
||||
{
|
||||
return $this->api_key !== '';
|
||||
return $this->settings->apiKey !== null && trim($this->settings->apiKey) !== '';
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -77,11 +90,11 @@ class Element14Provider implements InfoProviderInterface
|
|||
$response = $this->element14Client->request('GET', self::ENDPOINT_URL, [
|
||||
'query' => [
|
||||
'term' => $term,
|
||||
'storeInfo.id' => $this->store_id,
|
||||
'storeInfo.id' => $this->settings->storeId,
|
||||
'resultsSettings.offset' => 0,
|
||||
'resultsSettings.numberOfResults' => self::NUMBER_OF_RESULTS,
|
||||
'resultsSettings.responseGroup' => 'large',
|
||||
'callInfo.apiKey' => $this->api_key,
|
||||
'callInfo.apiKey' => $this->settings->apiKey,
|
||||
'callInfo.responseDataFormat' => 'json',
|
||||
'versionNumber' => self::API_VERSION_NUMBER,
|
||||
],
|
||||
|
|
@ -149,7 +162,7 @@ class Element14Provider implements InfoProviderInterface
|
|||
$locale = 'en_US';
|
||||
}
|
||||
|
||||
return 'https://' . $this->store_id . '/productimages/standard/' . $locale . $image['baseName'];
|
||||
return 'https://' . $this->settings->storeId . '/productimages/standard/' . $locale . $image['baseName'];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -184,7 +197,7 @@ class Element14Provider implements InfoProviderInterface
|
|||
public function getUsedCurrency(): string
|
||||
{
|
||||
//Decide based on the shop ID
|
||||
return match ($this->store_id) {
|
||||
return match ($this->settings->storeId) {
|
||||
'bg.farnell.com', 'at.farnell.com', 'si.farnell.com', 'sk.farnell.com', 'ro.farnell.com', 'pt.farnell.com', 'nl.farnell.com', 'be.farnell.com', 'lv.farnell.com', 'lt.farnell.com', 'it.farnell.com', 'fr.farnell.com', 'fi.farnell.com', 'ee.farnell.com', 'es.farnell.com', 'ie.farnell.com', 'cpcireland.farnell.com', 'de.farnell.com' => 'EUR',
|
||||
'cz.farnell.com' => 'CZK',
|
||||
'dk.farnell.com' => 'DKK',
|
||||
|
|
@ -211,7 +224,7 @@ class Element14Provider implements InfoProviderInterface
|
|||
'tw.element14.com' => 'TWD',
|
||||
'kr.element14.com' => 'KRW',
|
||||
'vn.element14.com' => 'VND',
|
||||
default => throw new \RuntimeException('Unknown store ID: ' . $this->store_id)
|
||||
default => throw new \RuntimeException('Unknown store ID: ' . $this->settings->storeId)
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -296,4 +309,4 @@ class Element14Provider implements InfoProviderInterface
|
|||
ProviderCapabilities::DATASHEET,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,8 +39,9 @@ interface InfoProviderInterface
|
|||
* - url?: The url of the provider (e.g. "https://www.digikey.com")
|
||||
* - disabled_help?: A help text which is shown when the provider is disabled, explaining how to enable it
|
||||
* - oauth_app_name?: The name of the OAuth app which is used for authentication (e.g. "ip_digikey_oauth"). If this is set a connect button will be shown
|
||||
* - settings_class?: The class name of the settings class which contains the settings for this provider (e.g. "App\Settings\InfoProviderSettings\DigikeySettings"). If this is set a link to the settings will be shown
|
||||
*
|
||||
* @phpstan-return array{ name: string, description?: string, logo?: string, url?: string, disabled_help?: string, oauth_app_name?: string }
|
||||
* @phpstan-return array{ name: string, description?: string, logo?: string, url?: string, disabled_help?: string, oauth_app_name?: string, settings_class?: class-string }
|
||||
*/
|
||||
public function getProviderInfo(): array;
|
||||
|
||||
|
|
@ -78,4 +79,4 @@ interface InfoProviderInterface
|
|||
* @return ProviderCapabilities[]
|
||||
*/
|
||||
public function getCapabilities(): array;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ 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\Settings\InfoProviderSystem\LCSCSettings;
|
||||
use Symfony\Component\HttpFoundation\Cookie;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
||||
|
|
@ -39,7 +40,7 @@ class LCSCProvider implements InfoProviderInterface
|
|||
|
||||
public const DISTRIBUTOR_NAME = 'LCSC';
|
||||
|
||||
public function __construct(private readonly HttpClientInterface $lcscClient, private readonly string $currency, private readonly bool $enabled = true)
|
||||
public function __construct(private readonly HttpClientInterface $lcscClient, private readonly LCSCSettings $settings)
|
||||
{
|
||||
|
||||
}
|
||||
|
|
@ -50,7 +51,8 @@ class LCSCProvider implements InfoProviderInterface
|
|||
'name' => 'LCSC',
|
||||
'description' => 'This provider uses the (unofficial) LCSC API to search for parts.',
|
||||
'url' => 'https://www.lcsc.com/',
|
||||
'disabled_help' => 'Set PROVIDER_LCSC_ENABLED to 1 (or true) in your environment variable config.'
|
||||
'disabled_help' => 'Enable this provider in the provider settings.',
|
||||
'settings_class' => LCSCSettings::class,
|
||||
];
|
||||
}
|
||||
|
||||
|
|
@ -62,7 +64,7 @@ class LCSCProvider implements InfoProviderInterface
|
|||
// This provider is always active
|
||||
public function isActive(): bool
|
||||
{
|
||||
return $this->enabled;
|
||||
return $this->settings->enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -73,7 +75,7 @@ class LCSCProvider implements InfoProviderInterface
|
|||
{
|
||||
$response = $this->lcscClient->request('GET', self::ENDPOINT_URL . "/product/detail", [
|
||||
'headers' => [
|
||||
'Cookie' => new Cookie('currencyCode', $this->currency)
|
||||
'Cookie' => new Cookie('currencyCode', $this->settings->currency)
|
||||
],
|
||||
'query' => [
|
||||
'productCode' => $id,
|
||||
|
|
@ -121,11 +123,11 @@ class LCSCProvider implements InfoProviderInterface
|
|||
*/
|
||||
private function queryByTerm(string $term): array
|
||||
{
|
||||
$response = $this->lcscClient->request('GET', self::ENDPOINT_URL . "/search/global", [
|
||||
$response = $this->lcscClient->request('POST', self::ENDPOINT_URL . "/search/v2/global", [
|
||||
'headers' => [
|
||||
'Cookie' => new Cookie('currencyCode', $this->currency)
|
||||
'Cookie' => new Cookie('currencyCode', $this->settings->currency)
|
||||
],
|
||||
'query' => [
|
||||
'json' => [
|
||||
'keyword' => $term,
|
||||
],
|
||||
]);
|
||||
|
|
@ -163,6 +165,9 @@ class LCSCProvider implements InfoProviderInterface
|
|||
if ($field === null) {
|
||||
return null;
|
||||
}
|
||||
// Replace "range" indicators with mathematical tilde symbols
|
||||
// so they don't get rendered as strikethrough by Markdown
|
||||
$field = preg_replace("/~/", "\u{223c}", $field);
|
||||
|
||||
return strip_tags($field);
|
||||
}
|
||||
|
|
@ -195,9 +200,6 @@ class LCSCProvider implements InfoProviderInterface
|
|||
$category = $product['parentCatalogName'] ?? null;
|
||||
if (isset($product['catalogName'])) {
|
||||
$category = ($category ?? '') . ' -> ' . $product['catalogName'];
|
||||
|
||||
// Replace the / with a -> for better readability
|
||||
$category = str_replace('/', ' -> ', $category);
|
||||
}
|
||||
|
||||
return new PartDetailDTO(
|
||||
|
|
@ -273,7 +275,7 @@ class LCSCProvider implements InfoProviderInterface
|
|||
'kr.' => 'DKK',
|
||||
'₹' => 'INR',
|
||||
//Fallback to the configured currency
|
||||
default => $this->currency,
|
||||
default => $this->settings->currency,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ use App\Services\InfoProviderSystem\DTOs\FileDTO;
|
|||
use App\Services\InfoProviderSystem\DTOs\PartDetailDTO;
|
||||
use App\Services\InfoProviderSystem\DTOs\PriceDTO;
|
||||
use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO;
|
||||
use App\Settings\InfoProviderSystem\MouserSettings;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
use Symfony\Contracts\HttpClient\ResponseInterface;
|
||||
|
||||
|
|
@ -50,10 +51,7 @@ class MouserProvider implements InfoProviderInterface
|
|||
|
||||
public function __construct(
|
||||
private readonly HttpClientInterface $mouserClient,
|
||||
private readonly string $api_key,
|
||||
private readonly string $language,
|
||||
private readonly string $options,
|
||||
private readonly int $search_limit
|
||||
private readonly MouserSettings $settings,
|
||||
) {
|
||||
}
|
||||
|
||||
|
|
@ -63,7 +61,8 @@ class MouserProvider implements InfoProviderInterface
|
|||
'name' => 'Mouser',
|
||||
'description' => 'This provider uses the Mouser API to search for parts.',
|
||||
'url' => 'https://www.mouser.com/',
|
||||
'disabled_help' => 'Configure the API key in the PROVIDER_MOUSER_KEY environment variable to enable.'
|
||||
'disabled_help' => 'Configure the API key in the provider settings to enable.',
|
||||
'settings_class' => MouserSettings::class
|
||||
];
|
||||
}
|
||||
|
||||
|
|
@ -74,7 +73,7 @@ class MouserProvider implements InfoProviderInterface
|
|||
|
||||
public function isActive(): bool
|
||||
{
|
||||
return $this->api_key !== '';
|
||||
return $this->settings->apiKey !== '' && $this->settings->apiKey !== null;
|
||||
}
|
||||
|
||||
public function searchByKeyword(string $keyword): array
|
||||
|
|
@ -94,6 +93,7 @@ class MouserProvider implements InfoProviderInterface
|
|||
From the startingRecord, the number of records specified will be returned up to the end of the recordset.
|
||||
This is useful for paging through the complete recordset of parts matching keyword.
|
||||
|
||||
|
||||
searchOptions string
|
||||
Optional.
|
||||
If not provided, the default is None.
|
||||
|
|
@ -119,15 +119,15 @@ class MouserProvider implements InfoProviderInterface
|
|||
|
||||
$response = $this->mouserClient->request('POST', self::ENDPOINT_URL."/keyword", [
|
||||
'query' => [
|
||||
'apiKey' => $this->api_key,
|
||||
'apiKey' => $this->settings->apiKey
|
||||
],
|
||||
'json' => [
|
||||
'SearchByKeywordRequest' => [
|
||||
'keyword' => $keyword,
|
||||
'records' => $this->search_limit, //self::NUMBER_OF_RESULTS,
|
||||
'records' => $this->settings->searchLimit, //self::NUMBER_OF_RESULTS,
|
||||
'startingRecord' => 0,
|
||||
'searchOptions' => $this->options,
|
||||
'searchWithYourSignUpLanguage' => $this->language,
|
||||
'searchOptions' => $this->settings->searchOption->value,
|
||||
'searchWithYourSignUpLanguage' => $this->settings->searchWithSignUpLanguage ? 'true' : 'false',
|
||||
]
|
||||
],
|
||||
]);
|
||||
|
|
@ -160,7 +160,7 @@ class MouserProvider implements InfoProviderInterface
|
|||
|
||||
$response = $this->mouserClient->request('POST', self::ENDPOINT_URL."/partnumber", [
|
||||
'query' => [
|
||||
'apiKey' => $this->api_key,
|
||||
'apiKey' => $this->settings->apiKey,
|
||||
],
|
||||
'json' => [
|
||||
'SearchByPartRequest' => [
|
||||
|
|
@ -176,11 +176,16 @@ class MouserProvider implements InfoProviderInterface
|
|||
throw new \RuntimeException('No part found with ID '.$id);
|
||||
}
|
||||
|
||||
//Manually filter out the part with the correct ID
|
||||
$tmp = array_filter($tmp, fn(PartDetailDTO $part) => $part->provider_id === $id);
|
||||
if (count($tmp) === 0) {
|
||||
throw new \RuntimeException('No part found with ID '.$id);
|
||||
}
|
||||
if (count($tmp) > 1) {
|
||||
throw new \RuntimeException('Multiple parts found with ID '.$id . ' ('.count($tmp).' found). This is basically a bug in Mousers API response. See issue #616.');
|
||||
throw new \RuntimeException('Multiple parts found with ID '.$id);
|
||||
}
|
||||
|
||||
return $tmp[0];
|
||||
return reset($tmp);
|
||||
}
|
||||
|
||||
public function getCapabilities(): array
|
||||
|
|
@ -341,4 +346,4 @@ class MouserProvider implements InfoProviderInterface
|
|||
|
||||
return $tmp;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -88,6 +88,8 @@ use App\Services\InfoProviderSystem\DTOs\PartDetailDTO;
|
|||
use App\Services\InfoProviderSystem\DTOs\PriceDTO;
|
||||
use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO;
|
||||
use App\Services\InfoProviderSystem\DTOs\ParameterDTO;
|
||||
use App\Settings\InfoProviderSystem\OEMSecretsSettings;
|
||||
use App\Settings\InfoProviderSystem\OEMSecretsSortMode;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
use Psr\Cache\CacheItemPoolInterface;
|
||||
|
||||
|
|
@ -99,12 +101,7 @@ class OEMSecretsProvider implements InfoProviderInterface
|
|||
|
||||
public function __construct(
|
||||
private readonly HttpClientInterface $oemsecretsClient,
|
||||
private readonly string $api_key,
|
||||
private readonly string $country_code,
|
||||
private readonly string $currency,
|
||||
private readonly string $zero_price,
|
||||
private readonly string $set_param,
|
||||
private readonly string $sort_criteria,
|
||||
private readonly OEMSecretsSettings $settings,
|
||||
private readonly CacheItemPoolInterface $partInfoCache
|
||||
)
|
||||
{
|
||||
|
|
@ -249,7 +246,8 @@ class OEMSecretsProvider implements InfoProviderInterface
|
|||
'name' => 'OEMSecrets',
|
||||
'description' => 'This provider uses the OEMSecrets API to search for parts.',
|
||||
'url' => 'https://www.oemsecrets.com/',
|
||||
'disabled_help' => 'Configure the API key in the PROVIDER_OEMSECRETS_KEY environment variable to enable.'
|
||||
'disabled_help' => 'Configure the API key in the provider settings to enable.',
|
||||
'settings_class' => OEMSecretsSettings::class
|
||||
];
|
||||
}
|
||||
/**
|
||||
|
|
@ -268,7 +266,7 @@ class OEMSecretsProvider implements InfoProviderInterface
|
|||
*/
|
||||
public function isActive(): bool
|
||||
{
|
||||
return $this->api_key !== '';
|
||||
return $this->settings->apiKey !== null && $this->settings->apiKey !== '';
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -288,18 +286,18 @@ class OEMSecretsProvider implements InfoProviderInterface
|
|||
public function searchByKeyword(string $keyword): array
|
||||
{
|
||||
/*
|
||||
oemsecrets Part Search API 3.0.1
|
||||
oemsecrets Part Search API 3.0.1
|
||||
|
||||
"https://oemsecretsapi.com/partsearch?
|
||||
searchTerm=BC547
|
||||
&apiKey=icawpb0bspoo2c6s64uv4vpdfp2vgr7e27bxw0yct2bzh87mpl027x353uelpq2x
|
||||
¤cy=EUR
|
||||
&countryCode=IT"
|
||||
|
||||
&countryCode=IT"
|
||||
|
||||
partsearch description:
|
||||
Use the Part Search API to find distributor data for a full or partial manufacturer
|
||||
Use the Part Search API to find distributor data for a full or partial manufacturer
|
||||
part number including part details, pricing, compliance and inventory.
|
||||
|
||||
|
||||
Required Parameter Format Description
|
||||
searchTerm string Part number you are searching for
|
||||
apiKey string Your unique API key provided to you by OEMsecrets
|
||||
|
|
@ -307,14 +305,14 @@ class OEMSecretsProvider implements InfoProviderInterface
|
|||
Additional Parameter Format Description
|
||||
countryCode string The country you want to output for
|
||||
currency string / array The currency you want the prices to be displayed as
|
||||
|
||||
|
||||
To display the output for GB and to view prices in USD, add [ countryCode=GB ] and [ currency=USD ]
|
||||
as seen below:
|
||||
oemsecretsapi.com/partsearch?apiKey=abcexampleapikey123&searchTerm=bd04&countryCode=GB¤cy=USD
|
||||
|
||||
|
||||
To view prices in both USD and GBP add [ currency[]=USD¤cy[]=GBP ]
|
||||
oemsecretsapi.com/partsearch?searchTerm=bd04&apiKey=abcexampleapikey123¤cy[]=USD¤cy[]=GBP
|
||||
|
||||
|
||||
*/
|
||||
|
||||
|
||||
|
|
@ -324,9 +322,9 @@ class OEMSecretsProvider implements InfoProviderInterface
|
|||
$response = $this->oemsecretsClient->request('GET', self::ENDPOINT_URL, [
|
||||
'query' => [
|
||||
'searchTerm' => $keyword,
|
||||
'apiKey' => $this->api_key,
|
||||
'currency' => $this->currency,
|
||||
'countryCode' => $this->country_code,
|
||||
'apiKey' => $this->settings->apiKey,
|
||||
'currency' => $this->settings->currency,
|
||||
'countryCode' => $this->settings->country,
|
||||
],
|
||||
]);
|
||||
|
||||
|
|
@ -533,7 +531,7 @@ class OEMSecretsProvider implements InfoProviderInterface
|
|||
|
||||
// Extract prices
|
||||
$priceDTOs = $this->getPrices($product);
|
||||
if (empty($priceDTOs) && (int)$this->zero_price === 0) {
|
||||
if (empty($priceDTOs) && !$this->settings->keepZeroPrices) {
|
||||
return null; // Skip products without valid prices
|
||||
}
|
||||
|
||||
|
|
@ -557,7 +555,7 @@ class OEMSecretsProvider implements InfoProviderInterface
|
|||
}
|
||||
|
||||
$imagesResults[$provider_id] = $this->getImages($product, $imagesResults[$provider_id] ?? []);
|
||||
if ($this->set_param == 1) {
|
||||
if ($this->settings->parseParams) {
|
||||
$parametersResults[$provider_id] = $this->getParameters($product, $parametersResults[$provider_id] ?? []);
|
||||
} else {
|
||||
$parametersResults[$provider_id] = [];
|
||||
|
|
@ -582,7 +580,7 @@ class OEMSecretsProvider implements InfoProviderInterface
|
|||
$regionB = $this->countryCodeToRegionMap[$countryCodeB] ?? '';
|
||||
|
||||
// If the map is empty or doesn't contain the key for $this->country_code, assign a placeholder region.
|
||||
$regionForEnvCountry = $this->countryCodeToRegionMap[$this->country_code] ?? '';
|
||||
$regionForEnvCountry = $this->countryCodeToRegionMap[$this->settings->country] ?? '';
|
||||
|
||||
// Convert to string before comparison to avoid mixed types
|
||||
$countryCodeA = (string) $countryCodeA;
|
||||
|
|
@ -599,9 +597,9 @@ class OEMSecretsProvider implements InfoProviderInterface
|
|||
}
|
||||
|
||||
// Step 1: country_code from the environment
|
||||
if ($countryCodeA === $this->country_code && $countryCodeB !== $this->country_code) {
|
||||
if ($countryCodeA === $this->settings->country && $countryCodeB !== $this->settings->country) {
|
||||
return -1;
|
||||
} elseif ($countryCodeA !== $this->country_code && $countryCodeB === $this->country_code) {
|
||||
} elseif ($countryCodeA !== $this->settings->country && $countryCodeB === $this->settings->country) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
|
@ -681,8 +679,8 @@ class OEMSecretsProvider implements InfoProviderInterface
|
|||
|
||||
if (is_array($prices)) {
|
||||
// Step 1: Check if prices exist in the preferred currency
|
||||
if (isset($prices[$this->currency]) && is_array($prices[$this->currency])) {
|
||||
$priceDetails = $prices[$this->currency];
|
||||
if (isset($prices[$this->settings->currency]) && is_array($prices[$this->settings->currency])) {
|
||||
$priceDetails = $prices[$this->$this->settings->currency];
|
||||
foreach ($priceDetails as $priceDetail) {
|
||||
if (
|
||||
is_array($priceDetail) &&
|
||||
|
|
@ -694,7 +692,7 @@ class OEMSecretsProvider implements InfoProviderInterface
|
|||
$priceDTOs[] = new PriceDTO(
|
||||
minimum_discount_amount: (float)$priceDetail['unit_break'],
|
||||
price: (string)$priceDetail['unit_price'],
|
||||
currency_iso_code: $this->currency,
|
||||
currency_iso_code: $this->settings->currency,
|
||||
includes_tax: false,
|
||||
price_related_quantity: 1.0
|
||||
);
|
||||
|
|
@ -1293,7 +1291,7 @@ class OEMSecretsProvider implements InfoProviderInterface
|
|||
private function sortResultsData(array &$resultsData, string $searchKeyword): void
|
||||
{
|
||||
// If the SORT_CRITERIA is not 'C' or 'M', do not sort
|
||||
if ($this->sort_criteria !== 'C' && $this->sort_criteria !== 'M') {
|
||||
if ($this->settings->sortMode !== OEMSecretsSortMode::COMPLETENESS && $this->settings->sortMode !== OEMSecretsSortMode::MANUFACTURER) {
|
||||
return;
|
||||
}
|
||||
usort($resultsData, function ($a, $b) use ($searchKeyword) {
|
||||
|
|
@ -1332,9 +1330,9 @@ class OEMSecretsProvider implements InfoProviderInterface
|
|||
}
|
||||
|
||||
// Final sorting: by completeness or manufacturer, if necessary
|
||||
if ($this->sort_criteria === 'C') {
|
||||
if ($this->settings->sortMode === OEMSecretsSortMode::COMPLETENESS) {
|
||||
return $this->compareByCompleteness($a, $b);
|
||||
} elseif ($this->sort_criteria === 'M') {
|
||||
} elseif ($this->settings->sortMode === OEMSecretsSortMode::MANUFACTURER) {
|
||||
return strcasecmp($a->manufacturer, $b->manufacturer);
|
||||
}
|
||||
|
||||
|
|
@ -1468,4 +1466,4 @@ class OEMSecretsProvider implements InfoProviderInterface
|
|||
return $url;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ use App\Services\InfoProviderSystem\DTOs\PartDetailDTO;
|
|||
use App\Services\InfoProviderSystem\DTOs\PriceDTO;
|
||||
use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO;
|
||||
use App\Services\OAuth\OAuthTokenManager;
|
||||
use App\Settings\InfoProviderSystem\OctopartSettings;
|
||||
use Psr\Cache\CacheItemPoolInterface;
|
||||
use Symfony\Component\HttpClient\HttpOptions;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
|
@ -114,9 +115,8 @@ class OctopartProvider implements InfoProviderInterface
|
|||
|
||||
public function __construct(private readonly HttpClientInterface $httpClient,
|
||||
private readonly OAuthTokenManager $authTokenManager, private readonly CacheItemPoolInterface $partInfoCache,
|
||||
private readonly string $clientId, private readonly string $secret,
|
||||
private readonly string $currency, private readonly string $country,
|
||||
private readonly int $search_limit, private readonly bool $onlyAuthorizedSellers)
|
||||
private readonly OctopartSettings $settings,
|
||||
)
|
||||
{
|
||||
|
||||
}
|
||||
|
|
@ -170,7 +170,8 @@ class OctopartProvider implements InfoProviderInterface
|
|||
'name' => 'Octopart',
|
||||
'description' => 'This provider uses the Nexar/Octopart API to search for parts on Octopart.',
|
||||
'url' => 'https://www.octopart.com/',
|
||||
'disabled_help' => 'Set the PROVIDER_OCTOPART_CLIENT_ID and PROVIDER_OCTOPART_SECRET env option.'
|
||||
'disabled_help' => 'Set the Client ID and Secret in provider settings.',
|
||||
'settings_class' => OctopartSettings::class
|
||||
];
|
||||
}
|
||||
|
||||
|
|
@ -183,7 +184,8 @@ class OctopartProvider implements InfoProviderInterface
|
|||
{
|
||||
//The client ID has to be set and a token has to be available (user clicked connect)
|
||||
//return /*!empty($this->clientId) && */ $this->authTokenManager->hasToken(self::OAUTH_APP_NAME);
|
||||
return $this->clientId !== '' && $this->secret !== '';
|
||||
return $this->settings->clientId !== null && $this->settings->clientId !== ''
|
||||
&& $this->settings->secret !== null && $this->settings->secret !== '';
|
||||
}
|
||||
|
||||
private function mapLifeCycleStatus(?string $value): ?ManufacturingStatus
|
||||
|
|
@ -337,7 +339,7 @@ class OctopartProvider implements InfoProviderInterface
|
|||
) {
|
||||
hits
|
||||
results {
|
||||
part
|
||||
part
|
||||
%s
|
||||
}
|
||||
}
|
||||
|
|
@ -347,10 +349,10 @@ class OctopartProvider implements InfoProviderInterface
|
|||
|
||||
$result = $this->makeGraphQLCall($graphQL, [
|
||||
'keyword' => $keyword,
|
||||
'limit' => $this->search_limit,
|
||||
'currency' => $this->currency,
|
||||
'country' => $this->country,
|
||||
'authorizedOnly' => $this->onlyAuthorizedSellers,
|
||||
'limit' => $this->settings->searchLimit,
|
||||
'currency' => $this->settings->currency,
|
||||
'country' => $this->settings->country,
|
||||
'authorizedOnly' => $this->settings->onlyAuthorizedSellers,
|
||||
]);
|
||||
|
||||
$tmp = [];
|
||||
|
|
@ -383,9 +385,9 @@ class OctopartProvider implements InfoProviderInterface
|
|||
|
||||
$result = $this->makeGraphQLCall($graphql, [
|
||||
'ids' => [$id],
|
||||
'currency' => $this->currency,
|
||||
'country' => $this->country,
|
||||
'authorizedOnly' => $this->onlyAuthorizedSellers,
|
||||
'currency' => $this->settings->currency,
|
||||
'country' => $this->settings->country,
|
||||
'authorizedOnly' => $this->settings->onlyAuthorizedSellers,
|
||||
]);
|
||||
|
||||
$tmp = $this->partResultToDTO($result['data']['supParts'][0]);
|
||||
|
|
@ -403,4 +405,4 @@ class OctopartProvider implements InfoProviderInterface
|
|||
ProviderCapabilities::PRICE,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ 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\PollinSettings;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
use Symfony\Component\DomCrawler\Crawler;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
|
@ -39,8 +40,7 @@ class PollinProvider implements InfoProviderInterface
|
|||
{
|
||||
|
||||
public function __construct(private readonly HttpClientInterface $client,
|
||||
#[Autowire(env: 'bool:PROVIDER_POLLIN_ENABLED')]
|
||||
private readonly bool $enabled = true,
|
||||
private readonly PollinSettings $settings,
|
||||
)
|
||||
{
|
||||
}
|
||||
|
|
@ -49,9 +49,10 @@ class PollinProvider implements InfoProviderInterface
|
|||
{
|
||||
return [
|
||||
'name' => 'Pollin',
|
||||
'description' => 'Webscrapping from pollin.de to get part information',
|
||||
'url' => 'https://www.reichelt.de/',
|
||||
'disabled_help' => 'Set PROVIDER_POLLIN_ENABLED env to 1'
|
||||
'description' => 'Webscraping from pollin.de to get part information',
|
||||
'url' => 'https://www.pollin.de/',
|
||||
'disabled_help' => 'Enable the provider in provider settings',
|
||||
'settings_class' => PollinSettings::class,
|
||||
];
|
||||
}
|
||||
|
||||
|
|
@ -62,7 +63,7 @@ class PollinProvider implements InfoProviderInterface
|
|||
|
||||
public function isActive(): bool
|
||||
{
|
||||
return $this->enabled;
|
||||
return $this->settings->enabled;
|
||||
}
|
||||
|
||||
public function searchByKeyword(string $keyword): array
|
||||
|
|
@ -157,7 +158,8 @@ class PollinProvider implements InfoProviderInterface
|
|||
category: $this->parseCategory($dom),
|
||||
manufacturer: $dom->filter('meta[property="product:brand"]')->count() > 0 ? $dom->filter('meta[property="product:brand"]')->attr('content') : null,
|
||||
preview_image_url: $dom->filter('meta[property="og:image"]')->attr('content'),
|
||||
manufacturing_status: $this->mapAvailability($dom->filter('link[itemprop="availability"]')->attr('href')),
|
||||
//TODO: Find another way to determine the manufacturing status, as the itemprop="availability" is often is not existing anymore in the page
|
||||
//manufacturing_status: $this->mapAvailability($dom->filter('link[itemprop="availability"]')->attr('href')),
|
||||
provider_url: $productPageUrl,
|
||||
notes: $this->parseNotes($dom),
|
||||
datasheets: $this->parseDatasheets($dom),
|
||||
|
|
@ -215,7 +217,7 @@ class PollinProvider implements InfoProviderInterface
|
|||
private function parseNotes(Crawler $dom): string
|
||||
{
|
||||
//Concat product highlights and product description
|
||||
return $dom->filter('div.product-detail-top-features')->html() . '<br><br>' . $dom->filter('div.product-detail-description-text')->html();
|
||||
return $dom->filter('div.product-detail-top-features')->html('') . '<br><br>' . $dom->filter('div.product-detail-description-text')->html('');
|
||||
}
|
||||
|
||||
private function parsePrices(Crawler $dom): array
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ 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\ReicheltSettings;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
use Symfony\Component\DomCrawler\Crawler;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
|
@ -39,16 +40,7 @@ class ReicheltProvider implements InfoProviderInterface
|
|||
public const DISTRIBUTOR_NAME = "Reichelt";
|
||||
|
||||
public function __construct(private readonly HttpClientInterface $client,
|
||||
#[Autowire(env: "bool:PROVIDER_REICHELT_ENABLED")]
|
||||
private readonly bool $enabled = true,
|
||||
#[Autowire(env: "PROVIDER_REICHELT_LANGUAGE")]
|
||||
private readonly string $language = "en",
|
||||
#[Autowire(env: "PROVIDER_REICHELT_COUNTRY")]
|
||||
private readonly string $country = "DE",
|
||||
#[Autowire(env: "PROVIDER_REICHELT_INCLUDE_VAT")]
|
||||
private readonly bool $includeVAT = false,
|
||||
#[Autowire(env: "PROVIDER_REICHELT_CURRENCY")]
|
||||
private readonly string $currency = "EUR",
|
||||
private readonly ReicheltSettings $settings,
|
||||
)
|
||||
{
|
||||
}
|
||||
|
|
@ -57,9 +49,10 @@ class ReicheltProvider implements InfoProviderInterface
|
|||
{
|
||||
return [
|
||||
'name' => 'Reichelt',
|
||||
'description' => 'Webscrapping from reichelt.com to get part information',
|
||||
'description' => 'Webscraping from reichelt.com to get part information',
|
||||
'url' => 'https://www.reichelt.com/',
|
||||
'disabled_help' => 'Set PROVIDER_REICHELT_ENABLED env to 1'
|
||||
'disabled_help' => 'Enable provider in provider settings.',
|
||||
'settings_class' => ReicheltSettings::class,
|
||||
];
|
||||
}
|
||||
|
||||
|
|
@ -70,7 +63,7 @@ class ReicheltProvider implements InfoProviderInterface
|
|||
|
||||
public function isActive(): bool
|
||||
{
|
||||
return $this->enabled;
|
||||
return $this->settings->enabled;
|
||||
}
|
||||
|
||||
public function searchByKeyword(string $keyword): array
|
||||
|
|
@ -121,8 +114,8 @@ class ReicheltProvider implements InfoProviderInterface
|
|||
sprintf(
|
||||
'https://www.reichelt.com/?ACTION=514&id=74&article=%s&LANGUAGE=%s&CCOUNTRY=%s',
|
||||
$id,
|
||||
strtoupper($this->language),
|
||||
strtoupper($this->country)
|
||||
strtoupper($this->settings->language),
|
||||
strtoupper($this->settings->country)
|
||||
)
|
||||
);
|
||||
$json = $response->toArray();
|
||||
|
|
@ -133,8 +126,8 @@ class ReicheltProvider implements InfoProviderInterface
|
|||
|
||||
$response = $this->client->request('GET', $productPage, [
|
||||
'query' => [
|
||||
'CCTYPE' => $this->includeVAT ? 'private' : 'business',
|
||||
'currency' => $this->currency,
|
||||
'CCTYPE' => $this->settings->includeVAT ? 'private' : 'business',
|
||||
'currency' => $this->settings->currency,
|
||||
],
|
||||
]);
|
||||
$html = $response->getContent();
|
||||
|
|
@ -158,7 +151,7 @@ class ReicheltProvider implements InfoProviderInterface
|
|||
distributor_name: self::DISTRIBUTOR_NAME,
|
||||
order_number: $json[0]['article_artnr'],
|
||||
prices: array_merge(
|
||||
[new PriceDTO(1.0, $priceString, $currency, $this->includeVAT)]
|
||||
[new PriceDTO(1.0, $priceString, $currency, $this->settings->includeVAT)]
|
||||
, $this->parseBatchPrices($dom, $currency)),
|
||||
product_url: $productPage
|
||||
);
|
||||
|
|
@ -218,7 +211,7 @@ class ReicheltProvider implements InfoProviderInterface
|
|||
//Strip any non-numeric characters
|
||||
$priceString = preg_replace('/[^0-9.]/', '', $priceString);
|
||||
|
||||
$prices[] = new PriceDTO($minAmount, $priceString, $currency, $this->includeVAT);
|
||||
$prices[] = new PriceDTO($minAmount, $priceString, $currency, $this->settings->includeVAT);
|
||||
});
|
||||
|
||||
return $prices;
|
||||
|
|
@ -270,7 +263,7 @@ class ReicheltProvider implements InfoProviderInterface
|
|||
private function getBaseURL(): string
|
||||
{
|
||||
//Without the trailing slash
|
||||
return 'https://www.reichelt.com/' . strtolower($this->country) . '/' . strtolower($this->language);
|
||||
return 'https://www.reichelt.com/' . strtolower($this->settings->country) . '/' . strtolower($this->settings->language);
|
||||
}
|
||||
|
||||
public function getCapabilities(): array
|
||||
|
|
@ -282,4 +275,4 @@ class ReicheltProvider implements InfoProviderInterface
|
|||
ProviderCapabilities::PRICE,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Services\InfoProviderSystem\Providers;
|
||||
|
||||
use App\Settings\InfoProviderSystem\TMESettings;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
use Symfony\Contracts\HttpClient\ResponseInterface;
|
||||
|
||||
|
|
@ -30,15 +31,15 @@ class TMEClient
|
|||
{
|
||||
public const BASE_URI = 'https://api.tme.eu';
|
||||
|
||||
public function __construct(private readonly HttpClientInterface $tmeClient, private readonly string $token, private readonly string $secret)
|
||||
public function __construct(private readonly HttpClientInterface $tmeClient, private readonly TMESettings $settings)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public function makeRequest(string $action, array $parameters): ResponseInterface
|
||||
{
|
||||
$parameters['Token'] = $this->token;
|
||||
$parameters['ApiSignature'] = $this->getSignature($action, $parameters, $this->secret);
|
||||
$parameters['Token'] = $this->settings->apiToken;
|
||||
$parameters['ApiSignature'] = $this->getSignature($action, $parameters, $this->settings->apiSecret);
|
||||
|
||||
return $this->tmeClient->request('POST', $this->getUrlForAction($action), [
|
||||
'body' => $parameters,
|
||||
|
|
@ -47,7 +48,7 @@ class TMEClient
|
|||
|
||||
public function isUsable(): bool
|
||||
{
|
||||
return $this->token !== '' && $this->secret !== '';
|
||||
return !($this->settings->apiToken === null || $this->settings->apiSecret === null);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -58,7 +59,7 @@ class TMEClient
|
|||
public function isUsingPrivateToken(): bool
|
||||
{
|
||||
//Private tokens are longer than anonymous ones (50 instead of 45 characters)
|
||||
return strlen($this->token) > 45;
|
||||
return strlen($this->settings->apiToken ?? '') > 45;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -93,4 +94,4 @@ class TMEClient
|
|||
|
||||
return $params;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,24 +30,21 @@ 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\TMESettings;
|
||||
|
||||
class TMEProvider implements InfoProviderInterface
|
||||
{
|
||||
|
||||
private const VENDOR_NAME = 'TME';
|
||||
|
||||
/** @var bool If true, the prices are gross prices. If false, the prices are net prices. */
|
||||
private readonly bool $get_gross_prices;
|
||||
|
||||
public function __construct(private readonly TMEClient $tmeClient, private readonly string $country,
|
||||
private readonly string $language, private readonly string $currency,
|
||||
bool $get_gross_prices)
|
||||
public function __construct(private readonly TMEClient $tmeClient, private readonly TMESettings $settings)
|
||||
{
|
||||
//If we have a private token, set get_gross_prices to false, as it is automatically determined by the account type then
|
||||
if ($this->tmeClient->isUsingPrivateToken()) {
|
||||
$this->get_gross_prices = false;
|
||||
} else {
|
||||
$this->get_gross_prices = $get_gross_prices;
|
||||
$this->get_gross_prices = $this->settings->grossPrices;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -57,7 +54,8 @@ class TMEProvider implements InfoProviderInterface
|
|||
'name' => 'TME',
|
||||
'description' => 'This provider uses the API of TME (Transfer Multipart).',
|
||||
'url' => 'https://tme.eu/',
|
||||
'disabled_help' => 'Configure the PROVIDER_TME_KEY and PROVIDER_TME_SECRET environment variables to use this provider.'
|
||||
'disabled_help' => 'Configure the API Token and secret in provider settings to use this provider.',
|
||||
'settings_class' => TMESettings::class
|
||||
];
|
||||
}
|
||||
|
||||
|
|
@ -74,8 +72,8 @@ class TMEProvider implements InfoProviderInterface
|
|||
public function searchByKeyword(string $keyword): array
|
||||
{
|
||||
$response = $this->tmeClient->makeRequest('Products/Search', [
|
||||
'Country' => $this->country,
|
||||
'Language' => $this->language,
|
||||
'Country' => $this->settings->country,
|
||||
'Language' => $this->settings->language,
|
||||
'SearchPlain' => $keyword,
|
||||
]);
|
||||
|
||||
|
|
@ -104,8 +102,8 @@ class TMEProvider implements InfoProviderInterface
|
|||
public function getDetails(string $id): PartDetailDTO
|
||||
{
|
||||
$response = $this->tmeClient->makeRequest('Products/GetProducts', [
|
||||
'Country' => $this->country,
|
||||
'Language' => $this->language,
|
||||
'Country' => $this->settings->country,
|
||||
'Language' => $this->settings->language,
|
||||
'SymbolList' => [$id],
|
||||
]);
|
||||
|
||||
|
|
@ -149,8 +147,8 @@ class TMEProvider implements InfoProviderInterface
|
|||
public function getFiles(string $id): array
|
||||
{
|
||||
$response = $this->tmeClient->makeRequest('Products/GetProductsFiles', [
|
||||
'Country' => $this->country,
|
||||
'Language' => $this->language,
|
||||
'Country' => $this->settings->country,
|
||||
'Language' => $this->settings->language,
|
||||
'SymbolList' => [$id],
|
||||
]);
|
||||
|
||||
|
|
@ -191,9 +189,9 @@ class TMEProvider implements InfoProviderInterface
|
|||
public function getVendorInfo(string $id, ?string $productURL = null): PurchaseInfoDTO
|
||||
{
|
||||
$response = $this->tmeClient->makeRequest('Products/GetPricesAndStocks', [
|
||||
'Country' => $this->country,
|
||||
'Language' => $this->language,
|
||||
'Currency' => $this->currency,
|
||||
'Country' => $this->settings->country,
|
||||
'Language' => $this->settings->language,
|
||||
'Currency' => $this->settings->currency,
|
||||
'GrossPrices' => $this->get_gross_prices,
|
||||
'SymbolList' => [$id],
|
||||
]);
|
||||
|
|
@ -234,8 +232,8 @@ class TMEProvider implements InfoProviderInterface
|
|||
public function getParameters(string $id, string|null &$footprint_name = null): array
|
||||
{
|
||||
$response = $this->tmeClient->makeRequest('Products/GetParameters', [
|
||||
'Country' => $this->country,
|
||||
'Language' => $this->language,
|
||||
'Country' => $this->settings->country,
|
||||
'Language' => $this->settings->language,
|
||||
'SymbolList' => [$id],
|
||||
]);
|
||||
|
||||
|
|
@ -298,4 +296,4 @@ class TMEProvider implements InfoProviderInterface
|
|||
ProviderCapabilities::PRICE,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue