Allow to retrieve price and shopping informations from info providers

This commit is contained in:
Jan Böhmer 2023-07-14 00:09:22 +02:00
parent c4439cc9db
commit 0cb46039dd
11 changed files with 348 additions and 4 deletions

View file

@ -44,6 +44,8 @@ class PartDetailDTO extends SearchResultDTO
public readonly ?array $datasheets = null,
/** @var ParameterDTO[]|null */
public readonly ?array $parameters = null,
/** @var PurchaseInfoDTO[]|null */
public readonly ?array $vendor_infos = null,
) {
parent::__construct(
provider_key: $provider_key,

View file

@ -0,0 +1,53 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2023 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\DTOs;
use Brick\Math\BigDecimal;
/**
* This DTO represents a price for a single unit in a certain discount range
*/
class PriceDTO
{
private readonly BigDecimal $price_as_big_decimal;
public function __construct(
/** @var float The minimum amount that needs to get ordered for this price to be valid */
public readonly float $minimum_discount_amount,
/** @var string The price as string (with .) */
public readonly string $price,
/** @var string The currency of the used ISO code of this price detail */
public readonly ?string $currency_iso_code,
/** @var bool If the price includes tax */
public readonly ?bool $includes_tax = true,
)
{
$this->price_as_big_decimal = BigDecimal::of($this->price);
}
public function getPriceAsBigDecimal(): BigDecimal
{
return $this->price_as_big_decimal;
}
}

View file

@ -0,0 +1,44 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2023 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\DTOs;
class PurchaseInfoDTO
{
public function __construct(
public readonly string $distributor_name,
public readonly string $order_number,
/** @var PriceDTO[] */
public readonly array $prices,
/** @var string|null An url to the product page of the vendor */
public readonly ?string $product_url = null,
)
{
//Ensure that the prices are PriceDTO instances
foreach ($this->prices as $price) {
if (!$price instanceof PriceDTO) {
throw new \InvalidArgumentException('The prices array must only contain PriceDTO instances');
}
}
}
}

View file

@ -31,9 +31,16 @@ use App\Entity\Parameters\PartParameter;
use App\Entity\Parts\Manufacturer;
use App\Entity\Parts\ManufacturingStatus;
use App\Entity\Parts\Part;
use App\Entity\Parts\Supplier;
use App\Entity\PriceInformations\Currency;
use App\Entity\PriceInformations\Orderdetail;
use App\Entity\PriceInformations\Pricedetail;
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 Brick\Math\BigDecimal;
use Doctrine\ORM\EntityManagerInterface;
/**
@ -42,7 +49,7 @@ use Doctrine\ORM\EntityManagerInterface;
class DTOtoEntityConverter
{
public function __construct(private readonly EntityManagerInterface $em)
public function __construct(private readonly EntityManagerInterface $em, private readonly string $base_currency)
{
}
@ -60,6 +67,35 @@ class DTOtoEntityConverter
return $entity;
}
public function convertPrice(PriceDTO $dto, Pricedetail $entity = new Pricedetail()): Pricedetail
{
$entity->setMinDiscountQuantity($dto->minimum_discount_amount);
$entity->setPrice($dto->getPriceAsBigDecimal());
//Currency TODO
if ($dto->currency_iso_code !== null) {
$entity->setCurrency($this->getCurrency($dto->currency_iso_code));
} else {
$entity->setCurrency(null);
}
return $entity;
}
public function convertPurchaseInfo(PurchaseInfoDTO $dto, Orderdetail $entity = new Orderdetail()): Orderdetail
{
$entity->setSupplierpartnr($dto->order_number);
$entity->setSupplierProductUrl($dto->product_url ?? '');
$entity->setSupplier($this->getOrCreateEntityNonNull(Supplier::class, $dto->distributor_name));
foreach ($dto->prices as $price) {
$entity->addPricedetail($this->convertPrice($price));
}
return $entity;
}
public function convertFile(FileDTO $dto, PartAttachment $entity = new PartAttachment()): PartAttachment
{
$entity->setURL($dto->url);
@ -101,9 +137,22 @@ class DTOtoEntityConverter
$entity->addAttachment($this->convertFile($datasheet));
}
//Add orderdetails and prices
foreach ($dto->vendor_infos ?? [] as $vendor_info) {
$entity->addOrderdetail($this->convertPurchaseInfo($vendor_info));
}
return $entity;
}
/**
* @template T of AbstractStructuralDBElement
* @param string $class
* @phpstan-param class-string<T> $class
* @param string|null $name
* @return AbstractStructuralDBElement|null
* @phpstan-return T|null
*/
private function getOrCreateEntity(string $class, ?string $name): ?AbstractStructuralDBElement
{
//Fall through to make converting easier
@ -111,7 +160,30 @@ class DTOtoEntityConverter
return null;
}
return $this->getOrCreateEntityNonNull($class, $name);
}
/**
* @template T of AbstractStructuralDBElement
* @param string $class The class of the entity to create
* @phpstan-param class-string<T> $class
* @param string $name The name of the entity to create
* @return AbstractStructuralDBElement
* @phpstan-return T|null
*/
private function getOrCreateEntityNonNull(string $class, string $name): AbstractStructuralDBElement
{
return $this->em->getRepository($class)->findOrCreateForInfoProvider($name);
}
private function getCurrency(string $iso_code): ?Currency
{
//Check if the currency is the base currency (then we can just return null)
if ($iso_code === $this->base_currency) {
return null;
}
return $this->em->getRepository(Currency::class)->findOrCreateByISOCode($iso_code);
}
}

View file

@ -28,7 +28,9 @@ use App\Form\InfoProviderSystem\ProviderSelectType;
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\SearchResultDTO;
use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO;
use Symfony\Contracts\HttpClient\HttpClientInterface;
class Element14Provider implements InfoProviderInterface
@ -39,6 +41,8 @@ class Element14Provider implements InfoProviderInterface
private const API_VERSION_NUMBER = '1.2';
private const NUMBER_OF_RESULTS = 20;
public const DISTRIBUTOR_NAME = 'Farnell';
private const COMPLIANCE_ATTRIBUTES = ['euEccn', 'hazardous', 'MSL', 'productTraceability', 'rohsCompliant',
'rohsPhthalatesCompliant', 'SVHC', 'tariffCode', 'usEccn', 'hazardCode'];
@ -105,15 +109,21 @@ class Element14Provider implements InfoProviderInterface
mpn: $product['translatedManufacturerPartNumber'],
preview_image_url: $this->toImageUrl($product['image'] ?? null),
manufacturing_status: $this->releaseStatusCodeToManufacturingStatus($product['releaseStatusCode'] ?? null),
provider_url: 'https://' . self::FARNELL_STORE_ID . '/' . $product['sku'],
provider_url: $this->generateProductURL($product['sku']),
datasheets: $this->parseDataSheets($product['datasheets'] ?? null),
parameters: $this->attributesToParameters($product['attributes'] ?? null),
vendor_infos: $this->pricesToVendorInfo($product['sku'], $product['prices'] ?? [])
);
}
return $result;
}
private function generateProductURL($sku): string
{
return 'https://' . self::FARNELL_STORE_ID . '/' . $sku;
}
/**
* @param mixed[]|null $datasheets
* @return FileDTO[]|null Array of FileDTOs
@ -147,6 +157,90 @@ class Element14Provider implements InfoProviderInterface
return 'https://' . self::FARNELL_STORE_ID . '/productimages/standard/' . $locale . $image['baseName'];
}
/**
* Converts the price array to a VendorInfoDTO array to be used in the PartDetailDTO
* @param string $sku
* @param array $prices
* @return array
*/
private function pricesToVendorInfo(string $sku, array $prices): array
{
$price_dtos = [];
foreach ($prices as $price) {
$price_dtos[] = new PriceDTO(
minimum_discount_amount: $price['from'],
price: (string) $price['cost'],
currency_iso_code: $this->getUsedCurrency(),
includes_tax: false,
);
}
return [
new PurchaseInfoDTO(
distributor_name: self::DISTRIBUTOR_NAME,
order_number: $sku,
prices: $price_dtos,
product_url: $this->generateProductURL($sku)
)
];
}
public function getUsedCurrency(): string
{
//Decide based on the shop ID
return match (self::FARNELL_STORE_ID) {
'bg.farnell.com' => 'EUR',
'cz.farnell.com' => 'CZK',
'dk.farnell.com' => 'DKK',
'at.farnell.com' => 'EUR',
'ch.farnell.com' => 'CHF',
'de.farnell.com' => 'EUR',
'cpc.farnell.com' => 'GBP',
'cpcireland.farnell.com' => 'EUR',
'export.farnell.com' => 'GBP',
'onecall.farnell.com' => 'GBP',
'ie.farnell.com' => 'EUR',
'il.farnell.com' => 'USD',
'uk.farnell.com' => 'GBP',
'es.farnell.com' => 'EUR',
'ee.farnell.com' => 'EUR',
'fi.farnell.com' => 'EUR',
'fr.farnell.com' => 'EUR',
'hu.farnell.com' => 'HUF',
'it.farnell.com' => 'EUR',
'lt.farnell.com' => 'EUR',
'lv.farnell.com' => 'EUR',
'be.farnell.com' => 'EUR',
'nl.farnell.com' => 'EUR',
'no.farnell.com' => 'NOK',
'pl.farnell.com' => 'PLN',
'pt.farnell.com' => 'EUR',
'ro.farnell.com' => 'EUR',
'ru.farnell.com' => 'RUB',
'sk.farnell.com' => 'EUR',
'si.farnell.com' => 'EUR',
'se.farnell.com' => 'SEK',
'tr.farnell.com' => 'TRY',
'canada.newark.com' => 'CAD',
'mexico.newark.com' => 'MXN',
'www.newark.com' => 'USD',
'cn.element14.com' => 'CNY',
'au.element14.com' => 'AUD',
'nz.element14.com' => 'NZD',
'hk.element14.com' => 'HKD',
'sg.element14.com' => 'SGD',
'my.element14.com' => 'MYR',
'ph.element14.com' => 'PHP',
'th.element14.com' => 'THB',
'in.element14.com' => 'INR',
'tw.element14.com' => 'TWD',
'kr.element14.com' => 'KRW',
'vn.element14.com' => 'VND',
default => throw new \RuntimeException('Unknown store ID: ' . self::FARNELL_STORE_ID)
};
}
/**
* @param array|null $attributes
* @return ParameterDTO[]|null