mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2026-08-03 15:11:40 +00:00
Use V2 api to fetch data from TME
This commit is contained in:
parent
a5cde64550
commit
d3020698a3
3 changed files with 309 additions and 207 deletions
|
|
@ -31,18 +31,68 @@ class TMEClient
|
||||||
{
|
{
|
||||||
public const BASE_URI = 'https://api.tme.eu';
|
public const BASE_URI = 'https://api.tme.eu';
|
||||||
|
|
||||||
|
private ?string $accessToken = null;
|
||||||
|
private ?string $refreshToken = null;
|
||||||
|
private int $tokenExpiry = 0;
|
||||||
|
|
||||||
public function __construct(private readonly HttpClientInterface $tmeClient, private readonly TMESettings $settings)
|
public function __construct(private readonly HttpClientInterface $tmeClient, private readonly TMESettings $settings)
|
||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function makeRequest(string $action, array $parameters): ResponseInterface
|
private function getAccessToken(): string
|
||||||
{
|
{
|
||||||
$parameters['Token'] = $this->settings->apiToken;
|
// Return cached token if still valid (30-second safety margin before expiry)
|
||||||
$parameters['ApiSignature'] = $this->getSignature($action, $parameters, $this->settings->apiSecret);
|
if ($this->accessToken !== null && time() < $this->tokenExpiry - 30) {
|
||||||
|
return $this->accessToken;
|
||||||
|
}
|
||||||
|
|
||||||
return $this->tmeClient->request('POST', $this->getUrlForAction($action), [
|
// Try refreshing before falling back to a full client_credentials flow
|
||||||
'body' => $parameters,
|
if ($this->refreshToken !== null) {
|
||||||
|
try {
|
||||||
|
$this->fetchToken('refresh_token', ['refresh_token' => $this->refreshToken]);
|
||||||
|
return $this->accessToken;
|
||||||
|
} catch (\Throwable) {
|
||||||
|
$this->refreshToken = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->fetchToken('client_credentials');
|
||||||
|
return $this->accessToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function fetchToken(string $grantType, array $extraParams = []): void
|
||||||
|
{
|
||||||
|
$credentials = base64_encode($this->settings->apiToken . ':' . $this->settings->apiSecret);
|
||||||
|
|
||||||
|
$response = $this->tmeClient->request('POST', self::BASE_URI . '/auth/token', [
|
||||||
|
'headers' => [
|
||||||
|
'Authorization' => 'Basic ' . $credentials,
|
||||||
|
],
|
||||||
|
'body' => array_merge(['grant_type' => $grantType], $extraParams),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$data = $response->toArray();
|
||||||
|
$this->accessToken = $data['access_token'];
|
||||||
|
$this->tokenExpiry = time() + (int) $data['expires_in'];
|
||||||
|
$this->refreshToken = $data['refresh_token'] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes an authenticated GET request to the given v2 endpoint.
|
||||||
|
*
|
||||||
|
* @param string $endpoint Path relative to BASE_URI, e.g. 'products/search'
|
||||||
|
* @param array $queryParams Query parameters; arrays are serialised as PHP-style brackets by Symfony's HTTP client
|
||||||
|
*/
|
||||||
|
public function makeRequest(string $endpoint, array $queryParams = []): ResponseInterface
|
||||||
|
{
|
||||||
|
$token = $this->getAccessToken();
|
||||||
|
|
||||||
|
return $this->tmeClient->request('GET', self::BASE_URI . '/' . $endpoint, [
|
||||||
|
'headers' => [
|
||||||
|
'Authorization' => 'Bearer ' . $token,
|
||||||
|
'Accept-Language' => $this->settings->language,
|
||||||
|
],
|
||||||
|
'query' => $queryParams,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -52,46 +102,11 @@ class TMEClient
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if the client is using a private (account related token) instead of a deprecated anonymous token
|
* In API v2 all tokens are private (50-char token + 20-char secret); kept for
|
||||||
* to authenticate with TME.
|
* backwards-compatibility with code that checks this flag.
|
||||||
* @return bool
|
|
||||||
*/
|
*/
|
||||||
public function isUsingPrivateToken(): bool
|
public function isUsingPrivateToken(): bool
|
||||||
{
|
{
|
||||||
//Private tokens are longer than anonymous ones (50 instead of 45 characters)
|
return strlen($this->settings->apiToken ?? '') >= 50;
|
||||||
return strlen($this->settings->apiToken ?? '') > 45;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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
|
|
||||||
*/
|
|
||||||
public function getSignature(string $action, array $parameters, string $appSecret): string
|
|
||||||
{
|
|
||||||
$parameters = $this->sortSignatureParams($parameters);
|
|
||||||
|
|
||||||
$queryString = http_build_query($parameters, '', '&', PHP_QUERY_RFC3986);
|
|
||||||
$signatureBase = strtoupper('POST') .
|
|
||||||
'&' . rawurlencode($this->getUrlForAction($action)) . '&' . rawurlencode($queryString);
|
|
||||||
|
|
||||||
return base64_encode(hash_hmac('sha1', $signatureBase, $appSecret, true));
|
|
||||||
}
|
|
||||||
|
|
||||||
private function getUrlForAction(string $action): string
|
|
||||||
{
|
|
||||||
return self::BASE_URI . '/' . $action . '.json';
|
|
||||||
}
|
|
||||||
|
|
||||||
private function sortSignatureParams(array $params): array
|
|
||||||
{
|
|
||||||
ksort($params);
|
|
||||||
|
|
||||||
foreach ($params as &$value) {
|
|
||||||
if (is_array($value)) {
|
|
||||||
$value = $this->sortSignatureParams($value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $params;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,9 @@ use App\Services\InfoProviderSystem\DTOs\ProviderInfoDTO;
|
||||||
use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO;
|
use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO;
|
||||||
use App\Services\InfoProviderSystem\DTOs\SearchResultDTO;
|
use App\Services\InfoProviderSystem\DTOs\SearchResultDTO;
|
||||||
use App\Settings\InfoProviderSystem\TMESettings;
|
use App\Settings\InfoProviderSystem\TMESettings;
|
||||||
|
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||||
|
|
||||||
|
use function PhpCsFixer\Fixer\PhpUnit\configurePostNormalisation;
|
||||||
|
|
||||||
class TMEProvider implements InfoProviderInterface, URLHandlerInfoProviderInterface
|
class TMEProvider implements InfoProviderInterface, URLHandlerInfoProviderInterface
|
||||||
{
|
{
|
||||||
|
|
@ -39,15 +42,9 @@ class TMEProvider implements InfoProviderInterface, URLHandlerInfoProviderInterf
|
||||||
|
|
||||||
private const VENDOR_NAME = 'TME';
|
private const VENDOR_NAME = 'TME';
|
||||||
|
|
||||||
private readonly bool $get_gross_prices;
|
public function __construct(private readonly TMEClient $tmeClient, private readonly TMESettings $settings,
|
||||||
public function __construct(private readonly TMEClient $tmeClient, private readonly TMESettings $settings)
|
private readonly HttpClientInterface $httpClient)
|
||||||
{
|
{
|
||||||
//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(): ProviderInfoDTO
|
public function getProviderInfo(): ProviderInfoDTO
|
||||||
|
|
@ -75,30 +72,66 @@ class TMEProvider implements InfoProviderInterface, URLHandlerInfoProviderInterf
|
||||||
return $this->tmeClient->isUsable();
|
return $this->tmeClient->isUsable();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function searchByKeyword(string $keyword, array $options = []): array
|
/**
|
||||||
|
* Converts a product id to a product URL
|
||||||
|
* @param string $productId
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private function productIDToProductURL(string $productId, bool $tryResolve = false): string
|
||||||
{
|
{
|
||||||
$response = $this->tmeClient->makeRequest('Products/Search', [
|
$tmp = 'https://www.tme.eu/' . strtolower($this->settings->country) . '/' . strtolower($this->settings->language) . '/details/' . $productId . '/';
|
||||||
'Country' => $this->settings->country,
|
|
||||||
'Language' => $this->settings->language,
|
if (!$tryResolve) {
|
||||||
'SearchPlain' => $keyword,
|
return $tmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Otherwise try to resolve the product URL by making a request to the product page and see where it redirects to.
|
||||||
|
$response = $this->httpClient->request('GET', $tmp, [
|
||||||
|
'max_redirects' => 0,
|
||||||
|
'http_version' => '2.0',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$data = $response->toArray()['Data'];
|
//If the response is a redirect, we can get the location header and return it
|
||||||
|
if ($response->getStatusCode() >= 300 && $response->getStatusCode() < 400) {
|
||||||
|
$location = $response->getHeaders(false)['location'][0] ?? null;
|
||||||
|
if ($location !== null) {
|
||||||
|
return $location;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Otherwise just return the original URL
|
||||||
|
return $tmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function searchByKeyword(string $keyword, array $options = []): array
|
||||||
|
{
|
||||||
|
$response = $this->tmeClient->makeRequest('products/search', [
|
||||||
|
'country' => $this->settings->country,
|
||||||
|
'phrase' => $keyword,
|
||||||
|
'sort' => [
|
||||||
|
'property' => 'ACCURACY',
|
||||||
|
'direction' => 'desc',
|
||||||
|
],
|
||||||
|
'scope' => ['products'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$data = $response->toArray()['data'];
|
||||||
|
|
||||||
$result = [];
|
$result = [];
|
||||||
|
|
||||||
foreach($data['ProductList'] as $product) {
|
foreach ($data['products']['elements'] as $product) {
|
||||||
$result[] = new SearchResultDTO(
|
$result[] = new SearchResultDTO(
|
||||||
provider_key: self::PROVIDER_KEY,
|
provider_key: self::PROVIDER_KEY,
|
||||||
provider_id: $product['Symbol'],
|
provider_id: $product['symbol'],
|
||||||
name: empty($product['OriginalSymbol']) ? $product['Symbol'] : $product['OriginalSymbol'],
|
name: $product['manufacturer_symbols'][0] ?? $product['symbol'],
|
||||||
description: $product['Description'],
|
description: $product['description'],
|
||||||
category: $product['Category'],
|
category: $product['category']['name'],
|
||||||
manufacturer: $product['Producer'],
|
manufacturer: $product['manufacturer']['name'] ?? null,
|
||||||
mpn: $product['OriginalSymbol'] ?? null,
|
mpn: $product['manufacturer_symbols'][0] ?? null,
|
||||||
preview_image_url: $this->normalizeURL($product['Photo']),
|
preview_image_url: $this->normalizeURL($product['assets']['primary_photo']['prime'] ?? null),
|
||||||
manufacturing_status: $this->productStatusArrayToManufacturingStatus($product['ProductStatusList']),
|
manufacturing_status: $this->productStatusArrayToManufacturingStatus($product['product_status'] ?? []),
|
||||||
provider_url: $this->normalizeURL($product['ProductInformationPage']),
|
provider_url: $this->productIDToProductURL($product['symbol']),
|
||||||
|
gtin: !empty($product['ean']) ? $product['ean'] : null
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -107,16 +140,14 @@ class TMEProvider implements InfoProviderInterface, URLHandlerInfoProviderInterf
|
||||||
|
|
||||||
public function getDetails(string $id, array $options = []): PartDetailDTO
|
public function getDetails(string $id, array $options = []): PartDetailDTO
|
||||||
{
|
{
|
||||||
$response = $this->tmeClient->makeRequest('Products/GetProducts', [
|
$response = $this->tmeClient->makeRequest('products', [
|
||||||
'Country' => $this->settings->country,
|
'country' => $this->settings->country,
|
||||||
'Language' => $this->settings->language,
|
'symbols' => [$id],
|
||||||
'SymbolList' => [$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->productIDToProductURL($product['symbol'], true);
|
||||||
$productInfoPage = $this->normalizeURL($product['ProductInformationPage']);
|
|
||||||
|
|
||||||
$files = $this->getFiles($id);
|
$files = $this->getFiles($id);
|
||||||
|
|
||||||
|
|
@ -126,21 +157,22 @@ class TMEProvider implements InfoProviderInterface, URLHandlerInfoProviderInterf
|
||||||
|
|
||||||
return new PartDetailDTO(
|
return new PartDetailDTO(
|
||||||
provider_key: self::PROVIDER_KEY,
|
provider_key: self::PROVIDER_KEY,
|
||||||
provider_id: $product['Symbol'],
|
provider_id: $product['symbol'],
|
||||||
name: empty($product['OriginalSymbol']) ? $product['Symbol'] : $product['OriginalSymbol'],
|
name: $product['manufacturer_symbols'][0] ?? $product['symbol'],
|
||||||
description: $product['Description'],
|
description: $product['description'],
|
||||||
category: $product['Category'],
|
category: $product['category']['name'],
|
||||||
manufacturer: $product['Producer'],
|
manufacturer: $product['manufacturer']['name'] ?? null,
|
||||||
mpn: $product['OriginalSymbol'] ?? null,
|
mpn: $product['manufacturer_symbols'][0] ?? null,
|
||||||
preview_image_url: $this->normalizeURL($product['Photo']),
|
preview_image_url: $this->normalizeURL($product['assets']['primary_photo']['prime'] ?? null),
|
||||||
manufacturing_status: $this->productStatusArrayToManufacturingStatus($product['ProductStatusList']),
|
manufacturing_status: $this->productStatusArrayToManufacturingStatus($product['product_status'] ?? []),
|
||||||
provider_url: $productInfoPage,
|
provider_url: $this->productIDToProductURL($product['symbol']),
|
||||||
footprint: $footprint,
|
footprint: $footprint,
|
||||||
|
gtin: !empty($product['ean']) ? $product['ean'] : null,
|
||||||
datasheets: $files['datasheets'],
|
datasheets: $files['datasheets'],
|
||||||
images: $files['images'],
|
images: $files['images'],
|
||||||
parameters: $parameters,
|
parameters: $parameters,
|
||||||
vendor_infos: [$this->getVendorInfo($id, $productInfoPage)],
|
vendor_infos: [$this->getVendorInfo($id, $productInfoPage)],
|
||||||
mass: $product['WeightUnit'] === 'g' ? $product['Weight'] : null,
|
mass: ($product['weight']['unit'] ?? null) === 'g' ? ($product['weight']['value'] ?? null) : null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -152,34 +184,27 @@ class TMEProvider implements InfoProviderInterface, URLHandlerInfoProviderInterf
|
||||||
*/
|
*/
|
||||||
public function getFiles(string $id): array
|
public function getFiles(string $id): array
|
||||||
{
|
{
|
||||||
$response = $this->tmeClient->makeRequest('Products/GetProductsFiles', [
|
$response = $this->tmeClient->makeRequest('products/files', [
|
||||||
'Country' => $this->settings->country,
|
'country' => $this->settings->country,
|
||||||
'Language' => $this->settings->language,
|
'symbols' => [$id],
|
||||||
'SymbolList' => [$id],
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$data = $response->toArray()['Data'];
|
$element = $response->toArray()['data']['elements'][0];
|
||||||
$files = $data['ProductList'][0]['Files'];
|
|
||||||
|
|
||||||
//Extract datasheets
|
|
||||||
$documentList = $files['DocumentList'];
|
|
||||||
$datasheets = [];
|
$datasheets = [];
|
||||||
foreach($documentList as $document) {
|
foreach ($element['documents']['elements'] ?? [] as $document) {
|
||||||
$datasheets[] = new FileDTO(
|
$datasheets[] = new FileDTO(
|
||||||
url: $this->normalizeURL($document['DocumentUrl']),
|
url: $this->normalizeURL($document['url']),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
//Extract images
|
|
||||||
$imageList = $files['AdditionalPhotoList'];
|
|
||||||
$images = [];
|
$images = [];
|
||||||
foreach($imageList as $image) {
|
foreach ($element['assets']['additional']['elements'] ?? [] as $photo) {
|
||||||
$images[] = new FileDTO(
|
$images[] = new FileDTO(
|
||||||
url: $this->normalizeURL($image['HighResolutionPhoto']),
|
url: $this->normalizeURL($photo['prime']),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'datasheets' => $datasheets,
|
'datasheets' => $datasheets,
|
||||||
'images' => $images,
|
'images' => $images,
|
||||||
|
|
@ -194,28 +219,27 @@ class TMEProvider implements InfoProviderInterface, URLHandlerInfoProviderInterf
|
||||||
*/
|
*/
|
||||||
public function getVendorInfo(string $id, ?string $productURL = null): PurchaseInfoDTO
|
public function getVendorInfo(string $id, ?string $productURL = null): PurchaseInfoDTO
|
||||||
{
|
{
|
||||||
$response = $this->tmeClient->makeRequest('Products/GetPricesAndStocks', [
|
$response = $this->tmeClient->makeRequest('products/data', [
|
||||||
'Country' => $this->settings->country,
|
'country' => $this->settings->country,
|
||||||
'Language' => $this->settings->language,
|
'currency' => $this->settings->currency,
|
||||||
'Currency' => $this->settings->currency,
|
'scope' => ['prices'],
|
||||||
'GrossPrices' => $this->get_gross_prices,
|
'symbols' => [$id],
|
||||||
'SymbolList' => [$id],
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$data = $response->toArray()['Data'];
|
$product = $response->toArray()['data']['elements'][0];
|
||||||
$currency = $data['Currency'];
|
$priceData = $product['prices'];
|
||||||
$include_tax = $data['PriceType'] === 'GROSS';
|
$currency = $priceData['currency'];
|
||||||
|
$include_tax = strtoupper($priceData['type'] ?? '') === 'GROSS';
|
||||||
|
|
||||||
|
|
||||||
$product = $response->toArray()['Data']['ProductList'][0];
|
$vendor_order_number = $product['symbol'];
|
||||||
$vendor_order_number = $product['Symbol'];
|
$priceList = $priceData['elements'] ?? [];
|
||||||
$priceList = $product['PriceList'];
|
|
||||||
|
|
||||||
$prices = [];
|
$prices = [];
|
||||||
foreach ($priceList as $price) {
|
foreach ($priceList as $price) {
|
||||||
$prices[] = new PriceDTO(
|
$prices[] = new PriceDTO(
|
||||||
minimum_discount_amount: $price['Amount'],
|
minimum_discount_amount: $price['amount'],
|
||||||
price: (string) $price['PriceValue'],
|
price: (string) $price['price'],
|
||||||
currency_iso_code: $currency,
|
currency_iso_code: $currency,
|
||||||
includes_tax: $include_tax,
|
includes_tax: $include_tax,
|
||||||
);
|
);
|
||||||
|
|
@ -237,24 +261,38 @@ class TMEProvider implements InfoProviderInterface, URLHandlerInfoProviderInterf
|
||||||
*/
|
*/
|
||||||
public function getParameters(string $id, string|null &$footprint_name = null): array
|
public function getParameters(string $id, string|null &$footprint_name = null): array
|
||||||
{
|
{
|
||||||
$response = $this->tmeClient->makeRequest('Products/GetParameters', [
|
$response = $this->tmeClient->makeRequest('products/parameters', [
|
||||||
'Country' => $this->settings->country,
|
'country' => $this->settings->country,
|
||||||
'Language' => $this->settings->language,
|
'symbols' => [$id],
|
||||||
'SymbolList' => [$id],
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$data = $response->toArray()['Data']['ProductList'][0];
|
$element = $response->toArray()['data']['elements'][0]['parameters'];
|
||||||
|
|
||||||
$result = [];
|
$result = [];
|
||||||
|
|
||||||
$footprint_name = null;
|
$footprint_name = null;
|
||||||
|
|
||||||
foreach($data['ParameterList'] as $parameter) {
|
foreach ($element['elements'] as $parameter) {
|
||||||
$result[] = ParameterDTO::parseValueIncludingUnit($parameter['ParameterName'], $parameter['ParameterValue']);
|
|
||||||
|
|
||||||
//Check if the parameter is the case/footprint
|
//Check if the parameter is the case/footprint
|
||||||
if ($parameter['ParameterId'] === 35) {
|
if ($parameter['id'] === 35) {
|
||||||
$footprint_name = $parameter['ParameterValue'];
|
$footprint_name = $parameter['values'][0]['value'];
|
||||||
|
}
|
||||||
|
|
||||||
|
//Skip related items parameter
|
||||||
|
if ($parameter['id'] === 1605) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count($parameter['values']) > 1) {
|
||||||
|
//Concatenate all values with a comma, if there are multiple values for the same parameter
|
||||||
|
$value = implode(', ', array_map(fn($v) => $v['value'], $parameter['values']));
|
||||||
|
$result[] = new ParameterDTO(
|
||||||
|
name: $parameter['name'],
|
||||||
|
value_text: $value,
|
||||||
|
);
|
||||||
|
} else if (count($parameter['values']) === 1) {
|
||||||
|
$result[] = ParameterDTO::parseValueIncludingUnit($parameter['name'], $parameter['values'][0]['value']);
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -276,14 +314,22 @@ class TMEProvider implements InfoProviderInterface, URLHandlerInfoProviderInterf
|
||||||
return ManufacturingStatus::DISCONTINUED;
|
return ManufacturingStatus::DISCONTINUED;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (in_array('NOT_IN_OFFER', $statusArray, true)) {
|
||||||
|
return ManufacturingStatus::DISCONTINUED;
|
||||||
|
}
|
||||||
|
|
||||||
//By default we assume that the part is active
|
//By default we assume that the part is active
|
||||||
return ManufacturingStatus::ACTIVE;
|
return ManufacturingStatus::ACTIVE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
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 a URL starts with // we assume that it is a relative URL and we add the protocol
|
||||||
if (str_starts_with($url, '//')) {
|
if (str_starts_with($url, '//')) {
|
||||||
$url = 'https:' . $url;
|
$url = 'https:' . $url;
|
||||||
|
|
|
||||||
|
|
@ -46,83 +46,111 @@ final class TMEProviderTest extends TestCase
|
||||||
{
|
{
|
||||||
$this->httpClient = new MockHttpClient();
|
$this->httpClient = new MockHttpClient();
|
||||||
$this->settings = SettingsTestHelper::createSettingsDummy(TMESettings::class);
|
$this->settings = SettingsTestHelper::createSettingsDummy(TMESettings::class);
|
||||||
// Use a short (anonymous-style) token so grossPrices is read from settings
|
$this->settings->apiToken = 'test_token_000000000000000000000000000000000000000000000000';
|
||||||
$this->settings->apiToken = 'test_token_000000000000000000000000000000000000000';
|
$this->settings->apiSecret = 'test_secret_00000000';
|
||||||
$this->settings->apiSecret = 'test_secret';
|
|
||||||
$this->settings->currency = 'EUR';
|
$this->settings->currency = 'EUR';
|
||||||
$this->settings->language = 'en';
|
$this->settings->language = 'en';
|
||||||
$this->settings->country = 'DE';
|
$this->settings->country = 'DE';
|
||||||
$this->settings->grossPrices = false;
|
|
||||||
$this->provider = new TMEProvider(new TMEClient($this->httpClient, $this->settings), $this->settings);
|
$this->provider = new TMEProvider(new TMEClient($this->httpClient, $this->settings), $this->settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Mock response helpers ---
|
// --- Mock response helpers ---
|
||||||
// Only fields actually read by TMEProvider are included.
|
|
||||||
|
|
||||||
private function mockProductList(array $products): MockResponse
|
/** OAuth2 token response – always the first reply in a sequence that triggers an API call */
|
||||||
|
private function mockTokenResponse(): MockResponse
|
||||||
{
|
{
|
||||||
return new MockResponse(json_encode([
|
return new MockResponse(json_encode([
|
||||||
'Status' => 'OK',
|
'access_token' => 'mock_access_token',
|
||||||
'Data' => ['ProductList' => $products],
|
'token_type' => 'Bearer',
|
||||||
|
'expires_in' => 300,
|
||||||
|
'refresh_token' => 'mock_refresh_token',
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function mockSearchResults(array $products): MockResponse
|
||||||
|
{
|
||||||
|
return new MockResponse(json_encode([
|
||||||
|
'status' => 'OK',
|
||||||
|
'data' => [
|
||||||
|
'products' => ['elements' => $products],
|
||||||
|
'parameters' => [],
|
||||||
|
'counters' => [],
|
||||||
|
],
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function mockProductsList(array $products): MockResponse
|
||||||
|
{
|
||||||
|
return new MockResponse(json_encode([
|
||||||
|
'status' => 'OK',
|
||||||
|
'data' => ['elements' => $products],
|
||||||
]));
|
]));
|
||||||
}
|
}
|
||||||
|
|
||||||
private function mockFilesList(array $products): MockResponse
|
private function mockFilesList(array $products): MockResponse
|
||||||
{
|
{
|
||||||
return new MockResponse(json_encode([
|
return new MockResponse(json_encode([
|
||||||
'Status' => 'OK',
|
'status' => 'OK',
|
||||||
'Data' => ['ProductList' => $products],
|
'data' => ['elements' => $products],
|
||||||
]));
|
]));
|
||||||
}
|
}
|
||||||
|
|
||||||
private function mockParametersList(array $products): MockResponse
|
private function mockParametersList(array $products): MockResponse
|
||||||
{
|
{
|
||||||
return new MockResponse(json_encode([
|
return new MockResponse(json_encode([
|
||||||
'Status' => 'OK',
|
'status' => 'OK',
|
||||||
'Data' => ['ProductList' => $products],
|
'data' => ['elements' => $products],
|
||||||
]));
|
]));
|
||||||
}
|
}
|
||||||
|
|
||||||
private function mockPrices(string $currency, string $priceType, array $products): MockResponse
|
private function mockPricesData(string $currency, string $priceType, array $products): MockResponse
|
||||||
{
|
{
|
||||||
return new MockResponse(json_encode([
|
return new MockResponse(json_encode([
|
||||||
'Status' => 'OK',
|
'status' => 'OK',
|
||||||
'Data' => [
|
'data' => [
|
||||||
'Currency' => $currency,
|
'currency' => $currency,
|
||||||
'PriceType' => $priceType,
|
'price_type' => $priceType,
|
||||||
'ProductList' => $products,
|
'elements' => $products,
|
||||||
],
|
],
|
||||||
]));
|
]));
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Mock data ---
|
// --- Mock data (v2 field names) ---
|
||||||
|
|
||||||
|
private function smd0603Product(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'symbol' => 'SMD0603-5K1-1%',
|
||||||
|
'original_symbol' => '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',
|
||||||
|
'statuses' => [],
|
||||||
|
'product_information_page' => '//www.tme.eu/en/details/smd0603-5k1-1%/smd-resistors/royalohm/0603saf5101t5e/',
|
||||||
|
'weight' => 0.021,
|
||||||
|
'weight_unit' => 'g',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function smd0603SearchResults(): MockResponse
|
||||||
|
{
|
||||||
|
return $this->mockSearchResults([$this->smd0603Product()]);
|
||||||
|
}
|
||||||
|
|
||||||
private function smd0603Products(): MockResponse
|
private function smd0603Products(): MockResponse
|
||||||
{
|
{
|
||||||
return $this->mockProductList([[
|
return $this->mockProductsList([$this->smd0603Product()]);
|
||||||
'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
|
private function smd0603Files(): MockResponse
|
||||||
{
|
{
|
||||||
return $this->mockFilesList([[
|
return $this->mockFilesList([[
|
||||||
'Symbol' => 'SMD0603-5K1-1%',
|
'symbol' => 'SMD0603-5K1-1%',
|
||||||
'Files' => [
|
'photos' => [],
|
||||||
'AdditionalPhotoList' => [],
|
'documents' => [
|
||||||
'DocumentList' => [
|
['url' => '//www.tme.eu/Document/b315665a56acbc42df513c99b390ad98/ROYALOHM-THICKFILM.pdf'],
|
||||||
['DocumentUrl' => '//www.tme.eu/Document/b315665a56acbc42df513c99b390ad98/ROYALOHM-THICKFILM.pdf'],
|
['url' => '//www.tme.eu/Document/c283990e907c122bb808207d1578ac7f/POWER_RATING-DTE.pdf'],
|
||||||
['DocumentUrl' => '//www.tme.eu/Document/c283990e907c122bb808207d1578ac7f/POWER_RATING-DTE.pdf'],
|
|
||||||
],
|
|
||||||
],
|
],
|
||||||
]]);
|
]]);
|
||||||
}
|
}
|
||||||
|
|
@ -130,55 +158,58 @@ final class TMEProviderTest extends TestCase
|
||||||
private function smd0603Parameters(): MockResponse
|
private function smd0603Parameters(): MockResponse
|
||||||
{
|
{
|
||||||
return $this->mockParametersList([[
|
return $this->mockParametersList([[
|
||||||
'Symbol' => 'SMD0603-5K1-1%',
|
'symbol' => 'SMD0603-5K1-1%',
|
||||||
'ParameterList' => [
|
'parameters' => [
|
||||||
['ParameterId' => 34, 'ParameterName' => 'Type of resistor', 'ParameterValue' => 'thick film'],
|
['id' => 34, 'name' => 'Type of resistor', 'value' => 'thick film'],
|
||||||
['ParameterId' => 35, 'ParameterName' => 'Case - mm', 'ParameterValue' => '1608'],
|
['id' => 35, 'name' => 'Case - mm', 'value' => '1608'],
|
||||||
['ParameterId' => 38, 'ParameterName' => 'Resistance', 'ParameterValue' => '5.1kΩ'],
|
['id' => 38, 'name' => 'Resistance', 'value' => '5.1kΩ'],
|
||||||
['ParameterId' => 39, 'ParameterName' => 'Tolerance', 'ParameterValue' => '±1%'],
|
['id' => 39, 'name' => 'Tolerance', 'value' => '±1%'],
|
||||||
['ParameterId' => 120, 'ParameterName' => 'Operating voltage', 'ParameterValue' => '50V'],
|
['id' => 120, 'name' => 'Operating voltage', 'value' => '50V'],
|
||||||
],
|
],
|
||||||
]]);
|
]]);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function smd0603Prices(): MockResponse
|
private function smd0603Prices(): MockResponse
|
||||||
{
|
{
|
||||||
return $this->mockPrices('EUR', 'NET', [[
|
return $this->mockPricesData('EUR', 'NET', [[
|
||||||
'Symbol' => 'SMD0603-5K1-1%',
|
'symbol' => 'SMD0603-5K1-1%',
|
||||||
'PriceList' => [
|
'price_list' => [
|
||||||
['Amount' => 100, 'PriceValue' => 0.01077],
|
['amount' => 100, 'price_value' => 0.01077],
|
||||||
['Amount' => 1000, 'PriceValue' => 0.00291],
|
['amount' => 1000, 'price_value' => 0.00291],
|
||||||
['Amount' => 5000, 'PriceValue' => 0.00150],
|
['amount' => 5000, 'price_value' => 0.00150],
|
||||||
],
|
],
|
||||||
]]);
|
]]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function etqp3mProduct(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'symbol' => 'ETQP3M6R8KVP',
|
||||||
|
'original_symbol' => 'ETQP3M6R8KVP',
|
||||||
|
'producer' => 'PANASONIC',
|
||||||
|
'description' => 'Inductor: wire; SMD; 6.8uH; 2.9A; R: 65.7mΩ; ±20%; ETQP3M; 5.5x5x3mm',
|
||||||
|
'category' => 'Inductors',
|
||||||
|
'photo' => '//ce8dc832c.cloudimg.io/v7/_cdn_/9E/27/A0/00/0/684777_1.jpg',
|
||||||
|
'statuses' => [],
|
||||||
|
'product_information_page' => '//www.tme.eu/en/details/etqp3m6r8kvp/inductors/panasonic/',
|
||||||
|
'weight' => 0.44,
|
||||||
|
'weight_unit' => 'g',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
private function etqp3mProducts(): MockResponse
|
private function etqp3mProducts(): MockResponse
|
||||||
{
|
{
|
||||||
return $this->mockProductList([[
|
return $this->mockProductsList([$this->etqp3mProduct()]);
|
||||||
'Symbol' => 'ETQP3M6R8KVP',
|
|
||||||
'OriginalSymbol' => 'ETQP3M6R8KVP',
|
|
||||||
'Producer' => 'PANASONIC',
|
|
||||||
'Description' => 'Inductor: wire; SMD; 6.8uH; 2.9A; R: 65.7mΩ; ±20%; ETQP3M; 5.5x5x3mm',
|
|
||||||
'Category' => 'Inductors',
|
|
||||||
'Photo' => '//ce8dc832c.cloudimg.io/v7/_cdn_/9E/27/A0/00/0/684777_1.jpg',
|
|
||||||
'ProductStatusList' => [],
|
|
||||||
'ProductInformationPage' => '//www.tme.eu/en/details/etqp3m6r8kvp/inductors/panasonic/',
|
|
||||||
'Weight' => 0.44,
|
|
||||||
'WeightUnit' => 'g',
|
|
||||||
]]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function etqp3mFiles(): MockResponse
|
private function etqp3mFiles(): MockResponse
|
||||||
{
|
{
|
||||||
return $this->mockFilesList([[
|
return $this->mockFilesList([[
|
||||||
'Symbol' => 'ETQP3M6R8KVP',
|
'symbol' => 'ETQP3M6R8KVP',
|
||||||
'Files' => [
|
'photos' => [],
|
||||||
'AdditionalPhotoList' => [],
|
'documents' => [
|
||||||
'DocumentList' => [
|
['url' => '//www.tme.eu/Document/50a845881f09d8a2248350946e11df38/AGL0000C63.pdf'],
|
||||||
['DocumentUrl' => '//www.tme.eu/Document/50a845881f09d8a2248350946e11df38/AGL0000C63.pdf'],
|
['url' => '//www.tme.eu/Document/8480690a42fa577214e35e33d3fc8d77/ETQP3M100KVN-LNK.txt'],
|
||||||
['DocumentUrl' => '//www.tme.eu/Document/8480690a42fa577214e35e33d3fc8d77/ETQP3M100KVN-LNK.txt'],
|
|
||||||
],
|
|
||||||
],
|
],
|
||||||
]]);
|
]]);
|
||||||
}
|
}
|
||||||
|
|
@ -186,23 +217,23 @@ final class TMEProviderTest extends TestCase
|
||||||
private function etqp3mParameters(): MockResponse
|
private function etqp3mParameters(): MockResponse
|
||||||
{
|
{
|
||||||
return $this->mockParametersList([[
|
return $this->mockParametersList([[
|
||||||
'Symbol' => 'ETQP3M6R8KVP',
|
'symbol' => 'ETQP3M6R8KVP',
|
||||||
'ParameterList' => [
|
'parameters' => [
|
||||||
['ParameterId' => 566, 'ParameterName' => 'Inductance', 'ParameterValue' => '6.8µH'],
|
['id' => 566, 'name' => 'Inductance', 'value' => '6.8µH'],
|
||||||
['ParameterId' => 370, 'ParameterName' => 'Operating current', 'ParameterValue' => '2.9A'],
|
['id' => 370, 'name' => 'Operating current', 'value' => '2.9A'],
|
||||||
['ParameterId' => 39, 'ParameterName' => 'Tolerance', 'ParameterValue' => '±20%'],
|
['id' => 39, 'name' => 'Tolerance', 'value' => '±20%'],
|
||||||
],
|
],
|
||||||
]]);
|
]]);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function etqp3mPrices(): MockResponse
|
private function etqp3mPrices(): MockResponse
|
||||||
{
|
{
|
||||||
return $this->mockPrices('EUR', 'NET', [[
|
return $this->mockPricesData('EUR', 'NET', [[
|
||||||
'Symbol' => 'ETQP3M6R8KVP',
|
'symbol' => 'ETQP3M6R8KVP',
|
||||||
'PriceList' => [
|
'price_list' => [
|
||||||
['Amount' => 1, 'PriceValue' => 0.589],
|
['amount' => 1, 'price_value' => 0.589],
|
||||||
['Amount' => 5, 'PriceValue' => 0.429],
|
['amount' => 5, 'price_value' => 0.429],
|
||||||
['Amount' => 10, 'PriceValue' => 0.399],
|
['amount' => 10, 'price_value' => 0.399],
|
||||||
],
|
],
|
||||||
]]);
|
]]);
|
||||||
}
|
}
|
||||||
|
|
@ -257,7 +288,11 @@ final class TMEProviderTest extends TestCase
|
||||||
|
|
||||||
public function testSearchByKeyword(): void
|
public function testSearchByKeyword(): void
|
||||||
{
|
{
|
||||||
$this->httpClient->setResponseFactory([$this->smd0603Products()]);
|
// Request order: POST /auth/token, GET /products/search
|
||||||
|
$this->httpClient->setResponseFactory([
|
||||||
|
$this->mockTokenResponse(),
|
||||||
|
$this->smd0603SearchResults(),
|
||||||
|
]);
|
||||||
|
|
||||||
$results = $this->provider->searchByKeyword('SMD0603-5K1-1%');
|
$results = $this->provider->searchByKeyword('SMD0603-5K1-1%');
|
||||||
|
|
||||||
|
|
@ -277,7 +312,10 @@ final class TMEProviderTest extends TestCase
|
||||||
|
|
||||||
public function testGetDetailsWithPercentInPartNumber(): void
|
public function testGetDetailsWithPercentInPartNumber(): void
|
||||||
{
|
{
|
||||||
|
// Request order: POST /auth/token, GET /products, GET /products/files,
|
||||||
|
// GET /products/parameters, GET /products/data
|
||||||
$this->httpClient->setResponseFactory([
|
$this->httpClient->setResponseFactory([
|
||||||
|
$this->mockTokenResponse(),
|
||||||
$this->smd0603Products(),
|
$this->smd0603Products(),
|
||||||
$this->smd0603Files(),
|
$this->smd0603Files(),
|
||||||
$this->smd0603Parameters(),
|
$this->smd0603Parameters(),
|
||||||
|
|
@ -325,7 +363,10 @@ final class TMEProviderTest extends TestCase
|
||||||
|
|
||||||
public function testGetDetailsForEtqp3m6r8kvp(): void
|
public function testGetDetailsForEtqp3m6r8kvp(): void
|
||||||
{
|
{
|
||||||
|
// Request order: POST /auth/token, GET /products, GET /products/files,
|
||||||
|
// GET /products/parameters, GET /products/data
|
||||||
$this->httpClient->setResponseFactory([
|
$this->httpClient->setResponseFactory([
|
||||||
|
$this->mockTokenResponse(),
|
||||||
$this->etqp3mProducts(),
|
$this->etqp3mProducts(),
|
||||||
$this->etqp3mFiles(),
|
$this->etqp3mFiles(),
|
||||||
$this->etqp3mParameters(),
|
$this->etqp3mParameters(),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue