vendor/shopware/platform/src/Core/Checkout/Cart/CartRuleLoader.php line 68

Open in your IDE?
  1. <?php declare(strict_types=1);
  2. namespace Shopware\Core\Checkout\Cart;
  3. use Doctrine\DBAL\Connection;
  4. use Psr\Log\LoggerInterface;
  5. use Shopware\Core\Checkout\Cart\Event\CartCreatedEvent;
  6. use Shopware\Core\Checkout\Cart\Exception\CartTokenNotFoundException;
  7. use Shopware\Core\Checkout\Cart\LineItem\LineItem;
  8. use Shopware\Core\Checkout\Cart\Price\Struct\CartPrice;
  9. use Shopware\Core\Checkout\Cart\Tax\TaxDetector;
  10. use Shopware\Core\Content\Rule\RuleCollection;
  11. use Shopware\Core\Defaults;
  12. use Shopware\Core\Framework\Context;
  13. use Shopware\Core\Framework\DataAbstractionLayer\Exception\EntityNotFoundException;
  14. use Shopware\Core\Framework\Util\FloatComparator;
  15. use Shopware\Core\Framework\Uuid\Uuid;
  16. use Shopware\Core\System\Country\CountryDefinition;
  17. use Shopware\Core\System\Country\CountryEntity;
  18. use Shopware\Core\System\SalesChannel\SalesChannelContext;
  19. use Symfony\Component\Cache\Adapter\TagAwareAdapterInterface;
  20. use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
  21. class CartRuleLoader
  22. {
  23.     private const MAX_ITERATION 7;
  24.     private CartPersisterInterface $cartPersister;
  25.     private ?RuleCollection $rules null;
  26.     private Processor $processor;
  27.     private LoggerInterface $logger;
  28.     private TagAwareAdapterInterface $cache;
  29.     private AbstractRuleLoader $ruleLoader;
  30.     private TaxDetector $taxDetector;
  31.     private EventDispatcherInterface $dispatcher;
  32.     private Connection $connection;
  33.     private array $currencyFactor = [];
  34.     public function __construct(
  35.         CartPersisterInterface $cartPersister,
  36.         Processor $processor,
  37.         LoggerInterface $logger,
  38.         TagAwareAdapterInterface $cache,
  39.         AbstractRuleLoader $loader,
  40.         TaxDetector $taxDetector,
  41.         Connection $connection,
  42.         EventDispatcherInterface $dispatcher
  43.     ) {
  44.         $this->cartPersister $cartPersister;
  45.         $this->processor $processor;
  46.         $this->logger $logger;
  47.         $this->cache $cache;
  48.         $this->ruleLoader $loader;
  49.         $this->taxDetector $taxDetector;
  50.         $this->dispatcher $dispatcher;
  51.         $this->connection $connection;
  52.     }
  53.     public function loadByToken(SalesChannelContext $contextstring $cartToken): RuleLoaderResult
  54.     {
  55.         try {
  56.             $cart $this->cartPersister->load($cartToken$context);
  57.             return $this->load($context$cart, new CartBehavior($context->getPermissions()), false);
  58.         } catch (CartTokenNotFoundException $e) {
  59.             $cart = new Cart($context->getSalesChannel()->getTypeId(), $cartToken);
  60.             $this->dispatcher->dispatch(new CartCreatedEvent($cart));
  61.             return $this->load($context$cart, new CartBehavior($context->getPermissions()), true);
  62.         }
  63.     }
  64.     public function loadByCart(SalesChannelContext $contextCart $cartCartBehavior $behaviorContextbool $isNew false): RuleLoaderResult
  65.     {
  66.         return $this->load($context$cart$behaviorContext$isNew);
  67.     }
  68.     public function reset(): void
  69.     {
  70.         $this->rules null;
  71.         $this->cache->deleteItem(CachedRuleLoader::CACHE_KEY);
  72.     }
  73.     private function load(SalesChannelContext $contextCart $cartCartBehavior $behaviorContextbool $new): RuleLoaderResult
  74.     {
  75.         $rules $this->loadRules($context->getContext());
  76.         // save all rules for later usage
  77.         $all $rules;
  78.         $ids $new $rules->getIds() : $cart->getRuleIds();
  79.         // update rules in current context
  80.         $context->setRuleIds($ids);
  81.         $iteration 1;
  82.         $timestamps $cart->getLineItems()->fmap(function (LineItem $lineItem) {
  83.             if ($lineItem->getDataTimestamp() === null) {
  84.                 return null;
  85.             }
  86.             return $lineItem->getDataTimestamp()->format(Defaults::STORAGE_DATE_TIME_FORMAT);
  87.         });
  88.         // start first cart calculation to have all objects enriched
  89.         $cart $this->processor->process($cart$context$behaviorContext);
  90.         do {
  91.             $compare $cart;
  92.             if ($iteration self::MAX_ITERATION) {
  93.                 break;
  94.             }
  95.             // filter rules which matches to current scope
  96.             $rules $rules->filterMatchingRules($cart$context);
  97.             // update matching rules in context
  98.             $context->setRuleIds($rules->getIds());
  99.             // calculate cart again
  100.             $cart $this->processor->process($cart$context$behaviorContext);
  101.             // check if the cart changed, in this case we have to recalculate the cart again
  102.             $recalculate $this->cartChanged($cart$compare);
  103.             // check if rules changed for the last calculated cart, in this case we have to recalculate
  104.             $ruleCompare $all->filterMatchingRules($cart$context);
  105.             if (!$rules->equals($ruleCompare)) {
  106.                 $recalculate true;
  107.                 $rules $ruleCompare;
  108.             }
  109.             ++$iteration;
  110.         } while ($recalculate);
  111.         $cart $this->validateTaxFree($context$cart$behaviorContext);
  112.         $index 0;
  113.         foreach ($rules as $rule) {
  114.             ++$index;
  115.             $this->logger->info(
  116.                 sprintf('#%s Rule detection: %s with priority %s (id: %s)'$index$rule->getName(), $rule->getPriority(), $rule->getId())
  117.             );
  118.         }
  119.         $context->setRuleIds($rules->getIds());
  120.         // save the cart if errors exist, so the errors get persisted
  121.         if ($cart->getErrors()->count() > || $this->updated($cart$timestamps)) {
  122.             $this->cartPersister->save($cart$context);
  123.         }
  124.         return new RuleLoaderResult($cart$rules);
  125.     }
  126.     private function loadRules(Context $context): RuleCollection
  127.     {
  128.         if ($this->rules !== null) {
  129.             return $this->rules;
  130.         }
  131.         return $this->rules $this->ruleLoader->load($context);
  132.     }
  133.     private function cartChanged(Cart $previousCart $current): bool
  134.     {
  135.         $previousLineItems $previous->getLineItems();
  136.         $currentLineItems $current->getLineItems();
  137.         return $previousLineItems->count() !== $currentLineItems->count()
  138.             || $previous->getPrice()->getTotalPrice() !== $current->getPrice()->getTotalPrice()
  139.             || $previousLineItems->getKeys() !== $currentLineItems->getKeys()
  140.             || $previousLineItems->getTypes() !== $currentLineItems->getTypes()
  141.         ;
  142.     }
  143.     private function detectTaxType(SalesChannelContext $contextfloat $cartNetAmount 0): string
  144.     {
  145.         $currency $context->getCurrency();
  146.         $currencyTaxFreeAmount $currency->getTaxFreeFrom();
  147.         $isReachedCurrencyTaxFreeAmount $currencyTaxFreeAmount && $cartNetAmount >= $currencyTaxFreeAmount;
  148.         if ($isReachedCurrencyTaxFreeAmount) {
  149.             return CartPrice::TAX_STATE_FREE;
  150.         }
  151.         $country $context->getShippingLocation()->getCountry();
  152.         $isReachedCustomerTaxFreeAmount $country->getCustomerTax()->getEnabled() && $this->isReachedCountryTaxFreeAmount($context$country$cartNetAmount);
  153.         $isReachedCompanyTaxFreeAmount $this->taxDetector->isCompanyTaxFree($context$country) && $this->isReachedCountryTaxFreeAmount($context$country$cartNetAmountCountryDefinition::TYPE_COMPANY_TAX_FREE);
  154.         if ($isReachedCustomerTaxFreeAmount || $isReachedCompanyTaxFreeAmount) {
  155.             return CartPrice::TAX_STATE_FREE;
  156.         }
  157.         if ($this->taxDetector->useGross($context)) {
  158.             return CartPrice::TAX_STATE_GROSS;
  159.         }
  160.         return CartPrice::TAX_STATE_NET;
  161.     }
  162.     /**
  163.      * @param array<string, string> $timestamps
  164.      */
  165.     private function updated(Cart $cart, array $timestamps): bool
  166.     {
  167.         foreach ($cart->getLineItems() as $lineItem) {
  168.             if (!isset($timestamps[$lineItem->getId()])) {
  169.                 return true;
  170.             }
  171.             $original $timestamps[$lineItem->getId()];
  172.             $timestamp $lineItem->getDataTimestamp() !== null $lineItem->getDataTimestamp()->format(Defaults::STORAGE_DATE_TIME_FORMAT) : null;
  173.             if ($original !== $timestamp) {
  174.                 return true;
  175.             }
  176.         }
  177.         return \count($timestamps) !== $cart->getLineItems()->count();
  178.     }
  179.     private function isReachedCountryTaxFreeAmount(
  180.         SalesChannelContext $context,
  181.         CountryEntity $country,
  182.         float $cartNetAmount 0,
  183.         string $taxFreeType CountryDefinition::TYPE_CUSTOMER_TAX_FREE
  184.     ): bool {
  185.         $countryTaxFreeLimit $taxFreeType === CountryDefinition::TYPE_CUSTOMER_TAX_FREE $country->getCustomerTax() : $country->getCompanyTax();
  186.         if (!$countryTaxFreeLimit->getEnabled()) {
  187.             return false;
  188.         }
  189.         $countryTaxFreeLimitAmount $countryTaxFreeLimit->getAmount() / $this->fetchCurrencyFactor($countryTaxFreeLimit->getCurrencyId(), $context);
  190.         $currency $context->getCurrency();
  191.         $cartNetAmount /= $this->fetchCurrencyFactor($currency->getId(), $context);
  192.         // currency taxFreeAmount === 0.0 mean currency taxFreeFrom is disabled
  193.         return $currency->getTaxFreeFrom() === 0.0 && FloatComparator::greaterThanOrEquals($cartNetAmount$countryTaxFreeLimitAmount);
  194.     }
  195.     private function fetchCurrencyFactor(string $currencyIdSalesChannelContext $context): float
  196.     {
  197.         if ($currencyId === Defaults::CURRENCY) {
  198.             return 1;
  199.         }
  200.         $currency $context->getCurrency();
  201.         if ($currencyId === $currency->getId()) {
  202.             return $currency->getFactor();
  203.         }
  204.         if (\array_key_exists($currencyId$this->currencyFactor)) {
  205.             return $this->currencyFactor[$currencyId];
  206.         }
  207.         $currencyFactor $this->connection->fetchOne(
  208.             'SELECT `factor` FROM `currency` WHERE `id` = :currencyId',
  209.             ['currencyId' => Uuid::fromHexToBytes($currencyId)]
  210.         );
  211.         if (!$currencyFactor) {
  212.             throw new EntityNotFoundException('currency'$currencyId);
  213.         }
  214.         return $this->currencyFactor[$currencyId] = (float) $currencyFactor;
  215.     }
  216.     private function validateTaxFree(SalesChannelContext $contextCart $cartCartBehavior $behaviorContext): Cart
  217.     {
  218.         $totalCartNetAmount $cart->getPrice()->getPositionPrice();
  219.         if ($context->getTaxState() === CartPrice::TAX_STATE_GROSS) {
  220.             $totalCartNetAmount $totalCartNetAmount $cart->getLineItems()->getPrices()->getCalculatedTaxes()->getAmount();
  221.         }
  222.         $taxState $this->detectTaxType($context$totalCartNetAmount);
  223.         $previous $context->getTaxState();
  224.         if ($taxState === $previous) {
  225.             return $cart;
  226.         }
  227.         $context->setTaxState($taxState);
  228.         $cart->setData(null);
  229.         $cart $this->processor->process($cart$context$behaviorContext);
  230.         if ($previous !== CartPrice::TAX_STATE_FREE) {
  231.             $context->setTaxState($previous);
  232.         }
  233.         return $cart;
  234.     }
  235. }