diff --git a/src/Services/InfoProviderSystem/Providers/TMEClient.php b/src/Services/InfoProviderSystem/Providers/TMEClient.php
index ae2ab0d14..a2d1bcd0d 100644
--- a/src/Services/InfoProviderSystem/Providers/TMEClient.php
+++ b/src/Services/InfoProviderSystem/Providers/TMEClient.php
@@ -24,6 +24,9 @@
namespace App\Services\InfoProviderSystem\Providers;
use App\Settings\InfoProviderSystem\TMESettings;
+use Symfony\Component\DependencyInjection\Attribute\Autowire;
+use Symfony\Contracts\Cache\CacheInterface;
+use Symfony\Contracts\Cache\ItemInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
@@ -31,67 +34,73 @@ class TMEClient
{
public const BASE_URI = 'https://api.tme.eu';
- public function __construct(private readonly HttpClientInterface $tmeClient, private readonly TMESettings $settings)
+ public function __construct(
+ private readonly HttpClientInterface $tmeClient,
+ private readonly TMESettings $settings,
+ #[Autowire(service: 'info_provider.cache')]
+ private readonly CacheInterface $cache
+ )
{
}
- public function makeRequest(string $action, array $parameters): ResponseInterface
+ /*
+ * Make a request to the TME API.
+ * Transparently handles session token generation and renewal.
+ * @return bool true if the client is usable
+ */
+ public function makeRequest(string $endpoint, array $parameters): ResponseInterface
{
- $parameters['Token'] = $this->settings->apiToken;
- $parameters['ApiSignature'] = $this->getSignature($action, $parameters, $this->settings->apiSecret);
+ $session_token = $this->getSessionToken();
- return $this->tmeClient->request('POST', $this->getUrlForAction($action), [
- 'body' => $parameters,
+ return $this->tmeClient->request('GET', $this->getUrlForEndpoint($endpoint), [
+ 'headers' => [
+ 'Authorization' => 'Bearer ' . $session_token
+ ],
+ 'query' => $parameters
]);
}
+ /*
+ * Checks if the current settings allow the client to be used.
+ * @return bool true if the client is usable
+ */
public function isUsable(): bool
{
return !($this->settings->apiToken === null || $this->settings->apiSecret === null);
}
- /**
- * Returns true if the client is using a private (account related token) instead of a deprecated anonymous token
- * to authenticate with TME.
- * @return bool
- */
- public function isUsingPrivateToken(): bool
+ private function getUrlForEndpoint(string $endpoint): string
{
- //Private tokens are longer than anonymous ones (50 instead of 45 characters)
- return strlen($this->settings->apiToken ?? '') > 45;
+ return self::BASE_URI . '/' . ltrim($endpoint, '/');
}
- /**
- * Generates the signature for the given action and parameters.
- * Taken from https://github.com/tme-dev/TME-API/blob/master/PHP/basic/using_curl.php
+ /*
+ * Gets the session token from the local cache, or requests a new one if it is nonexistent
+ * or expired.
+ * @return string valid session token
*/
- public function getSignature(string $action, array $parameters, string $appSecret): string
+ private function getSessionToken(): string
{
- $parameters = $this->sortSignatureParams($parameters);
+ return $this->cache->get($this->getTokenCacheKey(), function (ItemInterface $item): string {
- $queryString = http_build_query($parameters, '', '&', PHP_QUERY_RFC3986);
- $signatureBase = strtoupper('POST') .
- '&' . rawurlencode($this->getUrlForAction($action)) . '&' . rawurlencode($queryString);
+ // Request new session from the API
+ $response = $this->tmeClient->request('POST', $this->getUrlForEndpoint('/auth/token'), [
+ 'auth_basic' => [$this->settings->apiToken, $this->settings->apiSecret],
+ 'body' => ['grant_type' => 'client_credentials'],
+ ]);
- return base64_encode(hash_hmac('sha1', $signatureBase, $appSecret, true));
- }
+ $data = $response->toArray();
- private function getUrlForAction(string $action): string
- {
- return self::BASE_URI . '/' . $action . '.json';
+ // Subtract 30s as a safety buffer
+ $item->expiresAfter($data['expires_in'] - 30);
+
+ return $data['access_token'];
+ });
}
- private function sortSignatureParams(array $params): array
+ private function getTokenCacheKey(): string
{
- ksort($params);
-
- foreach ($params as &$value) {
- if (is_array($value)) {
- $value = $this->sortSignatureParams($value);
- }
- }
-
- return $params;
+ return 'tme_api_v2_token_' . hash('xxh3', $this->settings->apiToken . $this->settings->apiSecret);
}
}
diff --git a/src/Services/InfoProviderSystem/Providers/TMEProvider.php b/src/Services/InfoProviderSystem/Providers/TMEProvider.php
index 24ba0ea78..c3776d462 100644
--- a/src/Services/InfoProviderSystem/Providers/TMEProvider.php
+++ b/src/Services/InfoProviderSystem/Providers/TMEProvider.php
@@ -37,15 +37,8 @@ class TMEProvider implements InfoProviderInterface, URLHandlerInfoProviderInterf
private const VENDOR_NAME = 'TME';
- private readonly 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 = $this->settings->grossPrices;
- }
}
public function getProviderInfo(): array
@@ -71,28 +64,33 @@ public function isActive(): bool
public function searchByKeyword(string $keyword, array $options = []): array
{
- $response = $this->tmeClient->makeRequest('Products/Search', [
- 'Country' => $this->settings->country,
- 'Language' => $this->settings->language,
- 'SearchPlain' => $keyword,
+ $response = $this->tmeClient->makeRequest('/products/search', [
+ 'country' => $this->settings->country,
+ 'scope' => ['products'],
+ 'phrase' => $keyword,
+ 'limit' => 100,
]);
- $data = $response->toArray()['Data'];
+ $data = $response->toArray()['data'];
$result = [];
- foreach($data['ProductList'] as $product) {
+ foreach($data['products']['elements'] ?? [] as $product) {
+
+ $symbol = $product['symbol'];
+
$result[] = new SearchResultDTO(
provider_key: $this->getProviderKey(),
- provider_id: $product['Symbol'],
- name: empty($product['OriginalSymbol']) ? $product['Symbol'] : $product['OriginalSymbol'],
- description: $product['Description'],
- category: $product['Category'],
- manufacturer: $product['Producer'],
- mpn: $product['OriginalSymbol'] ?? null,
- preview_image_url: $this->normalizeURL($product['Photo']),
- manufacturing_status: $this->productStatusArrayToManufacturingStatus($product['ProductStatusList']),
- provider_url: $this->normalizeURL($product['ProductInformationPage']),
+ provider_id: $symbol,
+ name: empty($product['manufacturer_symbols']) ? $product['symbol'] : $product['manufacturer_symbols'][0],
+ description: $product['description'],
+ category: $product['category']['name'] ?? null,
+ manufacturer: $product['manufacturer']['name'] ?? null,
+ mpn: $product['manufacturer_symbols'][0] ?? null,
+ preview_image_url: $this->normalizeURL($product['assets']['primary_photo']['prime'] ?? null),
+ manufacturing_status: $this->productStatusArrayToManufacturingStatus($product['product_status'] ?? null),
+ provider_url: $this->normalizeURL($this->constructPartInfoUrl($symbol)),
+ gtin: $product['ean'] ?? null
);
}
@@ -101,43 +99,72 @@ public function searchByKeyword(string $keyword, array $options = []): array
public function getDetails(string $id, array $options = []): PartDetailDTO
{
- $response = $this->tmeClient->makeRequest('Products/GetProducts', [
- 'Country' => $this->settings->country,
- 'Language' => $this->settings->language,
- 'SymbolList' => [$id],
+ $response = $this->tmeClient->makeRequest('products', [
+ 'country' => $this->settings->country,
+ 'symbols' => [$id],
]);
- $product = $response->toArray()['Data']['ProductList'][0];
+ $product = $response->toArray()['data']['elements'][0];
- //Add a explicit https:// to the url if it is missing
- $productInfoPage = $this->normalizeURL($product['ProductInformationPage']);
+ $product_page = $this->normalizeURL($this->constructPartInfoUrl($id));
+ // Get additional product data
$files = $this->getFiles($id);
-
- $footprint = null;
-
$parameters = $this->getParameters($id, $footprint);
+ $vendor_info = $this->getVendorInfo($id, $product_page);
return new PartDetailDTO(
provider_key: $this->getProviderKey(),
- provider_id: $product['Symbol'],
- name: empty($product['OriginalSymbol']) ? $product['Symbol'] : $product['OriginalSymbol'],
- description: $product['Description'],
- category: $product['Category'],
- manufacturer: $product['Producer'],
- mpn: $product['OriginalSymbol'] ?? null,
- preview_image_url: $this->normalizeURL($product['Photo']),
- manufacturing_status: $this->productStatusArrayToManufacturingStatus($product['ProductStatusList']),
- provider_url: $productInfoPage,
- footprint: $footprint,
+ provider_id: $id,
+ name: empty($product['manufacturer_symbols']) ? $product['symbol'] : $product['manufacturer_symbols'][0],
+ description: $product['description'],
+ category: $product['category']['name'] ?? null,
+ manufacturer: $product['manufacturer']['name'] ?? null,
+ mpn: $product['manufacturer_symbols'][0] ?? null,
+ preview_image_url: $this->normalizeURL($product['assets']['primary_photo']['high_resolution'] ??
+ $product['assets']['primary_photo']['prime'] ?? null),
+ manufacturing_status: $this->productStatusArrayToManufacturingStatus($product['product_status'] ?? null),
+ provider_url: $this->normalizeURL($this->constructPartInfoUrl($id)),
+ gtin: $product['ean'] ?? null,
+
+ footprint: $parameters['footprint'],
datasheets: $files['datasheets'],
images: $files['images'],
- parameters: $parameters,
- vendor_infos: [$this->getVendorInfo($id, $productInfoPage)],
- mass: $product['WeightUnit'] === 'g' ? $product['Weight'] : null,
+ parameters: $parameters['parameters'],
+ vendor_infos: [$vendor_info],
+ mass: ($product['weight']['unit'] ?? null) === 'g' ? $product['weight']['value'] : null,
);
}
+ public function getCapabilities(): array
+ {
+ return [
+ ProviderCapabilities::BASIC,
+ ProviderCapabilities::FOOTPRINT,
+ ProviderCapabilities::PICTURE,
+ ProviderCapabilities::DATASHEET,
+ ProviderCapabilities::PRICE,
+ ];
+ }
+
+ public function getHandledDomains(): array
+ {
+ return ['tme.eu'];
+ }
+
+ public function getIDFromURL(string $url): ?string
+ {
+ //Input: https://www.tme.eu/de/details/fi321_se/kuhler/alutronic/
+ //The ID is the part after the details segment and before the next slash
+
+ $matches = [];
+ if (preg_match('#/details/([^/]+)/#', $url, $matches) === 1) {
+ return $matches[1];
+ }
+
+ return null;
+ }
+
/**
* Fetches all files for a given product id
* @param string $id
@@ -146,115 +173,132 @@ public function getDetails(string $id, array $options = []): PartDetailDTO
*/
public function getFiles(string $id): array
{
- $response = $this->tmeClient->makeRequest('Products/GetProductsFiles', [
- 'Country' => $this->settings->country,
- 'Language' => $this->settings->language,
- 'SymbolList' => [$id],
+ $response = $this->tmeClient->makeRequest('products/files', [
+ 'country' => $this->settings->country,
+ 'symbols' => [$id],
]);
- $data = $response->toArray()['Data'];
- $files = $data['ProductList'][0]['Files'];
+ $files = $response->toArray()['data']['elements'][0];
//Extract datasheets
- $documentList = $files['DocumentList'];
+ $documents = $files['documents']['elements'];
$datasheets = [];
- foreach($documentList as $document) {
- $datasheets[] = new FileDTO(
- url: $this->normalizeURL($document['DocumentUrl']),
- );
+ foreach ($documents as $document) {
+
+ // Check document type
+ $validDocumentTypes = ['INS', 'DTE', 'KCH', 'GWA', 'INB', 'PRE'];
+ if (in_array($document['type'], $validDocumentTypes, true)) {
+
+ $datasheets[] = new FileDTO(
+ url: $this->normalizeURL($document['url']),
+ name: $document['file_name']
+ );
+
+ }
}
//Extract images
- $imageList = $files['AdditionalPhotoList'];
- $images = [];
- foreach($imageList as $image) {
- $images[] = new FileDTO(
- url: $this->normalizeURL($image['HighResolutionPhoto']),
- );
+ $images = $files['assets']['additional']['elements'];
+ $image_dtos = [];
+ foreach($images as $image) {
+ $image_url = $this->normalizeURL($image['high_resolution'] ?? $image['prime'] ?? null);
+ $image_dtos[] = new FileDTO(url: $image_url);
}
-
return [
'datasheets' => $datasheets,
- 'images' => $images,
+ 'images' => $image_dtos,
];
}
/**
- * Fetches the vendor/purchase information for a given product id.
+ * Fetches the parameters of a product
* @param string $id
- * @param string|null $productURL
- * @return PurchaseInfoDTO
+ * @return array {footprint: string, parameters: ParameterDTO[]}
*/
- public function getVendorInfo(string $id, ?string $productURL = null): PurchaseInfoDTO
+ public function getParameters(string $id, string|null &$footprint_name = null): array
{
- $response = $this->tmeClient->makeRequest('Products/GetPricesAndStocks', [
- 'Country' => $this->settings->country,
- 'Language' => $this->settings->language,
- 'Currency' => $this->settings->currency,
- 'GrossPrices' => $this->get_gross_prices,
- 'SymbolList' => [$id],
+ $response = $this->tmeClient->makeRequest('/products/parameters', [
+ 'country' => $this->settings->country,
+ 'symbols' => [$id],
]);
- $data = $response->toArray()['Data'];
- $currency = $data['Currency'];
- $include_tax = $data['PriceType'] === 'GROSS';
+ $parameters = $response->toArray()['data']['elements'][0]['parameters']['elements'];
+ $parameters_dtos = [];
- $product = $response->toArray()['Data']['ProductList'][0];
- $vendor_order_number = $product['Symbol'];
- $priceList = $product['PriceList'];
+ $footprint = null;
+ $footprint_imperial = null;
+ $footprint_metric = null;
- $prices = [];
- foreach ($priceList as $price) {
- $prices[] = new PriceDTO(
- minimum_discount_amount: $price['Amount'],
- price: (string) $price['PriceValue'],
- currency_iso_code: $currency,
- includes_tax: $include_tax,
- );
+ foreach($parameters as $parameter) {
+
+ $id = $parameter['id'];
+ $value = $parameter['values'][0]['value'];
+
+ $parameters_dtos[] = ParameterDTO::parseValueIncludingUnit($parameter['name'], $value);
+
+ // Check if the parameter is the case/footprint
+ // id 35 is Case, id 2932 is Case-inch, id 2931 is Case-mm
+ if ($id === 35) {
+ $footprint = $value;
+ } else if ($id == 2932) {
+ $footprint_imperial = $value;
+ } else if ($id == 2931) {
+ $footprint_metric = $value;
+ }
}
- return new PurchaseInfoDTO(
- distributor_name: self::VENDOR_NAME,
- order_number: $vendor_order_number,
- prices: $prices,
- product_url: $productURL,
- );
+ // Select correct footprint
+ $footprint = $this->settings->preferMetricFootprint ?
+ ($footprint_metric ?? $footprint ?? $footprint_imperial) :
+ ($footprint_imperial ?? $footprint ?? $footprint_metric);
+
+ return [
+ 'footprint' => $footprint,
+ 'parameters' => $parameters_dtos
+ ];
}
/**
- * Fetches the parameters of a product
+ * Fetches the vendor/purchase information for a given product id.
* @param string $id
- * @param string|null $footprint_name You can pass a variable by reference, where the name of the footprint will be stored
- * @return ParameterDTO[]
+ * @param string|null $productURL
+ * @return PurchaseInfoDTO
*/
- public function getParameters(string $id, string|null &$footprint_name = null): array
+ public function getVendorInfo(string $id, ?string $productURL = null): PurchaseInfoDTO
{
- $response = $this->tmeClient->makeRequest('Products/GetParameters', [
- 'Country' => $this->settings->country,
- 'Language' => $this->settings->language,
- 'SymbolList' => [$id],
+ $response = $this->tmeClient->makeRequest('/products/data', [
+ 'country' => $this->settings->country,
+ 'currency' => $this->settings->currency,
+ 'scope' => ['prices'],
+ 'symbols' => [$id],
]);
- $data = $response->toArray()['Data']['ProductList'][0];
+ $prices = $response->toArray()['data']['elements'][0]['prices'];
- $result = [];
-
- $footprint_name = null;
+ $currency = $prices['currency'];
+ $include_tax = $prices['type'] === 'GROSS';
- foreach($data['ParameterList'] as $parameter) {
- $result[] = ParameterDTO::parseValueIncludingUnit($parameter['ParameterName'], $parameter['ParameterValue']);
-
- //Check if the parameter is the case/footprint
- if ($parameter['ParameterId'] === 35) {
- $footprint_name = $parameter['ParameterValue'];
- }
+ $prices_dtos = [];
+ foreach ($prices['elements'] as $price) {
+ $prices_dtos[] = new PriceDTO(
+ minimum_discount_amount: $price['amount'],
+ price: (string) $price['price'],
+ currency_iso_code: $currency,
+ includes_tax: $include_tax,
+ );
}
- return $result;
+ return new PurchaseInfoDTO(
+ distributor_name: self::VENDOR_NAME,
+ order_number: $id,
+ prices: $prices_dtos,
+ product_url: $productURL,
+ );
}
+
/**
* Convert the array of product statuses to a single manufacturing status
* @param array $statusArray
@@ -266,7 +310,10 @@ private function productStatusArrayToManufacturingStatus(array $statusArray): Ma
return ManufacturingStatus::EOL;
}
- if (in_array('INVALID', $statusArray, true)) {
+ if (in_array('INVALID', $statusArray, true) ||
+ in_array('PRODUCT_BLOCKED', $statusArray, true) ||
+ in_array('NOT_IN_OFFER', $statusArray, true)
+ ) {
return ManufacturingStatus::DISCONTINUED;
}
@@ -276,8 +323,12 @@ private function productStatusArrayToManufacturingStatus(array $statusArray): Ma
- private function normalizeURL(string $url): string
+ private function normalizeURL(?string $url): ?string
{
+ if ($url == null) {
+ return null;
+ }
+
//If a URL starts with // we assume that it is a relative URL and we add the protocol
if (str_starts_with($url, '//')) {
$url = 'https:' . $url;
@@ -290,32 +341,16 @@ private function normalizeURL(string $url): string
return $url;
}
- public function getCapabilities(): array
- {
- return [
- ProviderCapabilities::BASIC,
- ProviderCapabilities::FOOTPRINT,
- ProviderCapabilities::PICTURE,
- ProviderCapabilities::DATASHEET,
- ProviderCapabilities::PRICE,
- ];
- }
-
- public function getHandledDomains(): array
+ /**
+ * Construct the product detail page URL from the symbol.
+ * NOTE: the /xx/xx localization part of the URL is omitted, as TME automatically redirects to the correct version of the site
+ * @param string $symbol part symbol
+ * @return string product detail URL
+ */
+ private function constructPartInfoUrl(string $symbol): string
{
- return ['tme.eu'];
+ return "https://www.tme.eu/details/{$symbol}";
}
- public function getIDFromURL(string $url): ?string
- {
- //Input: https://www.tme.eu/de/details/fi321_se/kuhler/alutronic/
- //The ID is the part after the details segment and before the next slash
- $matches = [];
- if (preg_match('#/details/([^/]+)/#', $url, $matches) === 1) {
- return $matches[1];
- }
-
- return null;
- }
}
diff --git a/src/Settings/InfoProviderSystem/TMESettings.php b/src/Settings/InfoProviderSystem/TMESettings.php
index d6f03d341..5def63b49 100644
--- a/src/Settings/InfoProviderSystem/TMESettings.php
+++ b/src/Settings/InfoProviderSystem/TMESettings.php
@@ -59,17 +59,12 @@ class TMESettings
#[Assert\Choice(choices: self::SUPPORTED_CURRENCIES)]
public string $currency = "EUR";
- #[SettingsParameter(label: new TM("settings.ips.tme.language"), formType: LanguageType::class, formOptions: ["preferred_choices" => ["en", "de", "fr", "pl"]],
- envVar: "PROVIDER_TME_LANGUAGE", envVarMode: EnvVarMode::OVERWRITE)]
- #[Assert\Language]
- public string $language = "en";
-
#[SettingsParameter(label: new TM("settings.ips.tme.country"), formType: CountryType::class, formOptions: ["preferred_choices" => ["DE", "PL", "GB", "FR"]],
envVar: "PROVIDER_TME_COUNTRY", envVarMode: EnvVarMode::OVERWRITE)]
#[Assert\Country]
public string $country = "DE";
- #[SettingsParameter(label: new TM("settings.ips.tme.grossPrices"),
- envVar: "bool:PROVIDER_TME_GET_GROSS_PRICES", envVarMode: EnvVarMode::OVERWRITE)]
- public bool $grossPrices = true;
+ #[SettingsParameter(label: new TM("settings.ips.tme.preferMetricFootprint"),
+ envVar: "bool:PROVIDER_TME_PREFER_METRIC_FOOTPRINT", envVarMode: EnvVarMode::OVERWRITE)]
+ public bool $preferMetricFootprint = false;
}
diff --git a/tests/Services/InfoProviderSystem/Providers/TMEClientTest.php b/tests/Services/InfoProviderSystem/Providers/TMEClientTest.php
new file mode 100644
index 000000000..f3ef813ff
--- /dev/null
+++ b/tests/Services/InfoProviderSystem/Providers/TMEClientTest.php
@@ -0,0 +1,66 @@
+createMock(TMESettings::class);
+ $settings->apiToken = 'test_token';
+ $settings->apiSecret = 'test_secret';
+
+ // Setup the Mock HTTP Client to return a fake auth token first, then actual responses
+ $authResponse = new MockResponse(json_encode([
+ 'access_token' => 'fake_token_123',
+ 'token_type' => 'Bearer',
+ 'expires_in' => 300,
+ 'refresh_token' => 'fake_refresh'
+ ]));
+
+ $actionResponse1 = new MockResponse(json_encode(['status' => 'OK']));
+ $actionResponse2 = new MockResponse(json_encode(['status' => 'OK']));
+
+ $requests = [];
+ $callback = function($method, $url, $options) use (&$requests, $authResponse, $actionResponse1, $actionResponse2) {
+ $requests[] = ['method' => $method, 'url' => $url, 'options' => $options];
+ if (count($requests) === 1) return $authResponse;
+ if (count($requests) === 2) return $actionResponse1;
+ return $actionResponse2;
+ };
+
+ $httpClient = new MockHttpClient($callback);
+ $cache = new ArrayAdapter();
+
+ $client = new TMEClient($httpClient, $settings, $cache);
+
+ // First request should trigger the auth call
+ $response1 = $client->makeRequest('products/data', ['symbols' => ['M7-DIO']]);
+ $this->assertSame(200, $response1->getStatusCode());
+
+ // Second request should use the cached token
+ $response2 = $client->makeRequest('products/data', ['symbols' => ['M7-DIO']]);
+ $this->assertSame(200, $response2->getStatusCode());
+
+ // Total network requests should be 3 (1 for auth, 2 for the actual requests)
+ $this->assertCount(3, $requests);
+
+ // Check the auth request details
+ $this->assertSame('https://api.tme.eu/auth/token', $requests[0]['url']);
+ $this->assertSame('POST', $requests[0]['method']);
+ $this->assertStringContainsString('Authorization: Basic ' . base64_encode('test_token:test_secret'), implode("\n", $requests[0]['options']['headers']));
+ $this->assertStringContainsString('grant_type=client_credentials', $requests[0]['options']['body']);
+
+ // Check the action requests verify the cached token was appended
+ $this->assertSame('https://api.tme.eu/products/data', explode('?', $requests[1]['url'])[0]);
+ $this->assertStringContainsString('Authorization: Bearer fake_token_123', implode("\n", $requests[1]['options']['headers']));
+ $this->assertStringContainsString('Authorization: Bearer fake_token_123', implode("\n", $requests[2]['options']['headers']));
+ }
+}
diff --git a/tests/Services/InfoProviderSystem/Providers/TMEProviderTest.php b/tests/Services/InfoProviderSystem/Providers/TMEProviderTest.php
index 20fba5231..3435b0fb1 100644
--- a/tests/Services/InfoProviderSystem/Providers/TMEProviderTest.php
+++ b/tests/Services/InfoProviderSystem/Providers/TMEProviderTest.php
@@ -23,6 +23,8 @@
namespace App\Tests\Services\InfoProviderSystem\Providers;
use App\Entity\Parts\ManufacturingStatus;
+use App\Services\InfoProviderSystem\DTOs\FileDTO;
+use App\Services\InfoProviderSystem\DTOs\ParameterDTO;
use App\Services\InfoProviderSystem\DTOs\PartDetailDTO;
use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO;
use App\Services\InfoProviderSystem\DTOs\SearchResultDTO;
@@ -41,115 +43,24 @@ final class TMEProviderTest extends TestCase
private TMEProvider $provider;
private MockHttpClient $httpClient;
+
protected function setUp(): void
{
$this->httpClient = new MockHttpClient();
$this->settings = SettingsTestHelper::createSettingsDummy(TMESettings::class);
- // Use a short (anonymous-style) token so grossPrices is read from settings
- $this->settings->apiToken = 'test_token_000000000000000000000000000000000000000';
+ $this->settings->apiToken = 'test_token';
$this->settings->apiSecret = 'test_secret';
$this->settings->currency = 'EUR';
- $this->settings->language = 'en';
$this->settings->country = 'DE';
- $this->settings->grossPrices = false;
- $this->provider = new TMEProvider(new TMEClient($this->httpClient, $this->settings), $this->settings);
- }
-
- // --- Mock response helpers ---
- // Only fields actually read by TMEProvider are included.
-
- private function mockProductList(array $products): MockResponse
- {
- return new MockResponse(json_encode([
- 'Status' => 'OK',
- 'Data' => ['ProductList' => $products],
- ]));
- }
-
- private function mockFilesList(array $products): MockResponse
- {
- return new MockResponse(json_encode([
- 'Status' => 'OK',
- 'Data' => ['ProductList' => $products],
- ]));
- }
-
- private function mockParametersList(array $products): MockResponse
- {
- return new MockResponse(json_encode([
- 'Status' => 'OK',
- 'Data' => ['ProductList' => $products],
- ]));
- }
-
- private function mockPrices(string $currency, string $priceType, array $products): MockResponse
- {
- return new MockResponse(json_encode([
- 'Status' => 'OK',
- 'Data' => [
- 'Currency' => $currency,
- 'PriceType' => $priceType,
- 'ProductList' => $products,
- ],
- ]));
- }
-
- // --- Mock data ---
+ $cache = new \Symfony\Component\Cache\Adapter\ArrayAdapter();
- private function smd0603Products(): MockResponse
- {
- return $this->mockProductList([[
- 'Symbol' => 'SMD0603-5K1-1%',
- 'OriginalSymbol' => '0603SAF5101T5E',
- 'Producer' => 'ROYALOHM',
- 'Description' => 'Resistor: thick film; SMD; 0603; 5.1kΩ; 0.1W; ±1%; 50V; -55÷155°C',
- 'Category' => 'SMD resistors',
- 'Photo' => '//ce8dc832c.cloudimg.io/v7/_cdn_/E9/C2/B0/00/0/732318_1.jpg',
- 'ProductStatusList' => [],
- 'ProductInformationPage' => '//www.tme.eu/en/details/smd0603-5k1-1%/smd-resistors/royalohm/0603saf5101t5e/',
- 'Weight' => 0.021,
- 'WeightUnit' => 'g',
- ]]);
- }
-
- private function smd0603Files(): MockResponse
- {
- return $this->mockFilesList([[
- 'Symbol' => 'SMD0603-5K1-1%',
- 'Files' => [
- 'AdditionalPhotoList' => [],
- 'DocumentList' => [
- ['DocumentUrl' => '//www.tme.eu/Document/b315665a56acbc42df513c99b390ad98/ROYALOHM-THICKFILM.pdf'],
- ['DocumentUrl' => '//www.tme.eu/Document/c283990e907c122bb808207d1578ac7f/POWER_RATING-DTE.pdf'],
- ],
- ],
- ]]);
- }
-
- private function smd0603Parameters(): MockResponse
- {
- return $this->mockParametersList([[
- 'Symbol' => 'SMD0603-5K1-1%',
- 'ParameterList' => [
- ['ParameterId' => 34, 'ParameterName' => 'Type of resistor', 'ParameterValue' => 'thick film'],
- ['ParameterId' => 35, 'ParameterName' => 'Case - mm', 'ParameterValue' => '1608'],
- ['ParameterId' => 38, 'ParameterName' => 'Resistance', 'ParameterValue' => '5.1kΩ'],
- ['ParameterId' => 39, 'ParameterName' => 'Tolerance', 'ParameterValue' => '±1%'],
- ['ParameterId' => 120, 'ParameterName' => 'Operating voltage', 'ParameterValue' => '50V'],
- ],
- ]]);
- }
+ // Pre-populate the cache with a fake token so that tests don't make an HTTP auth request
+ $cacheKey = 'tme_api_v2_token_' . hash('xxh3', $this->settings->apiToken . $this->settings->apiSecret);
+ $item = $cache->getItem($cacheKey);
+ $item->set('fake_test_token');
+ $cache->save($item);
- private function smd0603Prices(): MockResponse
- {
- return $this->mockPrices('EUR', 'NET', [[
- 'Symbol' => 'SMD0603-5K1-1%',
- 'PriceList' => [
- ['Amount' => 100, 'PriceValue' => 0.01077],
- ['Amount' => 1000, 'PriceValue' => 0.00291],
- ['Amount' => 5000, 'PriceValue' => 0.00150],
- ],
- ]]);
+ $this->provider = new TMEProvider(new TMEClient($this->httpClient, $this->settings, $cache), $this->settings);
}
private function etqp3mProducts(): MockResponse
@@ -233,7 +144,8 @@ public function testIsActiveWithCredentials(): void
public function testIsActiveWithoutCredentials(): void
{
$this->settings->apiToken = null;
- $provider = new TMEProvider(new TMEClient($this->httpClient, $this->settings), $this->settings);
+ $cache = new \Symfony\Component\Cache\Adapter\ArrayAdapter();
+ $provider = new TMEProvider(new TMEClient($this->httpClient, $this->settings, $cache), $this->settings);
$this->assertFalse($provider->isActive());
}
@@ -263,31 +175,265 @@ public function testGetIDFromURL(): void
public function testSearchByKeyword(): void
{
- $this->httpClient->setResponseFactory([$this->smd0603Products()]);
+
+ $this->httpClient->setResponseFactory([function (string $method, string $url, array $options): MockResponse {
+
+ // Check request parameters
+ $exp_url = 'https://api.tme.eu/products/search?country=DE&scope[0]=products&phrase=SMD0603-5K1-1%25&limit=100';
+ $this->assertSame('GET', $method);
+ $this->assertSame($exp_url, $url);
+ $this->assertContains('Authorization: Bearer fake_test_token', $options['headers'] ?? []);
+
+ // Construct response
+ return new MockResponse(json_encode([
+ 'status' => 'OK',
+ 'data' => [
+ 'products' => [
+ 'elements' => [
+ [
+ 'product_status' => [],
+ 'symbol' => 'SMD0603-5K1-1%',
+ 'ean' => '978020137962',
+ 'category' => [
+ 'name' => 'SMD resistors'
+ ],
+ 'manufacturer_symbols' => ['0603SAF5101T5E', 'another_symbol'],
+ 'manufacturer' => [
+ 'name' => 'ROYALOHM'
+ ],
+ 'description' => 'Resistor: thick film; SMD; 0603; 5.1kΩ; 0.1W; ±1%; 50V; -55÷155°C',
+ 'assets' => [
+ 'primary_photo' => [
+ 'prime' => '//images.somecdn.net/somepath/smd0603_primary_prime.jpg'
+
+ ]
+ ]
+ ],
+ // Fake product specifically created to test nullable fields
+ [
+ 'product_status' => ['INVALID'],
+ 'symbol' => 'FAKE_PRODUCT',
+ 'description' => 'High-Quality bleeding edge fake product'
+ ]
+ ]
+ ]
+ ]
+ ]));
+ }]);
$results = $this->provider->searchByKeyword('SMD0603-5K1-1%');
$this->assertIsArray($results);
- $this->assertCount(1, $results);
+ $this->assertCount(2, $results);
+
$this->assertInstanceOf(SearchResultDTO::class, $results[0]);
+ $this->assertSame('tme', $results[0]->provider_key);
$this->assertSame('SMD0603-5K1-1%', $results[0]->provider_id);
$this->assertSame('0603SAF5101T5E', $results[0]->name);
- $this->assertSame('ROYALOHM', $results[0]->manufacturer);
+ $this->assertSame('Resistor: thick film; SMD; 0603; 5.1kΩ; 0.1W; ±1%; 50V; -55÷155°C', $results[0]->description);
$this->assertSame('SMD resistors', $results[0]->category);
+ $this->assertSame('ROYALOHM', $results[0]->manufacturer);
+ $this->assertSame('0603SAF5101T5E', $results[0]->mpn);
+ $this->assertSame('https://images.somecdn.net/somepath/smd0603_primary_prime.jpg', $results[0]->preview_image_url);
$this->assertSame(ManufacturingStatus::ACTIVE, $results[0]->manufacturing_status);
- $this->assertSame(
- 'https://www.tme.eu/en/details/smd0603-5k1-1%25/smd-resistors/royalohm/0603saf5101t5e/',
- $results[0]->provider_url
- );
+ $this->assertSame('https://www.tme.eu/details/SMD0603-5K1-1%25',$results[0]->provider_url);
+ $this->assertSame('978020137962', $results[0]->gtin);
+
+ $this->assertInstanceOf(SearchResultDTO::class, $results[1]);
+ $this->assertSame('tme', $results[0]->provider_key);
+ $this->assertSame('FAKE_PRODUCT', $results[1]->provider_id);
+ $this->assertSame('FAKE_PRODUCT', $results[1]->name);
+ $this->assertSame('High-Quality bleeding edge fake product', $results[1]->description);
+ $this->assertNull($results[1]->category);
+ $this->assertNull($results[1]->manufacturer);
+ $this->assertNull($results[1]->mpn);
+ $this->assertNull($results[1]->preview_image_url);
+ $this->assertSame(ManufacturingStatus::DISCONTINUED, $results[1]->manufacturing_status);
+ $this->assertSame('https://www.tme.eu/details/FAKE_PRODUCT',$results[1]->provider_url);
+ $this->assertNull($results[1]->gtin);
}
- public function testGetDetailsWithPercentInPartNumber(): void
+ public function testGetPartDetails(): void
{
+
$this->httpClient->setResponseFactory([
- $this->smd0603Products(),
- $this->smd0603Files(),
- $this->smd0603Parameters(),
- $this->smd0603Prices(),
+ // Request 1: get product info
+ function (string $method, string $url, array $options): MockResponse {
+ // Check request parameters
+ $exp_url = 'https://api.tme.eu/products?country=DE&symbols[0]=SMD0603-5K1-1%25';
+ $this->assertSame('GET', $method);
+ $this->assertSame($exp_url, $url);
+ $this->assertContains('Authorization: Bearer fake_test_token', $options['headers'] ?? []);
+
+ // Construct response
+ return new MockResponse(json_encode([
+ 'status' => 'OK',
+ 'data' => [
+ 'elements' => [
+ [
+ 'product_status' => [],
+ 'symbol' => 'SMD0603-5K1-1%',
+ 'ean' => '978020137962',
+ 'category' => [
+ 'name' => 'SMD resistors'
+ ],
+ 'manufacturer_symbols' => ['0603SAF5101T5E', 'another_symbol'],
+ 'manufacturer' => [
+ 'name' => 'ROYALOHM'
+ ],
+ 'assets' => [
+ 'primary_photo' => [
+ 'prime' => '//images.somecdn.net/somepath/smd0603_primary_prime.jpg',
+ 'high_resolution' => '//images.somecdn.net/somepath/smd0603_primary_high_resolution.jpg'
+ ]
+ ],
+ 'description' => 'Resistor: thick film; SMD; 0603; 5.1kΩ; 0.1W; ±1%; 50V; -55÷155°C',
+ 'weight' => [
+ 'value' => 10,
+ 'unit' => 'g',
+ ]
+ ]
+ ]
+ ]
+ ]));
+ },
+ // Request 2: get files
+ function (string $method, string $url, array $options): MockResponse {
+ // Check request parameters
+ $exp_url = 'https://api.tme.eu/products/files?country=DE&symbols[0]=SMD0603-5K1-1%25';
+ $this->assertSame('GET', $method);
+ $this->assertSame($exp_url, $url);
+ $this->assertContains('Authorization: Bearer fake_test_token', $options['headers'] ?? []);
+
+ // Construct response
+ return new MockResponse(json_encode([
+ 'status' => 'OK',
+ 'data' => [
+ 'elements' => [
+ [
+ 'symbol' => 'SMD0603-5K1-1%',
+ 'assets' => [
+ 'primary_photo' => [
+ 'prime' => '//images.somecdn.net/somepath/smd0603_primary_prime.jpg',
+ 'high_resolution' => '//images.somecdn.net/somepath/smd0603_primary_high_resolution.jpg',
+ ],
+ 'additional' => [
+ 'elements' => [
+ [
+ 'prime' => '//images.somecdn.net/somepath/smd0603_additional1_prime.jpg',
+ 'high_resolution' => '//images.somecdn.net/somepath/smd0603_additional1_high_resolution.jpg'
+ ],
+ [
+ 'prime' => '//images.somecdn.net/somepath/smd0603_additional2_prime.jpg',
+ ],
+ ]
+ ],
+ ],
+ 'documents' => [
+ 'elements' => [
+ [
+ 'url' => '//documents.somecdn.net/somepath/smd0603_datasheet.pdf',
+ 'type' => 'DTE',
+ 'file_name' => 'smd0603.pdf'
+ ],
+ // Document of the wrong type, check it doesn't get included
+ [
+ 'url' => '//documents.somecdn.net/somepath/some_firmware.bin',
+ 'type' => 'SFT',
+ 'file_name' => 'firmware.pdf'
+ ],
+ [
+ 'url' => '//documents.somecdn.net/somepath/smd0603_safety.pdf',
+ 'type' => 'KCH',
+ 'file_name' => 'smd0603_safety.pdf'
+ ],
+ ]
+ ]
+ ]
+ ]
+ ]
+ ]));
+ },
+ // Request 3: get parameters
+ function (string $method, string $url, array $options): MockResponse {
+ // Check request parameters
+ $exp_url = 'https://api.tme.eu/products/parameters?country=DE&symbols[0]=SMD0603-5K1-1%25';
+ $this->assertSame('GET', $method);
+ $this->assertSame($exp_url, $url);
+ $this->assertContains('Authorization: Bearer fake_test_token', $options['headers'] ?? []);
+
+ // Construct response
+ return new MockResponse(json_encode([
+ 'status' => 'OK',
+ 'data' => [
+ 'elements' => [
+ [
+ 'symbol' => 'SMD0603-5K1-1%',
+ 'parameters' => [
+ 'elements' => [
+ [
+ 'id' => 35,
+ 'name' => 'Case',
+ 'values' => [
+ [
+ 'value' => '0603',
+ ]
+ ]
+ ],
+ [
+ 'id' => 34,
+ 'name' => 'Resistance',
+ 'values' => [
+ [
+ 'value' => '5.1k\u03a9',
+ ]
+ ]
+ ]
+ ]
+ ]
+ ]
+ ]
+ ]
+ ]));
+ },
+ // Request 4: get product pricing information
+ function (string $method, string $url, array $options): MockResponse {
+ // Check request parameters
+ $exp_url = 'https://api.tme.eu/products/data?country=DE¤cy=EUR&scope[0]=prices&symbols[0]=SMD0603-5K1-1%25';
+ $this->assertSame('GET', $method);
+ $this->assertSame($exp_url, $url);
+ $this->assertContains('Authorization: Bearer fake_test_token', $options['headers'] ?? []);
+
+ // Construct response
+ return new MockResponse(json_encode([
+ 'status' => 'OK',
+ 'data' => [
+ 'elements' => [
+ [
+ 'symbol' => 'SMD0603-5K1-1%',
+ 'prices' => [
+ 'elements' => [
+ [
+ 'amount' => 1,
+ 'price' => 0.23
+ ],
+ [
+ 'amount' => 50,
+ 'price' => 0.19
+ ],
+ [
+ 'amount' => 100,
+ 'price' => 0.071
+ ],
+ ],
+ 'currency' => 'EUR',
+ 'type' => 'GROSS'
+ ]
+ ]
+ ]
+ ]
+ ]));
+ }
]);
$result = $this->provider->getDetails('SMD0603-5K1-1%');
@@ -296,78 +442,166 @@ public function testGetDetailsWithPercentInPartNumber(): void
$this->assertSame('SMD0603-5K1-1%', $result->provider_id);
$this->assertSame('0603SAF5101T5E', $result->name);
$this->assertSame('Resistor: thick film; SMD; 0603; 5.1kΩ; 0.1W; ±1%; 50V; -55÷155°C', $result->description);
+ $this->assertSame('SMD resistors', $result->category);
$this->assertSame('ROYALOHM', $result->manufacturer);
$this->assertSame('0603SAF5101T5E', $result->mpn);
- $this->assertSame('SMD resistors', $result->category);
+ $this->assertSame('https://images.somecdn.net/somepath/smd0603_primary_high_resolution.jpg', $result->preview_image_url);
$this->assertSame(ManufacturingStatus::ACTIVE, $result->manufacturing_status);
- $this->assertSame(0.021, $result->mass);
- $this->assertSame('1608', $result->footprint);
- $this->assertSame(
- 'https://www.tme.eu/en/details/smd0603-5k1-1%25/smd-resistors/royalohm/0603saf5101t5e/',
- $result->provider_url
- );
+ $this->assertSame('https://www.tme.eu/details/SMD0603-5K1-1%25',$result->provider_url);
+ $this->assertSame('0603', $result->footprint);
+ $this->assertSame('978020137962', $result->gtin);
+ $this->assertSame(10.0, $result->mass);
+
+ $this->assertCount(2, $result->images);
+ $this->assertInstanceOf(FileDTO::class, $result->images[0]);
+ $this->assertSame('https://images.somecdn.net/somepath/smd0603_additional1_high_resolution.jpg', $result->images[0]->url);
+ $this->assertInstanceOf(FileDTO::class, $result->images[1]);
+ $this->assertSame('https://images.somecdn.net/somepath/smd0603_additional2_prime.jpg', $result->images[1]->url);
$this->assertCount(2, $result->datasheets);
- $this->assertSame('https://www.tme.eu/Document/b315665a56acbc42df513c99b390ad98/ROYALOHM-THICKFILM.pdf', $result->datasheets[0]->url);
- $this->assertCount(0, $result->images);
+ $this->assertInstanceOf(FileDTO::class, $result->datasheets[0]);
+ $this->assertSame('https://documents.somecdn.net/somepath/smd0603_datasheet.pdf', $result->datasheets[0]->url);
+ $this->assertSame('smd0603.pdf', $result->datasheets[0]->name);
+ $this->assertInstanceOf(FileDTO::class, $result->datasheets[1]);
+ $this->assertSame('https://documents.somecdn.net/somepath/smd0603_safety.pdf', $result->datasheets[1]->url);
+ $this->assertSame('smd0603_safety.pdf', $result->datasheets[1]->name);
+
+ $this->assertCount(2, $result->parameters);
+ $this->assertSame('Case', $result->parameters[0]->name);
+ $this->assertSame('Resistance', $result->parameters[1]->name);
+ // Do not check values, conversion is handled by ParameterDTO
$this->assertCount(1, $result->vendor_infos);
- $vendorInfo = $result->vendor_infos[0];
- $this->assertInstanceOf(PurchaseInfoDTO::class, $vendorInfo);
- $this->assertSame('TME', $vendorInfo->distributor_name);
- $this->assertSame('SMD0603-5K1-1%', $vendorInfo->order_number);
- $this->assertSame(
- 'https://www.tme.eu/en/details/smd0603-5k1-1%25/smd-resistors/royalohm/0603saf5101t5e/',
- $vendorInfo->product_url
- );
- $this->assertCount(3, $vendorInfo->prices);
- $this->assertSame(100.0, $vendorInfo->prices[0]->minimum_discount_amount);
- $this->assertSame('0.01077', $vendorInfo->prices[0]->price);
- $this->assertSame('EUR', $vendorInfo->prices[0]->currency_iso_code);
- $this->assertFalse($vendorInfo->prices[0]->includes_tax);
+ $this->assertInstanceOf(PurchaseInfoDTO::class, $result->vendor_infos[0]);
+ $this->assertSame('TME', $result->vendor_infos[0]->distributor_name);
+ $this->assertSame('SMD0603-5K1-1%', $result->vendor_infos[0]->order_number);
+ $this->assertSame('https://www.tme.eu/details/SMD0603-5K1-1%25',$result->vendor_infos[0]->product_url);
+ $this->assertCount(3, $result->vendor_infos[0]->prices);
+ $this->assertSame(1.0, $result->vendor_infos[0]->prices[0]->minimum_discount_amount);
+ $this->assertSame('0.23', $result->vendor_infos[0]->prices[0]->price);
+ $this->assertSame('EUR', $result->vendor_infos[0]->prices[0]->currency_iso_code);
+ $this->assertTrue($result->vendor_infos[0]->prices[0]->includes_tax);
+ $this->assertSame(50.0, $result->vendor_infos[0]->prices[1]->minimum_discount_amount);
+ $this->assertSame('0.19', $result->vendor_infos[0]->prices[1]->price);
+ $this->assertSame('EUR', $result->vendor_infos[0]->prices[1]->currency_iso_code);
+ $this->assertTrue($result->vendor_infos[0]->prices[1]->includes_tax);
+ $this->assertSame(100.0, $result->vendor_infos[0]->prices[2]->minimum_discount_amount);
+ $this->assertSame('0.071', $result->vendor_infos[0]->prices[2]->price);
+ $this->assertSame('EUR', $result->vendor_infos[0]->prices[2]->currency_iso_code);
+ $this->assertTrue($result->vendor_infos[0]->prices[2]->includes_tax);
- $this->assertCount(5, $result->parameters);
}
- public function testGetDetailsForEtqp3m6r8kvp(): void
+ private function getPartDetailsFootprintTestData(): array
{
- $this->httpClient->setResponseFactory([
- $this->etqp3mProducts(),
- $this->etqp3mFiles(),
- $this->etqp3mParameters(),
- $this->etqp3mPrices(),
- ]);
+ return [
+ new MockResponse(json_encode([
+ 'status' => 'OK',
+ 'data' => [
+ 'elements' => [
+ [
+ 'product_status' => [],
+ 'symbol' => 'FAKE_PRODUCT',
+ 'description' => 'Really nice fake product'
+ ]
+ ]
+ ]
+ ])),
+ new MockResponse(json_encode([
+ 'status' => 'OK',
+ 'data' => [
+ 'elements' => [
+ [
+ 'symbol' => 'FAKE_PRODUCT',
+ 'assets' => [
+ 'primary_photo' => [
+ 'prime' => '//images.somecdn.net/somepath/fake_product_primary_prime.jpg',
+ ],
+ 'additional' => [
+ 'elements' => []
+ ]
+ ],
+ 'documents' => [
+ 'elements' => []
+ ]
+ ]
+ ]
+ ]
+ ])),
+ new MockResponse(json_encode([
+ 'status' => 'OK',
+ 'data' => [
+ 'elements' => [
+ [
+ 'symbol' => 'SMD0603-5K1-1%',
+ 'parameters' => [
+ 'elements' => [
+ [
+ 'id' => 2932,
+ 'name' => 'Case - inch',
+ 'values' => [
+ [
+ 'value' => 'footprint_imperial',
+ ]
+ ]
+ ],
+ [
+ 'id' => 2931,
+ 'name' => 'Case - mm',
+ 'values' => [
+ [
+ 'value' => 'footprint_metric',
+ ]
+ ]
+ ],
+ ]
+ ]
+ ]
+ ]
+ ]
+ ])),
+ new MockResponse(json_encode([
+ 'status' => 'OK',
+ 'data' => [
+ 'elements' => [
+ [
+ 'symbol' => 'FAKE_PRODUCT',
+ 'prices' => [
+ 'elements' => [],
+ 'currency' => 'EUR',
+ 'type' => 'GROSS'
+ ]
+ ]
+ ]
+ ]
+ ])),
+ ];
+ }
- $result = $this->provider->getDetails('ETQP3M6R8KVP');
+ public function testGetPartDetailsImperialFootprint(): void
+ {
- $this->assertInstanceOf(PartDetailDTO::class, $result);
- $this->assertSame('ETQP3M6R8KVP', $result->provider_id);
- $this->assertSame('ETQP3M6R8KVP', $result->name);
- $this->assertSame('Inductor: wire; SMD; 6.8uH; 2.9A; R: 65.7mΩ; ±20%; ETQP3M; 5.5x5x3mm', $result->description);
- $this->assertSame('PANASONIC', $result->manufacturer);
- $this->assertSame('ETQP3M6R8KVP', $result->mpn);
- $this->assertSame('Inductors', $result->category);
- $this->assertSame(ManufacturingStatus::ACTIVE, $result->manufacturing_status);
- $this->assertSame(0.44, $result->mass);
- $this->assertNull($result->footprint);
- $this->assertSame('https://www.tme.eu/en/details/etqp3m6r8kvp/inductors/panasonic/', $result->provider_url);
+ $this->httpClient->setResponseFactory($this->getPartDetailsFootprintTestData());
- $this->assertCount(2, $result->datasheets);
- $this->assertSame('https://www.tme.eu/Document/50a845881f09d8a2248350946e11df38/AGL0000C63.pdf', $result->datasheets[0]->url);
- $this->assertCount(0, $result->images);
+ # Set config option
+ $this->settings->preferMetricFootprint = false;
- $this->assertCount(1, $result->vendor_infos);
- $vendorInfo = $result->vendor_infos[0];
- $this->assertSame('TME', $vendorInfo->distributor_name);
- $this->assertSame('ETQP3M6R8KVP', $vendorInfo->order_number);
- $this->assertSame('https://www.tme.eu/en/details/etqp3m6r8kvp/inductors/panasonic/', $vendorInfo->product_url);
- $this->assertCount(3, $vendorInfo->prices);
- $this->assertSame(1.0, $vendorInfo->prices[0]->minimum_discount_amount);
- $this->assertSame('0.589', $vendorInfo->prices[0]->price);
- $this->assertSame('EUR', $vendorInfo->prices[0]->currency_iso_code);
- $this->assertFalse($vendorInfo->prices[0]->includes_tax);
-
- $this->assertCount(3, $result->parameters);
+ $result = $this->provider->getDetails('FAKE_PRODUCT');
+
+ $this->assertSame('footprint_imperial', $result->footprint);
+ }
+
+ public function testGetPartDetailsMetricFootprint(): void
+ {
+
+ $this->httpClient->setResponseFactory($this->getPartDetailsFootprintTestData());
+
+ # Set config option
+ $this->settings->preferMetricFootprint = true;
+
+ $result = $this->provider->getDetails('FAKE_PRODUCT');
+
+ $this->assertSame('footprint_metric', $result->footprint);
}
public function testNormalizeURLEncodesBarePctSign(): void
diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf
index 2a537775f..6bf7514b0 100644
--- a/translations/messages.en.xlf
+++ b/translations/messages.en.xlf
@@ -9722,10 +9722,10 @@ Please note, that you can not impersonate a disabled user. If you try you will g
Country
-
+
- settings.ips.tme.grossPrices
- Get gross prices (including tax)
+ settings.ips.tme.preferMetricFootprint
+ Use metric footprint when both metric and imperial are available