Files
kupshop/bundles/External/ZNZBundle/Synchronizers/PriceSynchronizer.php
2025-08-02 16:30:27 +02:00

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];
}
}