mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2026-02-11 12:09:36 +00:00
Merge upstream/master and resolve translation conflict
Merged new Conrad info provider and generic web provider translations from upstream while keeping Update Manager translations.
This commit is contained in:
commit
6b27f3aa14
22 changed files with 1787 additions and 377 deletions
|
|
@ -30,6 +30,7 @@ use App\Form\InfoProviderSystem\PartSearchType;
|
|||
use App\Services\InfoProviderSystem\ExistingPartFinder;
|
||||
use App\Services\InfoProviderSystem\PartInfoRetriever;
|
||||
use App\Services\InfoProviderSystem\ProviderRegistry;
|
||||
use App\Services\InfoProviderSystem\Providers\GenericWebProvider;
|
||||
use App\Settings\AppSettings;
|
||||
use App\Settings\InfoProviderSystem\InfoProviderGeneralSettings;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
|
@ -39,6 +40,7 @@ use Psr\Log\LoggerInterface;
|
|||
use Symfony\Bridge\Doctrine\Attribute\MapEntity;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\UrlType;
|
||||
use Symfony\Component\HttpClient\Exception\ClientException;
|
||||
use Symfony\Component\HttpClient\Exception\TransportException;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
|
@ -208,4 +210,58 @@ class InfoProviderController extends AbstractController
|
|||
'update_target' => $update_target
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/from_url', name: 'info_providers_from_url')]
|
||||
public function fromURL(Request $request, GenericWebProvider $provider): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('@info_providers.create_parts');
|
||||
|
||||
if (!$provider->isActive()) {
|
||||
$this->addFlash('error', "Generic Web Provider is not active. Please enable it in the provider settings.");
|
||||
return $this->redirectToRoute('info_providers_list');
|
||||
}
|
||||
|
||||
$formBuilder = $this->createFormBuilder();
|
||||
$formBuilder->add('url', UrlType::class, [
|
||||
'label' => 'info_providers.from_url.url.label',
|
||||
'required' => true,
|
||||
]);
|
||||
$formBuilder->add('submit', SubmitType::class, [
|
||||
'label' => 'info_providers.search.submit',
|
||||
]);
|
||||
|
||||
$form = $formBuilder->getForm();
|
||||
$form->handleRequest($request);
|
||||
|
||||
$partDetail = null;
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
//Try to retrieve the part detail from the given URL
|
||||
$url = $form->get('url')->getData();
|
||||
try {
|
||||
$searchResult = $this->infoRetriever->searchByKeyword(
|
||||
keyword: $url,
|
||||
providers: [$provider]
|
||||
);
|
||||
|
||||
if (count($searchResult) === 0) {
|
||||
$this->addFlash('warning', t('info_providers.from_url.no_part_found'));
|
||||
} else {
|
||||
$searchResult = $searchResult[0];
|
||||
//Redirect to the part creation page with the found part detail
|
||||
return $this->redirectToRoute('info_providers_create_part', [
|
||||
'providerKey' => $searchResult->provider_key,
|
||||
'providerId' => $searchResult->provider_id,
|
||||
]);
|
||||
}
|
||||
} catch (ExceptionInterface $e) {
|
||||
$this->addFlash('error', t('info_providers.search.error.general_exception', ['%type%' => (new \ReflectionClass($e))->getShortName()]));
|
||||
}
|
||||
}
|
||||
|
||||
return $this->render('info_providers/from_url/from_url.html.twig', [
|
||||
'form' => $form,
|
||||
'partDetail' => $partDetail,
|
||||
]);
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -135,6 +135,7 @@ final class PartController extends AbstractController
|
|||
'description_params' => $this->partInfoSettings->extractParamsFromDescription ? $parameterExtractor->extractParameters($part->getDescription()) : [],
|
||||
'comment_params' => $this->partInfoSettings->extractParamsFromNotes ? $parameterExtractor->extractParameters($part->getComment()) : [],
|
||||
'withdraw_add_helper' => $withdrawAddHelper,
|
||||
'highlightLotId' => $request->query->getInt('highlightLot', 0),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
|
|
|||
32
src/Exceptions/ProviderIDNotSupportedException.php
Normal file
32
src/Exceptions/ProviderIDNotSupportedException.php
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2026 Jan Böhmer (https://github.com/jbtronics)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
class ProviderIDNotSupportedException extends \RuntimeException
|
||||
{
|
||||
public function fromProvider(string $providerKey, string $id): self
|
||||
{
|
||||
return new self(sprintf('The given ID %s is not supported by the provider %s.', $id, $providerKey,));
|
||||
}
|
||||
}
|
||||
|
|
@ -277,8 +277,11 @@ class BOMImporter
|
|||
// Fetch suppliers once for efficiency
|
||||
$suppliers = $this->entityManager->getRepository(\App\Entity\Parts\Supplier::class)->findAll();
|
||||
$supplierSPNKeys = [];
|
||||
$suppliersByName = []; // Map supplier names to supplier objects
|
||||
foreach ($suppliers as $supplier) {
|
||||
$supplierSPNKeys[] = $supplier->getName() . ' SPN';
|
||||
$supplierName = $supplier->getName();
|
||||
$supplierSPNKeys[] = $supplierName . ' SPN';
|
||||
$suppliersByName[$supplierName] = $supplier;
|
||||
}
|
||||
|
||||
foreach ($csv->getRecords() as $offset => $entry) {
|
||||
|
|
@ -356,6 +359,41 @@ class BOMImporter
|
|||
}
|
||||
}
|
||||
|
||||
// Try to link existing part based on supplier part number if no Part-DB ID is given
|
||||
if ($part === null) {
|
||||
// Check all available supplier SPN fields
|
||||
foreach ($suppliersByName as $supplierName => $supplier) {
|
||||
$supplier_spn = null;
|
||||
|
||||
if (isset($mapped_entry[$supplierName . ' SPN']) && !empty(trim($mapped_entry[$supplierName . ' SPN']))) {
|
||||
$supplier_spn = trim($mapped_entry[$supplierName . ' SPN']);
|
||||
}
|
||||
|
||||
if ($supplier_spn !== null) {
|
||||
// Query for orderdetails with matching supplier and SPN
|
||||
$orderdetail = $this->entityManager->getRepository(\App\Entity\PriceInformations\Orderdetail::class)
|
||||
->findOneBy([
|
||||
'supplier' => $supplier,
|
||||
'supplierpartnr' => $supplier_spn,
|
||||
]);
|
||||
|
||||
if ($orderdetail !== null && $orderdetail->getPart() !== null) {
|
||||
$part = $orderdetail->getPart();
|
||||
$name = $part->getName(); // Update name with actual part name
|
||||
|
||||
$this->logger->info('Linked BOM entry to existing part via supplier SPN', [
|
||||
'supplier' => $supplierName,
|
||||
'supplier_spn' => $supplier_spn,
|
||||
'part_id' => $part->getID(),
|
||||
'part_name' => $part->getName(),
|
||||
]);
|
||||
|
||||
break; // Stop searching once a match is found
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create unique key for this entry (name + part ID)
|
||||
$entry_key = $name . '|' . ($part ? $part->getID() : 'null');
|
||||
|
||||
|
|
|
|||
320
src/Services/InfoProviderSystem/Providers/ConradProvider.php
Normal file
320
src/Services/InfoProviderSystem/Providers/ConradProvider.php
Normal file
|
|
@ -0,0 +1,320 @@
|
|||
<?php
|
||||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2026 Jan Böhmer (https://github.com/jbtronics)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
|
||||
namespace App\Services\InfoProviderSystem\Providers;
|
||||
|
||||
use App\Services\InfoProviderSystem\DTOs\FileDTO;
|
||||
use App\Services\InfoProviderSystem\DTOs\ParameterDTO;
|
||||
use App\Services\InfoProviderSystem\DTOs\PartDetailDTO;
|
||||
use App\Services\InfoProviderSystem\DTOs\PriceDTO;
|
||||
use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO;
|
||||
use App\Services\InfoProviderSystem\DTOs\SearchResultDTO;
|
||||
use App\Settings\InfoProviderSystem\ConradSettings;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
||||
readonly class ConradProvider implements InfoProviderInterface
|
||||
{
|
||||
|
||||
private const SEARCH_ENDPOINT = '/search/1/v3/facetSearch';
|
||||
public const DISTRIBUTOR_NAME = 'Conrad';
|
||||
|
||||
private HttpClientInterface $httpClient;
|
||||
|
||||
public function __construct( HttpClientInterface $httpClient, private ConradSettings $settings)
|
||||
{
|
||||
//We want everything in JSON
|
||||
$this->httpClient = $httpClient->withOptions([
|
||||
'headers' => [
|
||||
'Accept' => 'application/json',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function getProviderInfo(): array
|
||||
{
|
||||
return [
|
||||
'name' => 'Conrad',
|
||||
'description' => 'Retrieves part information from conrad.de',
|
||||
'url' => 'https://www.conrad.de/',
|
||||
'disabled_help' => 'Set API key in settings',
|
||||
'settings_class' => ConradSettings::class,
|
||||
];
|
||||
}
|
||||
|
||||
public function getProviderKey(): string
|
||||
{
|
||||
return 'conrad';
|
||||
}
|
||||
|
||||
public function isActive(): bool
|
||||
{
|
||||
return !empty($this->settings->apiKey);
|
||||
}
|
||||
|
||||
private function getProductUrl(string $productId): string
|
||||
{
|
||||
return 'https://' . $this->settings->shopID->getDomain() . '/' . $this->settings->shopID->getLanguage() . '/p/' . $productId;
|
||||
}
|
||||
|
||||
private function getFootprintFromTechnicalDetails(array $technicalDetails): ?string
|
||||
{
|
||||
foreach ($technicalDetails as $detail) {
|
||||
if ($detail['name'] === 'ATT_LOV_HOUSING_SEMICONDUCTORS') {
|
||||
return $detail['values'][0] ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function searchByKeyword(string $keyword): array
|
||||
{
|
||||
$url = $this->settings->shopID->getAPIRoot() . self::SEARCH_ENDPOINT . '/'
|
||||
. $this->settings->shopID->getDomainEnd() . '/' . $this->settings->shopID->getLanguage()
|
||||
. '/' . $this->settings->shopID->getCustomerType();
|
||||
|
||||
$response = $this->httpClient->request('POST', $url, [
|
||||
'query' => [
|
||||
'apikey' => $this->settings->apiKey,
|
||||
],
|
||||
'json' => [
|
||||
'query' => $keyword,
|
||||
'size' => 50,
|
||||
'sort' => [["field"=>"_score","order"=>"desc"]],
|
||||
],
|
||||
]);
|
||||
|
||||
$out = [];
|
||||
$results = $response->toArray();
|
||||
|
||||
foreach($results['hits'] as $result) {
|
||||
|
||||
$out[] = new SearchResultDTO(
|
||||
provider_key: $this->getProviderKey(),
|
||||
provider_id: $result['productId'],
|
||||
name: $result['manufacturerId'] ?? $result['productId'],
|
||||
description: $result['title'] ?? '',
|
||||
manufacturer: $result['brand']['name'] ?? null,
|
||||
mpn: $result['manufacturerId'] ?? null,
|
||||
preview_image_url: $result['image'] ?? null,
|
||||
provider_url: $this->getProductUrl($result['productId']),
|
||||
footprint: $this->getFootprintFromTechnicalDetails($result['technicalDetails'] ?? []),
|
||||
);
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
private function getFootprintFromTechnicalAttributes(array $technicalDetails): ?string
|
||||
{
|
||||
foreach ($technicalDetails as $detail) {
|
||||
if ($detail['attributeID'] === 'ATT.LOV.HOUSING_SEMICONDUCTORS') {
|
||||
return $detail['values'][0]['value'] ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $technicalAttributes
|
||||
* @return array<ParameterDTO>
|
||||
*/
|
||||
private function technicalAttributesToParameters(array $technicalAttributes): array
|
||||
{
|
||||
return array_map(static function (array $p) {
|
||||
if (count($p['values']) === 1) { //Single value attribute
|
||||
if (array_key_exists('unit', $p['values'][0])) {
|
||||
return ParameterDTO::parseValueField( //With unit
|
||||
name: $p['attributeName'],
|
||||
value: $p['values'][0]['value'],
|
||||
unit: $p['values'][0]['unit']['name'],
|
||||
);
|
||||
}
|
||||
|
||||
return ParameterDTO::parseValueIncludingUnit(
|
||||
name: $p['attributeName'],
|
||||
value: $p['values'][0]['value'],
|
||||
);
|
||||
}
|
||||
|
||||
if (count($p['values']) === 2) { //Multi value attribute (e.g. min/max)
|
||||
$value = $p['values'][0]['value'] ?? null;
|
||||
$value2 = $p['values'][1]['value'] ?? null;
|
||||
$unit = $p['values'][0]['unit']['name'] ?? '';
|
||||
$unit2 = $p['values'][1]['unit']['name'] ?? '';
|
||||
if ($unit === $unit2 && is_numeric($value) && is_numeric($value2)) {
|
||||
if (array_key_exists('unit', $p['values'][0])) { //With unit
|
||||
return new ParameterDTO(
|
||||
name: $p['attributeName'],
|
||||
value_min: (float)$value,
|
||||
value_max: (float)$value2,
|
||||
unit: $unit,
|
||||
);
|
||||
}
|
||||
|
||||
return new ParameterDTO(
|
||||
name: $p['attributeName'],
|
||||
value_min: (float)$value,
|
||||
value_max: (float)$value2,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// fallback implementation
|
||||
$values = implode(", ", array_map(fn($q) =>
|
||||
array_key_exists('unit', $q) ? $q['value']." ". ($q['unit']['name'] ?? $q['unit']) : $q['value']
|
||||
, $p['values']));
|
||||
return ParameterDTO::parseValueIncludingUnit(
|
||||
name: $p['attributeName'],
|
||||
value: $values,
|
||||
);
|
||||
}, $technicalAttributes);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $productMedia
|
||||
* @return array<FileDTO>
|
||||
*/
|
||||
public function productMediaToDatasheets(array $productMedia): array
|
||||
{
|
||||
$files = [];
|
||||
foreach ($productMedia['manuals'] as $manual) {
|
||||
//Filter out unwanted languages
|
||||
if (!empty($this->settings->attachmentLanguageFilter) && !in_array($manual['language'], $this->settings->attachmentLanguageFilter, true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$files[] = new FileDTO($manual['fullUrl'], $manual['title'] . ' (' . $manual['language'] . ')');
|
||||
}
|
||||
|
||||
return $files;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Queries prices for a given product ID. It makes a POST request to the Conrad API
|
||||
* @param string $productId
|
||||
* @return PurchaseInfoDTO
|
||||
*/
|
||||
private function queryPrices(string $productId): PurchaseInfoDTO
|
||||
{
|
||||
$priceQueryURL = $this->settings->shopID->getAPIRoot() . '/price-availability/4/'
|
||||
. $this->settings->shopID->getShopID() . '/facade';
|
||||
|
||||
$response = $this->httpClient->request('POST', $priceQueryURL, [
|
||||
'query' => [
|
||||
'apikey' => $this->settings->apiKey,
|
||||
'overrideCalculationSchema' => $this->settings->includeVAT ? 'GROSS' : 'NET'
|
||||
],
|
||||
'json' => [
|
||||
'ns:inputArticleItemList' => [
|
||||
"#namespaces" => [
|
||||
"ns" => "http://www.conrad.de/ccp/basit/service/article/priceandavailabilityservice/api"
|
||||
],
|
||||
'articles' => [
|
||||
[
|
||||
"articleID" => $productId,
|
||||
"calculatePrice" => true,
|
||||
"checkAvailability" => true,
|
||||
],
|
||||
]
|
||||
]
|
||||
]
|
||||
]);
|
||||
|
||||
$result = $response->toArray();
|
||||
|
||||
$priceInfo = $result['priceAndAvailabilityFacadeResponse']['priceAndAvailability']['price'] ?? [];
|
||||
$price = $priceInfo['price'] ?? "0.0";
|
||||
$currency = $priceInfo['currency'] ?? "EUR";
|
||||
$includesVat = !$priceInfo['isGrossAmount'] || $priceInfo['isGrossAmount'] === "true";
|
||||
$minOrderAmount = $result['priceAndAvailabilityFacadeResponse']['priceAndAvailability']['availabilityStatus']['minimumOrderQuantity'] ?? 1;
|
||||
|
||||
$prices = [];
|
||||
foreach ($priceInfo['priceScale'] ?? [] as $priceScale) {
|
||||
$prices[] = new PriceDTO(
|
||||
minimum_discount_amount: max($priceScale['scaleFrom'], $minOrderAmount),
|
||||
price: (string)$priceScale['pricePerUnit'],
|
||||
currency_iso_code: $currency,
|
||||
includes_tax: $includesVat
|
||||
);
|
||||
}
|
||||
if (empty($prices)) { //Fallback if no price scales are defined
|
||||
$prices[] = new PriceDTO(
|
||||
minimum_discount_amount: $minOrderAmount,
|
||||
price: (string)$price,
|
||||
currency_iso_code: $currency,
|
||||
includes_tax: $includesVat
|
||||
);
|
||||
}
|
||||
|
||||
return new PurchaseInfoDTO(
|
||||
distributor_name: self::DISTRIBUTOR_NAME,
|
||||
order_number: $productId,
|
||||
prices: $prices,
|
||||
product_url: $this->getProductUrl($productId)
|
||||
);
|
||||
}
|
||||
|
||||
public function getDetails(string $id): PartDetailDTO
|
||||
{
|
||||
$productInfoURL = $this->settings->shopID->getAPIRoot() . '/product/1/service/' . $this->settings->shopID->getShopID()
|
||||
. '/product/' . $id;
|
||||
|
||||
$response = $this->httpClient->request('GET', $productInfoURL, [
|
||||
'query' => [
|
||||
'apikey' => $this->settings->apiKey,
|
||||
]
|
||||
]);
|
||||
|
||||
$data = $response->toArray();
|
||||
|
||||
return new PartDetailDTO(
|
||||
provider_key: $this->getProviderKey(),
|
||||
provider_id: $data['shortProductNumber'],
|
||||
name: $data['productFullInformation']['manufacturer']['name'] ?? $data['productFullInformation']['manufacturer']['id'] ?? $data['shortProductNumber'],
|
||||
description: $data['productShortInformation']['title'] ?? '',
|
||||
category: $data['productShortInformation']['articleGroupName'] ?? null,
|
||||
manufacturer: $data['brand']['displayName'] !== null ? preg_replace("/[\u{2122}\u{00ae}]/", "", $data['brand']['displayName']) : null, //Replace ™ and ® symbols
|
||||
mpn: $data['productFullInformation']['manufacturer']['id'] ?? null,
|
||||
preview_image_url: $data['productShortInformation']['mainImage']['imageUrl'] ?? null,
|
||||
provider_url: $this->getProductUrl($data['shortProductNumber']),
|
||||
footprint: $this->getFootprintFromTechnicalAttributes($data['productFullInformation']['technicalAttributes'] ?? []),
|
||||
notes: $data['productFullInformation']['description'] ?? null,
|
||||
datasheets: $this->productMediaToDatasheets($data['productMedia'] ?? []),
|
||||
parameters: $this->technicalAttributesToParameters($data['productFullInformation']['technicalAttributes'] ?? []),
|
||||
vendor_infos: [$this->queryPrices($data['shortProductNumber'])]
|
||||
);
|
||||
}
|
||||
|
||||
public function getCapabilities(): array
|
||||
{
|
||||
return [
|
||||
ProviderCapabilities::BASIC,
|
||||
ProviderCapabilities::PICTURE,
|
||||
ProviderCapabilities::DATASHEET,
|
||||
ProviderCapabilities::PRICE,
|
||||
];
|
||||
}
|
||||
}
|
||||
336
src/Services/InfoProviderSystem/Providers/GenericWebProvider.php
Normal file
336
src/Services/InfoProviderSystem/Providers/GenericWebProvider.php
Normal file
|
|
@ -0,0 +1,336 @@
|
|||
<?php
|
||||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2026 Jan Böhmer (https://github.com/jbtronics)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
|
||||
namespace App\Services\InfoProviderSystem\Providers;
|
||||
|
||||
use App\Exceptions\ProviderIDNotSupportedException;
|
||||
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\GenericWebProviderSettings;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Financial\Securities\Price;
|
||||
use Symfony\Component\DomCrawler\Crawler;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
||||
class GenericWebProvider implements InfoProviderInterface
|
||||
{
|
||||
|
||||
public const DISTRIBUTOR_NAME = 'Website';
|
||||
|
||||
private readonly HttpClientInterface $httpClient;
|
||||
|
||||
public function __construct(HttpClientInterface $httpClient, private readonly GenericWebProviderSettings $settings)
|
||||
{
|
||||
$this->httpClient = $httpClient->withOptions(
|
||||
[
|
||||
'headers' => [
|
||||
'User-Agent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36',
|
||||
],
|
||||
'timeout' => 15,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function getProviderInfo(): array
|
||||
{
|
||||
return [
|
||||
'name' => 'Generic Web URL',
|
||||
'description' => 'Tries to extract a part from a given product webpage URL using common metadata standards like JSON-LD and OpenGraph.',
|
||||
//'url' => 'https://example.com',
|
||||
'disabled_help' => 'Enable in settings to use this provider',
|
||||
'settings_class' => GenericWebProviderSettings::class,
|
||||
];
|
||||
}
|
||||
|
||||
public function getProviderKey(): string
|
||||
{
|
||||
return 'generic_web';
|
||||
}
|
||||
|
||||
public function isActive(): bool
|
||||
{
|
||||
return $this->settings->enabled;
|
||||
}
|
||||
|
||||
public function searchByKeyword(string $keyword): array
|
||||
{
|
||||
try {
|
||||
return [
|
||||
$this->getDetails($keyword)
|
||||
]; } catch (ProviderIDNotSupportedException $e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private function extractShopName(string $url): string
|
||||
{
|
||||
$host = parse_url($url, PHP_URL_HOST);
|
||||
if ($host === false || $host === null) {
|
||||
return self::DISTRIBUTOR_NAME;
|
||||
}
|
||||
return $host;
|
||||
}
|
||||
|
||||
private function productJsonLdToPart(array $jsonLd, string $url, Crawler $dom): PartDetailDTO
|
||||
{
|
||||
$notes = $jsonLd['description'] ?? "";
|
||||
if (isset($jsonLd['disambiguatingDescription'])) {
|
||||
if (!empty($notes)) {
|
||||
$notes .= "\n\n";
|
||||
}
|
||||
$notes .= $jsonLd['disambiguatingDescription'];
|
||||
}
|
||||
|
||||
$vendor_infos = null;
|
||||
if (isset($jsonLd['offers'])) {
|
||||
|
||||
if (array_is_list($jsonLd['offers'])) {
|
||||
$offer = $jsonLd['offers'][0];
|
||||
} else {
|
||||
$offer = $jsonLd['offers'];
|
||||
}
|
||||
|
||||
//Make $jsonLd['url'] absolute if it's relative
|
||||
if (isset($jsonLd['url']) && parse_url($jsonLd['url'], PHP_URL_SCHEME) === null) {
|
||||
$parsedUrl = parse_url($url);
|
||||
$scheme = $parsedUrl['scheme'] ?? 'https';
|
||||
$host = $parsedUrl['host'] ?? '';
|
||||
$jsonLd['url'] = $scheme.'://'.$host.$jsonLd['url'];
|
||||
}
|
||||
|
||||
$prices = [];
|
||||
if (isset($offer['price'])) {
|
||||
$prices[] = new PriceDTO(
|
||||
minimum_discount_amount: 1,
|
||||
price: (string) $offer['price'],
|
||||
currency_iso_code: $offer['priceCurrency'] ?? null
|
||||
);
|
||||
} else if (isset($offer['offers']) && array_is_list($offer['offers'])) {
|
||||
//Some sites nest offers
|
||||
foreach ($offer['offers'] as $subOffer) {
|
||||
if (isset($subOffer['price'])) {
|
||||
$prices[] = new PriceDTO(
|
||||
minimum_discount_amount: 1,
|
||||
price: (string) $subOffer['price'],
|
||||
currency_iso_code: $subOffer['priceCurrency'] ?? null
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$vendor_infos = [new PurchaseInfoDTO(
|
||||
distributor_name: $this->extractShopName($url),
|
||||
order_number: (string) ($jsonLd['sku'] ?? $jsonLd['@id'] ?? $jsonLd['gtin'] ?? 'Unknown'),
|
||||
prices: $prices,
|
||||
product_url: $jsonLd['url'] ?? $url,
|
||||
)];
|
||||
}
|
||||
|
||||
$image = null;
|
||||
if (isset($jsonLd['image'])) {
|
||||
if (is_array($jsonLd['image'])) {
|
||||
if (array_is_list($jsonLd['image'])) {
|
||||
$image = $jsonLd['image'][0] ?? null;
|
||||
}
|
||||
} elseif (is_string($jsonLd['image'])) {
|
||||
$image = $jsonLd['image'];
|
||||
}
|
||||
}
|
||||
//If image is an object with @type ImageObject, extract the url
|
||||
if (is_array($image) && isset($image['@type']) && $image['@type'] === 'ImageObject') {
|
||||
$image = $image['contentUrl'] ?? $image['url'] ?? null;
|
||||
}
|
||||
|
||||
//Try to extract parameters from additionalProperty
|
||||
$parameters = [];
|
||||
if (isset($jsonLd['additionalProperty']) && array_is_list($jsonLd['additionalProperty'])) {
|
||||
foreach ($jsonLd['additionalProperty'] as $property) { //TODO: Handle minValue and maxValue
|
||||
if (isset ($property['unitText'])) {
|
||||
$parameters[] = ParameterDTO::parseValueField(
|
||||
name: $property['name'] ?? 'Unknown',
|
||||
value: $property['value'] ?? '',
|
||||
unit: $property['unitText']
|
||||
);
|
||||
} else {
|
||||
$parameters[] = ParameterDTO::parseValueIncludingUnit(
|
||||
name: $property['name'] ?? 'Unknown',
|
||||
value: $property['value'] ?? ''
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return new PartDetailDTO(
|
||||
provider_key: $this->getProviderKey(),
|
||||
provider_id: $url,
|
||||
name: $jsonLd ['name'] ?? 'Unknown Name',
|
||||
description: $this->getMetaContent($dom, 'og:description') ?? $this->getMetaContent($dom, 'description') ?? '',
|
||||
category: isset($jsonLd['category']) && is_string($jsonLd['category']) ? $jsonLd['category'] : null,
|
||||
manufacturer: $jsonLd['manufacturer']['name'] ?? $jsonLd['brand']['name'] ?? null,
|
||||
mpn: $jsonLd['mpn'] ?? null,
|
||||
preview_image_url: $image,
|
||||
provider_url: $url,
|
||||
notes: $notes,
|
||||
parameters: $parameters,
|
||||
vendor_infos: $vendor_infos,
|
||||
mass: isset($jsonLd['weight']['value']) ? (float)$jsonLd['weight']['value'] : null,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes JSON in a forgiving way, trying to fix common issues.
|
||||
* @param string $json
|
||||
* @return array
|
||||
* @throws \JsonException
|
||||
*/
|
||||
private function json_decode_forgiving(string $json): array
|
||||
{
|
||||
//Sanitize common issues
|
||||
$json = preg_replace("/[\r\n]+/", " ", $json);
|
||||
return json_decode($json, true, 512, JSON_THROW_ON_ERROR);
|
||||
}
|
||||
|
||||
private function getMetaContent(Crawler $dom, string $name): ?string
|
||||
{
|
||||
$meta = $dom->filter('meta[property="'.$name.'"]');
|
||||
if ($meta->count() > 0) {
|
||||
return $meta->attr('content');
|
||||
}
|
||||
|
||||
//Try name attribute
|
||||
$meta = $dom->filter('meta[name="'.$name.'"]');
|
||||
if ($meta->count() > 0) {
|
||||
return $meta->attr('content');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getDetails(string $id): PartDetailDTO
|
||||
{
|
||||
//Add scheme if missing
|
||||
if (!preg_match('/^https?:\/\//', $id)) {
|
||||
//Remove any leading slashes
|
||||
$id = ltrim($id, '/');
|
||||
|
||||
$id = 'https://'.$id;
|
||||
}
|
||||
|
||||
$url = $id;
|
||||
|
||||
//If this is not a valid URL with host, domain and path, throw an exception
|
||||
if (filter_var($url, FILTER_VALIDATE_URL) === false ||
|
||||
parse_url($url, PHP_URL_HOST) === null ||
|
||||
parse_url($url, PHP_URL_PATH) === null) {
|
||||
throw new ProviderIDNotSupportedException("The given ID is not a valid URL: ".$id);
|
||||
}
|
||||
|
||||
//Try to get the webpage content
|
||||
$response = $this->httpClient->request('GET', $url);
|
||||
$content = $response->getContent();
|
||||
|
||||
$dom = new Crawler($content);
|
||||
|
||||
//Try to determine a canonical URL
|
||||
$canonicalURL = $url;
|
||||
if ($dom->filter('link[rel="canonical"]')->count() > 0) {
|
||||
$canonicalURL = $dom->filter('link[rel="canonical"]')->attr('href');
|
||||
} else if ($dom->filter('meta[property="og:url"]')->count() > 0) {
|
||||
$canonicalURL = $dom->filter('meta[property="og:url"]')->attr('content');
|
||||
}
|
||||
|
||||
//If the canonical URL is relative, make it absolute
|
||||
if (parse_url($canonicalURL, PHP_URL_SCHEME) === null) {
|
||||
$parsedUrl = parse_url($url);
|
||||
$scheme = $parsedUrl['scheme'] ?? 'https';
|
||||
$host = $parsedUrl['host'] ?? '';
|
||||
$canonicalURL = $scheme.'://'.$host.$canonicalURL;
|
||||
}
|
||||
|
||||
//Try to find json-ld data in the head
|
||||
$jsonLdNodes = $dom->filter('script[type="application/ld+json"]');
|
||||
foreach ($jsonLdNodes as $node) {
|
||||
$jsonLd = $this->json_decode_forgiving($node->textContent);
|
||||
//If the content of json-ld is an array, try to find a product inside
|
||||
if (!array_is_list($jsonLd)) {
|
||||
$jsonLd = [$jsonLd];
|
||||
}
|
||||
foreach ($jsonLd as $item) {
|
||||
if (isset($item['@type']) && $item['@type'] === 'Product') {
|
||||
return $this->productJsonLdToPart($item, $canonicalURL, $dom);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//If no JSON-LD data is found, try to extract basic data from meta tags
|
||||
$pageTitle = $dom->filter('title')->count() > 0 ? $dom->filter('title')->text() : 'Unknown';
|
||||
|
||||
$prices = [];
|
||||
if ($price = $this->getMetaContent($dom, 'product:price:amount')) {
|
||||
$prices[] = new PriceDTO(
|
||||
minimum_discount_amount: 1,
|
||||
price: $price,
|
||||
currency_iso_code: $this->getMetaContent($dom, 'product:price:currency'),
|
||||
);
|
||||
} else {
|
||||
//Amazon fallback
|
||||
$amazonAmount = $dom->filter('input[type="hidden"][name*="amount"]');
|
||||
if ($amazonAmount->count() > 0) {
|
||||
$prices[] = new PriceDTO(
|
||||
minimum_discount_amount: 1,
|
||||
price: $amazonAmount->first()->attr('value'),
|
||||
currency_iso_code: $dom->filter('input[type="hidden"][name*="currencyCode"]')->first()->attr('value'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$vendor_infos = [new PurchaseInfoDTO(
|
||||
distributor_name: $this->extractShopName($canonicalURL),
|
||||
order_number: 'Unknown',
|
||||
prices: $prices,
|
||||
product_url: $canonicalURL,
|
||||
)];
|
||||
|
||||
return new PartDetailDTO(
|
||||
provider_key: $this->getProviderKey(),
|
||||
provider_id: $canonicalURL,
|
||||
name: $this->getMetaContent($dom, 'og:title') ?? $pageTitle,
|
||||
description: $this->getMetaContent($dom, 'og:description') ?? $this->getMetaContent($dom, 'description') ?? '',
|
||||
manufacturer: $this->getMetaContent($dom, 'product:brand'),
|
||||
preview_image_url: $this->getMetaContent($dom, 'og:image'),
|
||||
provider_url: $canonicalURL,
|
||||
vendor_infos: $vendor_infos,
|
||||
);
|
||||
}
|
||||
|
||||
public function getCapabilities(): array
|
||||
{
|
||||
return [
|
||||
ProviderCapabilities::BASIC,
|
||||
ProviderCapabilities::PICTURE,
|
||||
ProviderCapabilities::PRICE
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -92,7 +92,7 @@ final class BarcodeRedirector
|
|||
throw new EntityNotFoundException();
|
||||
}
|
||||
|
||||
return $this->urlGenerator->generate('app_part_show', ['id' => $lot->getPart()->getID()]);
|
||||
return $this->urlGenerator->generate('app_part_show', ['id' => $lot->getPart()->getID(), 'highlightLot' => $lot->getID()]);
|
||||
|
||||
case LabelSupportedElement::STORELOCATION:
|
||||
return $this->urlGenerator->generate('part_list_store_location', ['id' => $barcodeScan->target_id]);
|
||||
|
|
|
|||
|
|
@ -39,6 +39,8 @@ use App\Entity\UserSystem\User;
|
|||
use App\Helpers\Trees\TreeViewNode;
|
||||
use App\Services\Cache\UserCacheKeyGenerator;
|
||||
use App\Services\ElementTypeNameGenerator;
|
||||
use App\Services\InfoProviderSystem\Providers\GenericWebProvider;
|
||||
use App\Settings\InfoProviderSystem\GenericWebProviderSettings;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use Symfony\Contracts\Cache\ItemInterface;
|
||||
|
|
@ -58,6 +60,7 @@ class ToolsTreeBuilder
|
|||
protected UserCacheKeyGenerator $keyGenerator,
|
||||
protected Security $security,
|
||||
private readonly ElementTypeNameGenerator $elementTypeNameGenerator,
|
||||
private readonly GenericWebProviderSettings $genericWebProviderSettings
|
||||
) {
|
||||
}
|
||||
|
||||
|
|
@ -147,6 +150,13 @@ class ToolsTreeBuilder
|
|||
$this->urlGenerator->generate('info_providers_search')
|
||||
))->setIcon('fa-treeview fa-fw fa-solid fa-cloud-arrow-down');
|
||||
|
||||
if ($this->genericWebProviderSettings->enabled) {
|
||||
$nodes[] = (new TreeViewNode(
|
||||
$this->translator->trans('info_providers.from_url.title'),
|
||||
$this->urlGenerator->generate('info_providers_from_url')
|
||||
))->setIcon('fa-treeview fa-fw fa-solid fa-book-atlas');
|
||||
}
|
||||
|
||||
$nodes[] = (new TreeViewNode(
|
||||
$this->translator->trans('info_providers.bulk_import.manage_jobs'),
|
||||
$this->urlGenerator->generate('bulk_info_provider_manage')
|
||||
|
|
|
|||
77
src/Settings/InfoProviderSystem/ConradSettings.php
Normal file
77
src/Settings/InfoProviderSystem/ConradSettings.php
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
<?php
|
||||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2026 Jan Böhmer (https://github.com/jbtronics)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
|
||||
namespace App\Settings\InfoProviderSystem;
|
||||
|
||||
use App\Form\Type\APIKeyType;
|
||||
use App\Settings\SettingsIcon;
|
||||
use Jbtronics\SettingsBundle\Metadata\EnvVarMode;
|
||||
use Jbtronics\SettingsBundle\ParameterTypes\ArrayType;
|
||||
use Jbtronics\SettingsBundle\ParameterTypes\StringType;
|
||||
use Jbtronics\SettingsBundle\Settings\Settings;
|
||||
use Jbtronics\SettingsBundle\Settings\SettingsParameter;
|
||||
use Jbtronics\SettingsBundle\Settings\SettingsTrait;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CountryType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\EnumType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\LanguageType;
|
||||
use Symfony\Component\Translation\TranslatableMessage as TM;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
#[Settings(label: new TM("settings.ips.conrad"))]
|
||||
#[SettingsIcon("fa-plug")]
|
||||
class ConradSettings
|
||||
{
|
||||
use SettingsTrait;
|
||||
|
||||
#[SettingsParameter(label: new TM("settings.ips.element14.apiKey"),
|
||||
formType: APIKeyType::class,
|
||||
formOptions: ["help_html" => true], envVar: "PROVIDER_CONRAD_API_KEY", envVarMode: EnvVarMode::OVERWRITE)]
|
||||
public ?string $apiKey = null;
|
||||
|
||||
#[SettingsParameter(label: new TM("settings.ips.conrad.shopID"),
|
||||
description: new TM("settings.ips.conrad.shopID.description"),
|
||||
formType: EnumType::class,
|
||||
formOptions: ['class' => ConradShopIDs::class],
|
||||
)]
|
||||
public ConradShopIDs $shopID = ConradShopIDs::COM_B2B;
|
||||
|
||||
#[SettingsParameter(label: new TM("settings.ips.reichelt.include_vat"))]
|
||||
public bool $includeVAT = true;
|
||||
|
||||
/**
|
||||
* @var array|string[] Only attachments in these languages will be downloaded (ISO 639-1 codes)
|
||||
*/
|
||||
#[Assert\Unique()]
|
||||
#[Assert\All([new Assert\Language()])]
|
||||
#[SettingsParameter(type: ArrayType::class,
|
||||
label: new TM("settings.ips.conrad.attachment_language_filter"), description: new TM("settings.ips.conrad.attachment_language_filter.description"),
|
||||
options: ['type' => StringType::class],
|
||||
formType: LanguageType::class,
|
||||
formOptions: [
|
||||
'multiple' => true,
|
||||
'preferred_choices' => ['en', 'de', 'fr', 'it', 'cs', 'da', 'nl', 'hu', 'hr', 'sk', 'pl']
|
||||
],
|
||||
)]
|
||||
public array $attachmentLanguageFilter = ['en'];
|
||||
}
|
||||
167
src/Settings/InfoProviderSystem/ConradShopIDs.php
Normal file
167
src/Settings/InfoProviderSystem/ConradShopIDs.php
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
<?php
|
||||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2026 Jan Böhmer (https://github.com/jbtronics)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
|
||||
namespace App\Settings\InfoProviderSystem;
|
||||
|
||||
use Symfony\Contracts\Translation\TranslatableInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
enum ConradShopIDs: string implements TranslatableInterface
|
||||
{
|
||||
case COM_B2B = 'HP_COM_B2B';
|
||||
case DE_B2B = 'CQ_DE_B2B';
|
||||
case DE_B2C = 'CQ_DE_B2C';
|
||||
case AT_B2C = 'CQ_AT_B2C';
|
||||
case CH_B2C_DE = 'CQ_CH_B2C_DE';
|
||||
case CH_B2C_FR = 'CQ_CH_B2C_FR';
|
||||
case SE_B2B = 'HP_SE_B2B';
|
||||
case HU_B2C = 'CQ_HU_B2C';
|
||||
case CZ_B2B = 'HP_CZ_B2B';
|
||||
case SI_B2B = 'HP_SI_B2B';
|
||||
case SK_B2B = 'HP_SK_B2B';
|
||||
case BE_B2B = 'HP_BE_B2B';
|
||||
case PL_B2B = 'HP_PL_B2B';
|
||||
case NL_B2B = 'CQ_NL_B2B';
|
||||
case NL_B2C = 'CQ_NL_B2C';
|
||||
case DK_B2B = 'HP_DK_B2B';
|
||||
case IT_B2B = 'HP_IT_B2B';
|
||||
|
||||
case FR_B2B = 'HP_FR_B2B';
|
||||
case AT_B2B = 'CQ_AT_B2B';
|
||||
case HR_B2B = 'HP_HR_B2B';
|
||||
|
||||
|
||||
public function trans(TranslatorInterface $translator, ?string $locale = null): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::DE_B2B => "conrad.de (B2B)",
|
||||
self::AT_B2C => "conrad.at (B2C)",
|
||||
self::CH_B2C_DE => "conrad.ch DE (B2C)",
|
||||
self::CH_B2C_FR => "conrad.ch FR (B2C)",
|
||||
self::SE_B2B => "conrad.se (B2B)",
|
||||
self::HU_B2C => "conrad.hu (B2C)",
|
||||
self::CZ_B2B => "conrad.cz (B2B)",
|
||||
self::SI_B2B => "conrad.si (B2B)",
|
||||
self::SK_B2B => "conrad.sk (B2B)",
|
||||
self::BE_B2B => "conrad.be (B2B)",
|
||||
self::DE_B2C => "conrad.de (B2C)",
|
||||
self::PL_B2B => "conrad.pl (B2B)",
|
||||
self::NL_B2B => "conrad.nl (B2B)",
|
||||
self::DK_B2B => "conradelektronik.dk (B2B)",
|
||||
self::IT_B2B => "conrad.it (B2B)",
|
||||
self::NL_B2C => "conrad.nl (B2C)",
|
||||
self::FR_B2B => "conrad.fr (B2B)",
|
||||
self::COM_B2B => "conrad.com (B2B)",
|
||||
self::AT_B2B => "conrad.at (B2B)",
|
||||
self::HR_B2B => "conrad.hr (B2B)",
|
||||
};
|
||||
}
|
||||
|
||||
public function getDomain(): string
|
||||
{
|
||||
if ($this === self::DK_B2B) {
|
||||
return 'conradelektronik.dk';
|
||||
}
|
||||
|
||||
return 'conrad.' . $this->getDomainEnd();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the API root URL for this shop ID. e.g. https://api.conrad.de
|
||||
* @return string
|
||||
*/
|
||||
public function getAPIRoot(): string
|
||||
{
|
||||
return 'https://api.' . $this->getDomain();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the shop ID value used in the API requests. e.g. 'CQ_DE_B2B'
|
||||
* @return string
|
||||
*/
|
||||
public function getShopID(): string
|
||||
{
|
||||
if ($this === self::CH_B2C_FR || $this === self::CH_B2C_DE) {
|
||||
return 'CQ_CH_B2C';
|
||||
}
|
||||
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
public function getDomainEnd(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::DE_B2B, self::DE_B2C => 'de',
|
||||
self::AT_B2B, self::AT_B2C => 'at',
|
||||
self::CH_B2C_DE => 'ch', self::CH_B2C_FR => 'ch',
|
||||
self::SE_B2B => 'se',
|
||||
self::HU_B2C => 'hu',
|
||||
self::CZ_B2B => 'cz',
|
||||
self::SI_B2B => 'si',
|
||||
self::SK_B2B => 'sk',
|
||||
self::BE_B2B => 'be',
|
||||
self::PL_B2B => 'pl',
|
||||
self::NL_B2B, self::NL_B2C => 'nl',
|
||||
self::DK_B2B => 'dk',
|
||||
self::IT_B2B => 'it',
|
||||
self::FR_B2B => 'fr',
|
||||
self::COM_B2B => 'com',
|
||||
self::HR_B2B => 'hr',
|
||||
};
|
||||
}
|
||||
|
||||
public function getLanguage(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::DE_B2B, self::DE_B2C, self::AT_B2B, self::AT_B2C => 'de',
|
||||
self::CH_B2C_DE => 'de', self::CH_B2C_FR => 'fr',
|
||||
self::SE_B2B => 'sv',
|
||||
self::HU_B2C => 'hu',
|
||||
self::CZ_B2B => 'cs',
|
||||
self::SI_B2B => 'sl',
|
||||
self::SK_B2B => 'sk',
|
||||
self::BE_B2B => 'nl',
|
||||
self::PL_B2B => 'pl',
|
||||
self::NL_B2B, self::NL_B2C => 'nl',
|
||||
self::DK_B2B => 'da',
|
||||
self::IT_B2B => 'it',
|
||||
self::FR_B2B => 'fr',
|
||||
self::COM_B2B => 'en',
|
||||
self::HR_B2B => 'hr',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the customer type for this shop ID. e.g. 'b2b' or 'b2c'
|
||||
* @return string 'b2b' or 'b2c'
|
||||
*/
|
||||
public function getCustomerType(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::DE_B2B, self::AT_B2B, self::SE_B2B, self::CZ_B2B, self::SI_B2B,
|
||||
self::SK_B2B, self::BE_B2B, self::PL_B2B, self::NL_B2B, self::DK_B2B,
|
||||
self::IT_B2B, self::FR_B2B, self::COM_B2B, self::HR_B2B => 'b2b',
|
||||
self::DE_B2C, self::AT_B2C, self::CH_B2C_DE, self::CH_B2C_FR, self::HU_B2C, self::NL_B2C => 'b2c',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2026 Jan Böhmer (https://github.com/jbtronics)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
|
||||
namespace App\Settings\InfoProviderSystem;
|
||||
|
||||
use App\Settings\SettingsIcon;
|
||||
use Jbtronics\SettingsBundle\Metadata\EnvVarMode;
|
||||
use Jbtronics\SettingsBundle\Settings\Settings;
|
||||
use Jbtronics\SettingsBundle\Settings\SettingsParameter;
|
||||
use Jbtronics\SettingsBundle\Settings\SettingsTrait;
|
||||
use Symfony\Component\Translation\TranslatableMessage as TM;
|
||||
|
||||
#[Settings(name: "generic_web_provider", label: new TM("settings.ips.generic_web_provider"), description: new TM("settings.ips.generic_web_provider.description"))]
|
||||
#[SettingsIcon("fa-plug")]
|
||||
class GenericWebProviderSettings
|
||||
{
|
||||
use SettingsTrait;
|
||||
|
||||
#[SettingsParameter(label: new TM("settings.ips.lcsc.enabled"), description: new TM("settings.ips.generic_web_provider.enabled.help"),
|
||||
envVar: "bool:PROVIDER_GENERIC_WEB_ENABLED", envVarMode: EnvVarMode::OVERWRITE
|
||||
)]
|
||||
public bool $enabled = false;
|
||||
}
|
||||
|
|
@ -37,6 +37,9 @@ class InfoProviderSettings
|
|||
#[EmbeddedSettings]
|
||||
public ?InfoProviderGeneralSettings $general = null;
|
||||
|
||||
#[EmbeddedSettings]
|
||||
public ?GenericWebProviderSettings $genericWebProvider = null;
|
||||
|
||||
#[EmbeddedSettings]
|
||||
public ?DigikeySettings $digikey = null;
|
||||
|
||||
|
|
@ -63,7 +66,10 @@ class InfoProviderSettings
|
|||
|
||||
#[EmbeddedSettings]
|
||||
public ?PollinSettings $pollin = null;
|
||||
|
||||
|
||||
#[EmbeddedSettings]
|
||||
public ?BuerklinSettings $buerklin = null;
|
||||
|
||||
#[EmbeddedSettings]
|
||||
public ?ConradSettings $conrad = null;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue