456 lines
16 KiB
PHP
456 lines
16 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace External\ZNZBundle\Synchronizers;
|
|
|
|
use External\ZNZBundle\Exception\ZNZException;
|
|
use KupShop\KupShopBundle\Context\CurrencyContext;
|
|
use KupShop\KupShopBundle\Util\Contexts;
|
|
use KupShop\KupShopBundle\Util\Functional\Mapping;
|
|
use KupShop\KupShopBundle\Util\StringUtil;
|
|
use KupShop\PricelistBundle\Util\PriceListWorker;
|
|
use KupShop\QuantityDiscountBundle\Util\QuantityDiscountUtil;
|
|
use KupShop\SynchronizationBundle\Exception\RabbitRetryMessageException;
|
|
use Query\Operator;
|
|
|
|
class PriceSynchronizer extends BaseSynchronizer
|
|
{
|
|
/** Jedna se o vychozi cenu, ktera se neuklada do ceniku, ale jde rovnou k produktu/variante */
|
|
private const TYPE_DEFAULT = 'default';
|
|
/** Jedna se o vychozi cenu, ktera se uklada rovnou do ceniku (pouze pokud je zapnuto `pricelist_per_website`) */
|
|
private const TYPE_DEFAULT_PRICE_LIST = 'default_price_list';
|
|
/** Jedna se o cenu v ceniku */
|
|
private const TYPE_PRICE_LIST = 'price_list';
|
|
|
|
protected static string $type = 'price';
|
|
|
|
/** @required */
|
|
public PriceListWorker $priceListWorker;
|
|
|
|
/** @required */
|
|
public QuantityDiscountUtil $quantityDiscountUtil;
|
|
|
|
public static function getHandledTables(): array
|
|
{
|
|
return [
|
|
'ProduktCena' => 'processPrice',
|
|
'ProduktCenaTier' => 'processQuantityDiscount',
|
|
];
|
|
}
|
|
|
|
public function processPrice(array $item): void
|
|
{
|
|
if ($item['meta']['delete'] ?? false) {
|
|
if (!($item = $this->getProductPriceDeleteItem($item))) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (!$this->isMessageWithStoreRequiredValid($item)) {
|
|
return;
|
|
}
|
|
|
|
// CenovaUroven = ID ceniku v Heliosu, v podstate kazda cena by mela spadat pod nejaky cenik
|
|
if (empty($item['CenovaUroven'])) {
|
|
return;
|
|
}
|
|
|
|
if (!($priceListId = $this->znzUtil->getPriceListByZNZId($item['CenovaUroven']))) {
|
|
return;
|
|
}
|
|
|
|
[$productId, $variationId] = $this->znzUtil->getProductMapping($item['IdProdukt']);
|
|
if (!$productId) {
|
|
throw new RabbitRetryMessageException('Product not found');
|
|
}
|
|
|
|
$variationId = $this->znzUtil->getProductVariationIdWithStoreCheck($productId, $variationId, $item['IdSklad'] ?? null);
|
|
|
|
$priceListType = $this->getPriceListType($item, $priceListId);
|
|
$isPriceListWithDefaultPrice = $this->isPriceListWithDefaultPrice($item, $priceListType, $priceListId);
|
|
|
|
$maxId = $this->znzUtil->getZNZMaxId($item['IdProdukt'], 'ProduktCena', (string) $item['CenovaUroven']);
|
|
// je to stara zmena, ktera nas uz nezajima, protoze mame novejsi data
|
|
if ($maxId && $maxId > $item['meta']['id_change']) {
|
|
return;
|
|
}
|
|
|
|
$price = $this->createProductPrice($productId, $item);
|
|
|
|
// aktualizovat vychozi cenu u produktu
|
|
if ($isPriceListWithDefaultPrice) {
|
|
$this->updateItemDefaultPrice($productId, $variationId, $price);
|
|
}
|
|
|
|
$this->updatePriceList($priceListId, $productId, $variationId, $price);
|
|
$this->updatePriceForDiscount($priceListId, $productId, $variationId, $price, $isPriceListWithDefaultPrice);
|
|
|
|
$this->znzUtil->updateZNZMaxId($item['IdProdukt'], 'ProduktCena', (string) $item['CenovaUroven'], $item['meta']['id_change']);
|
|
}
|
|
|
|
public function processQuantityDiscount(array $item): void
|
|
{
|
|
if (!$this->isMessageWithStoreRequiredValid($item)) {
|
|
return;
|
|
}
|
|
|
|
[$productId, $variationId] = $this->znzUtil->getProductMapping($item['IDProdukt']);
|
|
if (!$productId) {
|
|
return;
|
|
}
|
|
|
|
$variationId = $this->znzUtil->getProductVariationIdWithStoreCheck($productId, $variationId, $item['IdSklad'] ?? null);
|
|
|
|
$discount = $this->createProductPrice($productId, $item, 'Sleva4');
|
|
|
|
$groups = array_flip($this->quantityDiscountUtil->getGroups());
|
|
|
|
// v pripade B2B shopu je mnozstevni sleva per cenik
|
|
if ($this->configuration->isB2BShop()) {
|
|
if (!($priceListId = $this->znzUtil->getPriceListByZNZId($item['CenovaUroven']))) {
|
|
return;
|
|
}
|
|
|
|
// id skupiny mnozsteni slevy je stejne jako ID ceniku
|
|
$groupId = $priceListId;
|
|
} else {
|
|
$groupId = null;
|
|
// ulozim si mnozstevni slevu pod spravnou skupinu (skupiny jsou vytvorene podle websites)
|
|
if ($groups[$item['IdWebsite']] ?? false) {
|
|
$groupId = $groups[$item['IdWebsite']];
|
|
}
|
|
}
|
|
|
|
$currencyContext = Contexts::get(CurrencyContext::class);
|
|
|
|
$currency = $item['Mena4'] ?? $currencyContext->getDefaultId();
|
|
|
|
if (!($currencyContext->getAll()[$currency] ?? false)) {
|
|
throw new ZNZException('Currency "'.$currency.'" not found!');
|
|
}
|
|
|
|
$this->updateQuantityDiscount(
|
|
$productId,
|
|
$variationId,
|
|
(int) $item['MnozstviOd'],
|
|
$discount,
|
|
$groupId,
|
|
$currency
|
|
);
|
|
}
|
|
|
|
protected function getPriceListType(array $item, int $priceListId): string
|
|
{
|
|
// jedna se o cenik s priznakem Vychozi=true
|
|
if ($this->isPriceListDefault($priceListId)) {
|
|
// pokud je zapnuto vychozi cenik pro kazdou website, tak to vzdycky musi byt cenik
|
|
if ($this->configuration->isDefaultPriceListPerWebsite()) {
|
|
return self::TYPE_DEFAULT_PRICE_LIST;
|
|
}
|
|
|
|
// pokud je to cenik ve vychozi mene + to je vychozi cenik, tak vracim TYPE_DEFAULT
|
|
// to znamena, ze se cena neulozi do ceniku, ale ulozi se primo k produktu/variante
|
|
if ($item['Mena'] === Contexts::get(CurrencyContext::class)->getDefaultId()) {
|
|
return self::TYPE_DEFAULT;
|
|
}
|
|
}
|
|
|
|
return self::TYPE_PRICE_LIST;
|
|
}
|
|
|
|
protected function isPriceListDefault(int $priceListId): bool
|
|
{
|
|
static $cache = [];
|
|
|
|
if (($cache[$priceListId] ?? null) === null) {
|
|
$cache[$priceListId] = (bool) sqlQueryBuilder()
|
|
->select('is_default')
|
|
->from('znz_pricelists')
|
|
->where(Operator::equals(['id_pricelist' => $priceListId]))
|
|
->execute()->fetchOne();
|
|
}
|
|
|
|
return $cache[$priceListId];
|
|
}
|
|
|
|
private function updatePriceForDiscount(int $priceListId, int $productId, ?int $variationId, \Decimal $price, bool $isPriceListWithDefaultPrice): void
|
|
{
|
|
if (!findModule(\Modules::PRICE_HISTORY)) {
|
|
return;
|
|
}
|
|
|
|
// aktualizovat CPS v ceniku
|
|
sqlQueryBuilder()
|
|
->update('pricelists_products')
|
|
->set('price_for_discount', 'LEAST(:price, price_for_discount)')
|
|
->setParameter('price', $price)
|
|
->where(Operator::equalsNullable(['id_pricelist' => $priceListId, 'id_product' => $productId, 'id_variation' => $variationId]))
|
|
->execute();
|
|
|
|
// aktualizovat CPS u produktu/varianty - pokud se jedna o cenik s vychozi cenou
|
|
if ($isPriceListWithDefaultPrice) {
|
|
sqlQueryBuilder()
|
|
->update($variationId ? 'products_variations' : 'products')
|
|
->set('price_for_discount', 'LEAST(:price, price_for_discount)')
|
|
->setParameter('price', $price)
|
|
->where(Operator::equals(['id' => $variationId ?: $productId]))
|
|
->execute();
|
|
}
|
|
}
|
|
|
|
private function updateItemDefaultPrice(int $productId, ?int $variationId, \Decimal $price): void
|
|
{
|
|
// aktualizovat vychozi cenu
|
|
sqlQueryBuilder()
|
|
->update($variationId ? 'products_variations' : 'products')
|
|
->directValues(['price' => $price])
|
|
->where(Operator::equals(['id' => $variationId ?: $productId]))
|
|
->execute();
|
|
}
|
|
|
|
private function getProductPriceDeleteItem(array $item): ?array
|
|
{
|
|
$parts = explode('-', $item['meta']['unique_id'] ?? '');
|
|
if (empty($parts[0])) {
|
|
return null;
|
|
}
|
|
|
|
$znzPriceListId = !empty($parts[2]) ? (int) $parts[2] : null;
|
|
|
|
$priceList = $znzPriceListId ? $this->getPriceListByZNZId($znzPriceListId) : null;
|
|
|
|
return [
|
|
'IdProdukt' => (int) $parts[0],
|
|
'CenovaUroven' => $znzPriceListId,
|
|
'Mena' => $priceList['currency'] ?? $this->configuration->getWebsiteCurrency($item['meta']['website']),
|
|
'IdWebsite' => $item['meta']['website'],
|
|
'Cena' => 0,
|
|
'meta' => $item['meta'],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Urcuje, zda se jedna o cenik, ktery zaroven obsahuje vychozi cenu, ktera bude ulozena primo k produktu, aby byla
|
|
* videt v administraci.
|
|
*/
|
|
private function isPriceListWithDefaultPrice(array $item, string $priceListType, int $priceListId): bool
|
|
{
|
|
if ($priceListType === self::TYPE_DEFAULT) {
|
|
return true;
|
|
}
|
|
|
|
if ($priceListType !== self::TYPE_DEFAULT_PRICE_LIST) {
|
|
return false;
|
|
}
|
|
|
|
// pokud neni cenik ve vychozi mene, tak nemuze byt bran jako cenik s vychozi cenou
|
|
if (Contexts::get(CurrencyContext::class)->getDefaultId() !== $this->getPriceListCurrency($priceListId)) {
|
|
return false;
|
|
}
|
|
|
|
return $this->configuration->getMainWebsite() === $item['IdWebsite'];
|
|
}
|
|
|
|
private function getDefaultPriceListId(string $priceListName, string $currency, array $data = []): int
|
|
{
|
|
static $defaultPriceListsByName;
|
|
|
|
if ($defaultPriceListsByName === null) {
|
|
$priceListsAll = sqlQueryBuilder()
|
|
->select('id, name')
|
|
->from('pricelists')
|
|
->execute()->fetchAllAssociative();
|
|
|
|
$priceListsByName = Mapping::mapKeys(
|
|
$priceListsAll,
|
|
fn ($k, $v) => [StringUtil::slugify($v['name']), (int) $v['id']]
|
|
);
|
|
}
|
|
|
|
// pokud cenik se stejnym nazvem uz existuje, tak vratim jeho ID
|
|
if ($defaultPriceListsByName[StringUtil::slugify($priceListName)] ?? false) {
|
|
return $defaultPriceListsByName[StringUtil::slugify($priceListName)];
|
|
}
|
|
|
|
if (!($defaultPriceListsByName[StringUtil::slugify($priceListName)] ?? false)) {
|
|
$priceListId = sqlGetConnection()->transactional(function () use ($priceListName, $currency, $data) {
|
|
$priceListId = $this->priceListWorker->findPriceList($priceListName, $currency);
|
|
|
|
sqlQueryBuilder()
|
|
->update('pricelists')
|
|
->directValues(['data' => json_encode($data)])
|
|
->where(Operator::equals(['id' => $priceListId]))
|
|
->execute();
|
|
|
|
return $priceListId;
|
|
});
|
|
|
|
$defaultPriceListsByName[StringUtil::slugify($priceListName)] = $priceListId;
|
|
}
|
|
|
|
return $defaultPriceListsByName[StringUtil::slugify($priceListName)];
|
|
}
|
|
|
|
private function getPriceListId(int $znzId, string $priceListName, string $currency, array $data = []): int
|
|
{
|
|
static $priceLists, $priceListsByName;
|
|
|
|
if ($priceLists === null) {
|
|
$priceListsAll = sqlQueryBuilder()
|
|
->select('id, name')
|
|
->from('pricelists')
|
|
->execute()->fetchAllAssociative();
|
|
|
|
$priceLists = Mapping::mapKeys(
|
|
$priceListsAll,
|
|
fn ($k, $v) => [$v['id'], (int) $v['id']]
|
|
);
|
|
|
|
$priceListsByName = Mapping::mapKeys(
|
|
$priceListsAll,
|
|
fn ($k, $v) => [StringUtil::slugify($v['name']), (int) $v['id']]
|
|
);
|
|
}
|
|
|
|
if ($priceListsByName[StringUtil::slugify($priceListName)] ?? false) {
|
|
return $priceListsByName[StringUtil::slugify($priceListName)];
|
|
}
|
|
|
|
if (!($priceLists[$znzId] ?? false)) {
|
|
$priceListId = sqlGetConnection()->transactional(function () use ($znzId, $priceListName, $currency, $data) {
|
|
$tmpId = $this->priceListWorker->findPriceList($priceListName, $currency);
|
|
|
|
sqlQueryBuilder()
|
|
->update('pricelists')
|
|
->directValues(['data' => json_encode($data)])
|
|
->where(Operator::equals(['id' => $tmpId]))
|
|
->execute();
|
|
|
|
if (!empty($znzId) && $tmpId != $znzId) {
|
|
sqlQueryBuilder()
|
|
->update('pricelists')
|
|
->directValues(['id' => $znzId])
|
|
->where(Operator::equals(['id' => $tmpId]))
|
|
->execute();
|
|
}
|
|
|
|
return $znzId;
|
|
});
|
|
|
|
$priceLists[$znzId] = $priceListId;
|
|
}
|
|
|
|
return $priceLists[$znzId];
|
|
}
|
|
|
|
private function getPriceListByZNZId(int $znzId): ?array
|
|
{
|
|
static $priceListsById;
|
|
|
|
if ($priceListsById === null) {
|
|
$priceListsAll = sqlQueryBuilder()
|
|
->select('pl.id, pl.name, pl.currency, zpl.id_znz')
|
|
->from('pricelists', 'pl')
|
|
->join('pl', 'znz_pricelists', 'zpl', 'zpl.id_pricelist = pl.id')
|
|
->execute()->fetchAllAssociative();
|
|
|
|
$priceLists = Mapping::mapKeys(
|
|
$priceListsAll,
|
|
fn ($k, $v) => [$v['id_znz'], $v]
|
|
);
|
|
}
|
|
|
|
return $priceLists[$znzId] ?? null;
|
|
}
|
|
|
|
private function updatePriceList(int $priceListId, int $productId, ?int $variationId, \Decimal $price): void
|
|
{
|
|
sqlQueryBuilder()
|
|
->insert('pricelists_products')
|
|
->directValues([
|
|
'id_pricelist' => $priceListId,
|
|
'id_product' => $productId,
|
|
'id_variation' => $variationId,
|
|
'price' => $price,
|
|
])
|
|
->onDuplicateKeyUpdate(['price'])
|
|
->execute();
|
|
}
|
|
|
|
private function updateQuantityDiscount(int $productId, ?int $variationId, int $pieces, \Decimal $discount, ?int $groupId, string $currency): bool
|
|
{
|
|
$id = sqlQueryBuilder()
|
|
->select('id')
|
|
->from('products_quantity_discounts')
|
|
->where(
|
|
Operator::equalsNullable(
|
|
[
|
|
'id_group' => $groupId,
|
|
'id_product' => $productId,
|
|
'id_variation' => $variationId,
|
|
'pieces' => $pieces,
|
|
]
|
|
)
|
|
)->execute()->fetchOne();
|
|
|
|
if ($id) {
|
|
sqlQueryBuilder()
|
|
->update('products_quantity_discounts')
|
|
->directValues(
|
|
[
|
|
'discount' => $discount,
|
|
'discount_type' => $currency,
|
|
]
|
|
)
|
|
->where(Operator::equals(['id' => $id]))
|
|
->execute();
|
|
} else {
|
|
sqlQueryBuilder()
|
|
->insert('products_quantity_discounts')
|
|
->directValues(
|
|
[
|
|
'id_group' => $groupId,
|
|
'id_product' => $productId,
|
|
'id_variation' => $variationId,
|
|
'pieces' => $pieces,
|
|
'discount' => $discount,
|
|
'discount_type' => $currency,
|
|
]
|
|
)->execute();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private function createProductPrice(int $productId, array $item, string $priceField = 'Cena'): \Decimal
|
|
{
|
|
$price = toDecimal($item[$priceField]);
|
|
|
|
// pokud je uvedena cena s DPH, tak DPH odeberu aby se to u nas ulozilo spravne
|
|
if (($item['BezDPH'] ?? 'N') === 'N') {
|
|
$price = $price->removeVat(
|
|
$this->znzUtil->getProductVat($productId)
|
|
);
|
|
}
|
|
|
|
return $price;
|
|
}
|
|
|
|
private function getPriceListCurrency(int $priceListId): string
|
|
{
|
|
static $priceListCurrencyCache = [];
|
|
|
|
if (!($priceListCurrencyCache[$priceListId] ?? false)) {
|
|
$priceListCurrencyCache[$priceListId] = sqlQueryBuilder()
|
|
->select('currency')
|
|
->from('pricelists')
|
|
->where(Operator::equals(['id' => $priceListId]))
|
|
->sendToMaster()
|
|
->execute()->fetchOne();
|
|
}
|
|
|
|
return $priceListCurrencyCache[$priceListId];
|
|
}
|
|
}
|