Files
kupshop/bundles/KupShop/PreordersBundle/Service/DynamicContexts.php
2025-08-02 16:30:27 +02:00

261 lines
8.4 KiB
PHP

<?php
declare(strict_types=1);
namespace KupShop\PreordersBundle\Service;
use KupShop\I18nBundle\Entity\Currency;
use KupShop\I18nBundle\Util\PriceConverter;
use KupShop\KupShopBundle\Context\ContextManager;
use KupShop\KupShopBundle\Context\CurrencyContext;
use KupShop\KupShopBundle\Context\EmptiableContext;
use KupShop\KupShopBundle\Context\LanguageContext;
use KupShop\KupShopBundle\Context\PriceLevelContext;
use KupShop\KupShopBundle\Context\UserContext;
use KupShop\KupShopBundle\Exception\InvalidArgumentException;
use KupShop\KupShopBundle\Util\Contexts;
use KupShop\KupShopBundle\Util\Price\TotalPrice;
use KupShop\OrderingBundle\Util\Purchase\PurchaseUtil;
use KupShop\PreordersBundle\Entity\Preorder;
use KupShop\PreordersBundle\Entity\UserPreorder;
use KupShop\PricelistBundle\Context\PricelistContext;
use Query\Operator;
class DynamicContexts
{
/**
* @var array<int, string> Mapping of pricelist IDs to currency (cache in case of multiple calls to activatePreorder)
*/
private array $pricelistCurrenciesCache = [];
public function __construct(
private readonly ContextManager $contextManager,
private readonly PurchaseUtil $purchaseUtil,
private readonly PreorderUtil $listUtil,
private readonly PricelistContext $pricelistContext,
private readonly PriceConverter $priceConverter,
) {
}
/**
* @template T of mixed
*
* @param callable(): T $callback
*
* @return T
*
* @throws InvalidArgumentException
*/
public function withDynamicPrices(Preorder $preorder, array $items, callable $callback)
{
$contexts = array_merge(
$this->getBaseContexts($preorder),
$this->getDynamicContexts($preorder, $items),
);
return $this->contextManager->activateContexts($contexts, $callback);
}
/**
* @template T of mixed
*
* @param callable(): T $callback
*
* @return T
*
* @throws InvalidArgumentException
*/
public function withBasePrices(Preorder $preorder, callable $callback)
{
$contexts = $this->getBaseContexts($preorder);
return $this->contextManager->activateContexts($contexts, $callback);
}
protected function getBaseContexts(Preorder $preorder, ?\User $user = null): array
{
$contexts = [];
if ($user) {
$contexts[UserContext::class] = $user;
}
if (PreorderUtil::fieldIsComposedOfDigits($preorder, 'id_pricelist')) {
$contexts[PricelistContext::class] = (int) $preorder['id_pricelist'];
} else {
$contexts[PricelistContext::class] = ContextManager::FORCE_EMPTY;
}
if (PreorderUtil::fieldIsComposedOfDigits($preorder, 'id_price_level')) {
$contexts[PriceLevelContext::class] = (int) $preorder['id_price_level'];
} else {
$contexts[PriceLevelContext::class] = ContextManager::FORCE_EMPTY;
}
return $contexts;
}
/**
* @throws InvalidArgumentException
*/
private function getDynamicContexts(Preorder $preorder, array $items = []): array
{
$contexts = [];
// Pokud nemáme nastaveny dynamické ceny, tak není potřeba již volat výpočet total price
if (!$preorder->hasDynamicPrice()) {
return $contexts;
}
/* @var TotalPrice $total */
$this->withBasePrices($preorder, function () use ($items, &$total) {
$productList = $this->listUtil->getPreorderProductList($items);
$purchaseState = PreorderUtil::toPurchaseState($productList->getProducts());
$total = $this->purchaseUtil->recalculateTotalPrices($purchaseState)
->getTotalPrice();
});
// convert currency of total price to default preorder's price-list's currency
if ($pricelistId = $preorder['id_pricelist'] ?? false) {
/** @var Currency $currency */
$currency = $this->contextManager->activateContexts(
[PricelistContext::class => $pricelistId],
fn () => $this->pricelistContext->getActive()->getCurrency(),
);
if ($total->getCurrency()->getId() !== $currency->getId()) {
$total = $this->priceConverter->convertPrice(
$total->getCurrency(),
$currency,
$total->getPriceWithoutVat(),
);
}
}
if (method_exists($total, 'getPriceWithoutVat')) {
$total = $total->getPriceWithoutVat();
}
if ($idPriceLevel = $preorder->getPriceLevelID($total)) {
$contexts[PriceLevelContext::class] = $idPriceLevel;
}
if ($idPricelist = $preorder->getPricelistID($total)) {
$contexts[PricelistContext::class] = $idPricelist;
}
return $contexts;
}
/**
* @template T of mixed
*
* @param (callable(): T)|null $callback
*
* @return T
*
* @throws InvalidArgumentException
*/
public function activateUserPreorder(UserPreorder $userPreorder, ?callable $callback = null)
{
$contexts = array_merge(
$this->getBaseContexts($userPreorder->getPreorder()),
$this->getDynamicContexts($userPreorder->getPreorder(), $userPreorder->getItems()),
);
$customData = $userPreorder->getCustomData();
if ($lang = $customData->get('language') ?? false) {
$contexts[LanguageContext::class] = $lang;
}
if ($currency = $customData->get('currency') ?? false) {
$contexts[CurrencyContext::class] = $currency;
}
if ($callback) {
return $this->contextManager->activateContexts($contexts, $callback);
}
foreach ($contexts as $class => $id) {
Contexts::get($class)?->activate($id === null ? null : (string) $id);
}
return null;
}
/**
* @template T of mixed
*
* @param (callable(): T)|null $callback
*
* @return T
*
* @throws InvalidArgumentException
*/
public function activatePreorder(Preorder $preorder, ?\User $user, ?callable $callback = null)
{
$contexts = $this->getBaseContexts($preorder, $user);
if ($user) {
$contexts = array_merge(
$contexts,
$this->getDynamicContexts($preorder, $preorder->getItemsByUser($user)),
);
}
// Activate the appropriate currency in case of the preorder having a different currency than the site.
// E.g. user is on the czech site with CZK, but the preorder pricelist is EUR -> prices displayed in CZK,
// but total price is calculated based on the pricelist = EUR values with Kč.
// "Appropriate currency" = currency of the default pricelist in preorder settings.
if (null !== ($priceListId = $preorder->getPricelistID())) {
$currency = $this->getCurrencyFromPricelist($priceListId);
if ($currency) {
$contexts[CurrencyContext::class] = $currency;
}
}
if ($callback) {
return $this->contextManager->activateContexts($contexts, $callback);
}
foreach ($contexts as $class => $id) {
$context = Contexts::get($class);
switch ($id) {
case ContextManager::FORCE_EMPTY:
if (!($context instanceof EmptiableContext)) {
throw new \LogicException('cannot force non-emptiable context to be empty');
}
$context->forceEmpty();
break;
case null:
$context->clearCache();
break;
default:
$context->activate(is_int($id) ? (string) $id : $id);
}
}
return null;
}
private function getCurrencyFromPricelist(int $pricelistId): ?string
{
if (!array_key_exists($pricelistId, $this->pricelistCurrenciesCache)) {
$currency = sqlQueryBuilder()->select('currency')
->from('pricelists')
->andWhere(Operator::equals(['id' => $pricelistId]))
->execute()
->fetchOne();
$this->pricelistCurrenciesCache[$pricelistId] = $currency ?: null;
}
return $this->pricelistCurrenciesCache[$pricelistId];
}
}