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

@ -107,7 +107,14 @@ export default class extends Controller {
}
if (data.short) {
return '<div><b>' + escape(data.short) + '</b></div>';
let short = escape(data.short)
//Make text italic, if the item is not yet in the DB
if (data.not_in_db_yet) {
short = '<i>' + short + '</i>';
}
return '<div><b>' + short + '</b></div>';
}
let name = "";

View file

@ -22,6 +22,7 @@ declare(strict_types=1);
namespace App\Entity\PriceInformations;
use App\Repository\CurrencyRepository;
use Doctrine\DBAL\Types\Types;
use App\Entity\Attachments\CurrencyAttachment;
use App\Entity\Base\AbstractStructuralDBElement;
@ -42,7 +43,7 @@ use Symfony\Component\Validator\Constraints as Assert;
* @extends AbstractStructuralDBElement<CurrencyAttachment, CurrencyParameter>
*/
#[UniqueEntity('iso_code')]
#[ORM\Entity]
#[ORM\Entity(repositoryClass: CurrencyRepository::class)]
#[ORM\Table(name: 'currencies')]
#[ORM\Index(name: 'currency_idx_name', columns: ['name'])]
#[ORM\Index(name: 'currency_idx_parent_name', columns: ['parent_id', 'name'])]

View file

@ -102,6 +102,9 @@ class StructuralEntityChoiceHelper
$symbol = empty($choice->getIsoCode()) ? null : Currencies::getSymbol($choice->getIsoCode());
$tmp['data-short'] = $options['short'] ? $symbol : $choice->getName();
//Show entities that are not added to DB yet separately from other entities
$tmp['data-not_in_db_yet'] = $choice->getID() === null;
return $tmp + [
'data-symbol' => $symbol,
];

View file

@ -23,6 +23,7 @@ declare(strict_types=1);
namespace App\Form\Type\Helper;
use App\Entity\Base\AbstractStructuralDBElement;
use App\Entity\PriceInformations\Currency;
use App\Repository\StructuralDBElementRepository;
use App\Services\Trees\NodesListBuilder;
use Doctrine\ORM\EntityManagerInterface;
@ -59,6 +60,12 @@ class StructuralEntityChoiceLoader extends AbstractChoiceLoader
public function createNewEntitiesFromValue(string $value): array
{
if (!$this->options['allow_add']) {
//Always allow the starting element to be added
if ($this->starting_element !== null && $this->starting_element->getID() === null) {
$this->entityManager->persist($this->starting_element);
return [$this->starting_element];
}
throw new \RuntimeException('Cannot create new entity, because allow_add is not enabled!');
}

View file

@ -0,0 +1,59 @@
<?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\Repository;
use App\Entity\Base\AbstractStructuralDBElement;
use App\Entity\PriceInformations\Currency;
use Symfony\Component\Intl\Currencies;
/**
* @extends StructuralDBElementRepository<Currency>
*/
class CurrencyRepository extends StructuralDBElementRepository
{
/**
* Finds or create a currency with the given ISO code.
* @param string $iso_code
* @return Currency
*/
public function findOrCreateByISOCode(string $iso_code): Currency
{
//Normalize ISO code
$iso_code = strtoupper($iso_code);
//Try to find currency
$currency = $this->findOneBy(['iso_code' => $iso_code]);
if ($currency !== null) {
return $currency;
}
//Create currency if it does not exist
$name = Currencies::getName($iso_code);
$currency = $this->findOrCreateForInfoProvider($name);
$currency->setIsoCode($iso_code);
return $currency;
}
}

View file

@ -192,6 +192,7 @@ class StructuralDBElementRepository extends NamedDBElementRepository
* Also, it will try to find the element using the additional names field, of the elements.
* @param string $name
* @return AbstractStructuralDBElement|null
* @phpstan-return TEntityClass|null
*/
public function findForInfoProvider(string $name): ?AbstractStructuralDBElement
{
@ -228,6 +229,7 @@ class StructuralDBElementRepository extends NamedDBElementRepository
* Similar to findForInfoProvider, but will create a new element with the given name if none was found.
* @param string $name
* @return AbstractStructuralDBElement
* @phpstan-return TEntityClass
*/
public function findOrCreateForInfoProvider(string $name): AbstractStructuralDBElement
{

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