vendor/pimcore/pimcore/bundles/EcommerceFrameworkBundle/CartManager/CartPriceCalculator.php line 114

Open in your IDE?
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4.  * Pimcore
  5.  *
  6.  * This source file is available under two different licenses:
  7.  * - GNU General Public License version 3 (GPLv3)
  8.  * - Pimcore Commercial License (PCL)
  9.  * Full copyright and license information is available in
  10.  * LICENSE.md which is distributed with this source code.
  11.  *
  12.  *  @copyright  Copyright (c) Pimcore GmbH (http://www.pimcore.org)
  13.  *  @license    http://www.pimcore.org/license     GPLv3 and PCL
  14.  */
  15. namespace Pimcore\Bundle\EcommerceFrameworkBundle\CartManager;
  16. use Pimcore\Bundle\EcommerceFrameworkBundle\CartManager\CartPriceModificator\CartPriceModificatorInterface;
  17. use Pimcore\Bundle\EcommerceFrameworkBundle\EnvironmentInterface;
  18. use Pimcore\Bundle\EcommerceFrameworkBundle\Exception\UnsupportedException;
  19. use Pimcore\Bundle\EcommerceFrameworkBundle\Factory;
  20. use Pimcore\Bundle\EcommerceFrameworkBundle\Model\Currency;
  21. use Pimcore\Bundle\EcommerceFrameworkBundle\PriceSystem\ModificatedPriceInterface;
  22. use Pimcore\Bundle\EcommerceFrameworkBundle\PriceSystem\Price;
  23. use Pimcore\Bundle\EcommerceFrameworkBundle\PriceSystem\PriceInterface;
  24. use Pimcore\Bundle\EcommerceFrameworkBundle\PriceSystem\TaxManagement\TaxEntry;
  25. use Pimcore\Bundle\EcommerceFrameworkBundle\PricingManager\PriceInfoInterface;
  26. use Pimcore\Bundle\EcommerceFrameworkBundle\PricingManager\PricingManagerInterface;
  27. use Pimcore\Bundle\EcommerceFrameworkBundle\PricingManager\RuleInterface;
  28. use Pimcore\Bundle\EcommerceFrameworkBundle\Type\Decimal;
  29. use Symfony\Component\OptionsResolver\OptionsResolver;
  30. class CartPriceCalculator implements CartPriceCalculatorInterface
  31. {
  32.     /**
  33.      * @var EnvironmentInterface
  34.      */
  35.     protected $environment;
  36.     /**
  37.      * @var CartInterface
  38.      */
  39.     protected $cart;
  40.     /**
  41.      * @var bool
  42.      */
  43.     protected $isCalculated false;
  44.     /**
  45.      * @var PriceInterface
  46.      */
  47.     protected $subTotal;
  48.     /**
  49.      * @var PriceInterface
  50.      */
  51.     protected $grandTotal;
  52.     /**
  53.      * Standard modificators are handled as configuration as they may
  54.      * be reinitialized on demand (e.g. inside AJAX calls).
  55.      *
  56.      * @var array
  57.      */
  58.     protected $modificatorConfig = [];
  59.     /**
  60.      * @var CartPriceModificatorInterface[]
  61.      */
  62.     protected $modificators = [];
  63.     /**
  64.      * @var ModificatedPriceInterface[]
  65.      */
  66.     protected $modifications = [];
  67.     /**
  68.      * @var RuleInterface[]
  69.      */
  70.     protected $appliedPricingRules = [];
  71.     /**
  72.      * @var PricingManagerInterface|null
  73.      */
  74.     protected $pricingManager;
  75.     /**
  76.      * @param EnvironmentInterface $environment
  77.      * @param CartInterface $cart
  78.      * @param array $modificatorConfig
  79.      */
  80.     public function __construct(EnvironmentInterface $environmentCartInterface $cart, array $modificatorConfig = [])
  81.     {
  82.         $this->environment $environment;
  83.         $this->cart $cart;
  84.         $this->setModificatorConfig($modificatorConfig);
  85.         $this->initModificators();
  86.     }
  87.     /**
  88.      * (Re-)initialize standard price modificators, e.g. after removing an item from a cart
  89.      * within the same request, such as an AJAX-call.
  90.      */
  91.     public function initModificators()
  92.     {
  93.         $this->reset();
  94.         $this->modificators = [];
  95.         foreach ($this->modificatorConfig as $config) {
  96.             $this->modificators[] = $this->buildModificator($config);
  97.         }
  98.     }
  99.     protected function buildModificator(array $config): CartPriceModificatorInterface
  100.     {
  101.         /** @var CartPriceModificatorInterface $modificator */
  102.         $modificator null;
  103.         $className $config['class'];
  104.         if (!empty($config['options'])) {
  105.             $modificator = new $className($config['options']);
  106.         } else {
  107.             $modificator = new $className();
  108.         }
  109.         return $modificator;
  110.     }
  111.     protected function setModificatorConfig(array $modificatorConfig)
  112.     {
  113.         $resolver = new OptionsResolver();
  114.         $this->configureModificatorResolver($resolver);
  115.         foreach ($modificatorConfig as $config) {
  116.             $this->modificatorConfig[] = $resolver->resolve($config);
  117.         }
  118.     }
  119.     protected function configureModificatorResolver(OptionsResolver $resolver)
  120.     {
  121.         $resolver->setDefined(['class''options']);
  122.         $resolver->setAllowedTypes('class''string');
  123.         $resolver->setDefaults([
  124.             'options' => [],
  125.         ]);
  126.     }
  127.     /**
  128.      * @param bool $ignorePricingRules
  129.      *
  130.      * @return void
  131.      *
  132.      * @throws UnsupportedException
  133.      */
  134.     public function calculate($ignorePricingRules false)
  135.     {
  136.         // sum up all item prices
  137.         $subTotalNet Decimal::zero();
  138.         $subTotalGross Decimal::zero();
  139.         /** @var Currency|null $currency */
  140.         $currency null;
  141.         /** @var TaxEntry[] $subTotalTaxes */
  142.         $subTotalTaxes = [];
  143.         /** @var TaxEntry[] $grandTotalTaxes */
  144.         $grandTotalTaxes = [];
  145.         foreach ($this->cart->getItems() as $item) {
  146.             if (!is_object($item->getPrice())) {
  147.                 continue;
  148.             }
  149.             if (null === $currency) {
  150.                 $currency $item->getPrice()->getCurrency();
  151.             }
  152.             if ($currency->getShortName() !== $item->getPrice()->getCurrency()->getShortName()) {
  153.                 throw new UnsupportedException(sprintf(
  154.                     'Different currencies within one cart are not supported. See cart %s and product %s)',
  155.                     $this->cart->getId(),
  156.                     $item->getProduct()->getId()
  157.                 ));
  158.             }
  159.             $itemPrice $item->getTotalPrice();
  160.             $subTotalNet $subTotalNet->add($itemPrice->getNetAmount());
  161.             $subTotalGross $subTotalGross->add($itemPrice->getGrossAmount());
  162.             $taxEntries $item->getTotalPrice()->getTaxEntries();
  163.             foreach ($taxEntries as $taxEntry) {
  164.                 $taxId $taxEntry->getTaxId();
  165.                 if (empty($subTotalTaxes[$taxId])) {
  166.                     $subTotalTaxes[$taxId] = clone $taxEntry;
  167.                     $grandTotalTaxes[$taxId] = clone $taxEntry;
  168.                 } else {
  169.                     $subTotalTaxes[$taxId]->setAmount(
  170.                         $subTotalTaxes[$taxId]->getAmount()->add($taxEntry->getAmount())
  171.                     );
  172.                     $grandTotalTaxes[$taxId]->setAmount(
  173.                         $grandTotalTaxes[$taxId]->getAmount()->add($taxEntry->getAmount())
  174.                     );
  175.                 }
  176.             }
  177.         }
  178.         // by default currency is retrieved from item prices. if there are no items, its loaded from the default locale
  179.         // defined in the environment
  180.         if (null === $currency) {
  181.             $currency $this->getDefaultCurrency();
  182.         }
  183.         // populate subTotal price, set net and gross amount, set tax entries and set tax entry combination mode to fixed
  184.         $this->subTotal $this->getDefaultPriceObject($subTotalGross$currency);
  185.         $this->subTotal->setNetAmount($subTotalNet);
  186.         $this->subTotal->setTaxEntries($subTotalTaxes);
  187.         $this->subTotal->setTaxEntryCombinationMode(TaxEntry::CALCULATION_MODE_FIXED);
  188.         // consider all price modificators
  189.         $currentSubTotal $this->getDefaultPriceObject($subTotalGross$currency);
  190.         $currentSubTotal->setNetAmount($subTotalNet);
  191.         $currentSubTotal->setTaxEntryCombinationMode(TaxEntry::CALCULATION_MODE_FIXED);
  192.         $this->modifications = [];
  193.         foreach ($this->getModificators() as $modificator) {
  194.             $modification $modificator->modify($currentSubTotal$this->cart);
  195.             if ($modification !== null) {
  196.                 $this->modifications[$modificator->getName()] = $modification;
  197.                 $currentSubTotal->setNetAmount(
  198.                     $currentSubTotal->getNetAmount()->add($modification->getNetAmount())
  199.                 );
  200.                 $currentSubTotal->setGrossAmount(
  201.                     $currentSubTotal->getGrossAmount()->add($modification->getGrossAmount())
  202.                 );
  203.                 $taxEntries $modification->getTaxEntries();
  204.                 foreach ($taxEntries as $taxEntry) {
  205.                     $taxId $taxEntry->getTaxId();
  206.                     if (empty($grandTotalTaxes[$taxId])) {
  207.                         $grandTotalTaxes[$taxId] = clone $taxEntry;
  208.                     } else {
  209.                         $grandTotalTaxes[$taxId]->setAmount(
  210.                             $grandTotalTaxes[$taxId]->getAmount()->add($taxEntry->getAmount())
  211.                         );
  212.                     }
  213.                 }
  214.             }
  215.         }
  216.         $currentSubTotal->setTaxEntries($grandTotalTaxes);
  217.         $this->grandTotal $currentSubTotal;
  218.         $this->isCalculated true;
  219.         if (!$ignorePricingRules) {
  220.             // apply pricing rules
  221.             $this->appliedPricingRules $this->getPricingManager()->applyCartRules($this->cart);
  222.             // @phpstan-ignore-next-line check if some pricing rule needs recalculation of sums
  223.             if (!$this->isCalculated) {
  224.                 $this->calculate(true);
  225.             }
  226.         }
  227.     }
  228.     public function setPricingManager(PricingManagerInterface $pricingManager)
  229.     {
  230.         $this->pricingManager $pricingManager;
  231.     }
  232.     public function getPricingManager()
  233.     {
  234.         if (empty($this->pricingManager)) {
  235.             $this->pricingManager Factory::getInstance()->getPricingManager();
  236.         }
  237.         return $this->pricingManager;
  238.     }
  239.     /**
  240.      * gets default currency object based on the default currency locale defined in the environment
  241.      *
  242.      * @return Currency
  243.      */
  244.     protected function getDefaultCurrency()
  245.     {
  246.         return $this->environment->getDefaultCurrency();
  247.     }
  248.     /**
  249.      * Possibility to overwrite the price object that should be used
  250.      *
  251.      * @param Decimal $amount
  252.      * @param Currency $currency
  253.      *
  254.      * @return PriceInterface
  255.      */
  256.     protected function getDefaultPriceObject(Decimal $amountCurrency $currency): PriceInterface
  257.     {
  258.         return new Price($amount$currency);
  259.     }
  260.     /**
  261.      * @return PriceInterface $price
  262.      */
  263.     public function getGrandTotal(): PriceInterface
  264.     {
  265.         if (!$this->isCalculated) {
  266.             $this->calculate();
  267.         }
  268.         return $this->grandTotal;
  269.     }
  270.     /**
  271.      * @return ModificatedPriceInterface[] $priceModification
  272.      */
  273.     public function getPriceModifications(): array
  274.     {
  275.         if (!$this->isCalculated) {
  276.             $this->calculate();
  277.         }
  278.         return $this->modifications;
  279.     }
  280.     /**
  281.      * @return PriceInterface $price
  282.      */
  283.     public function getSubTotal(): PriceInterface
  284.     {
  285.         if (!$this->isCalculated) {
  286.             $this->calculate();
  287.         }
  288.         return $this->subTotal;
  289.     }
  290.     /**
  291.      * @return void
  292.      */
  293.     public function reset()
  294.     {
  295.         $this->isCalculated false;
  296.     }
  297.     /**
  298.      * @param CartPriceModificatorInterface $modificator
  299.      *
  300.      * @return CartPriceCalculatorInterface
  301.      */
  302.     public function addModificator(CartPriceModificatorInterface $modificator)
  303.     {
  304.         $this->reset();
  305.         $this->modificators[] = $modificator;
  306.         return $this;
  307.     }
  308.     /**
  309.      * @return CartPriceModificatorInterface[]
  310.      */
  311.     public function getModificators(): array
  312.     {
  313.         return $this->modificators;
  314.     }
  315.     /**
  316.      * @param CartPriceModificatorInterface $modificator
  317.      *
  318.      * @return CartPriceCalculatorInterface
  319.      */
  320.     public function removeModificator(CartPriceModificatorInterface $modificator)
  321.     {
  322.         foreach ($this->modificators as $key => $mod) {
  323.             if ($mod === $modificator) {
  324.                 unset($this->modificators[$key]);
  325.             }
  326.         }
  327.         return $this;
  328.     }
  329.     /**
  330.      * @return RuleInterface[]
  331.      *
  332.      * @throws UnsupportedException
  333.      */
  334.     public function getAppliedPricingRules(): array
  335.     {
  336.         if (!$this->isCalculated) {
  337.             $this->calculate();
  338.         }
  339.         $itemRules = [];
  340.         foreach ($this->cart->getItems() as $item) {
  341.             $priceInfo $item->getPriceInfo();
  342.             if ($priceInfo instanceof PriceInfoInterface) {
  343.                 $itemRules array_merge($itemRules$priceInfo->getRules());
  344.             }
  345.         }
  346.         $itemRules array_filter($itemRules, function (RuleInterface $rule) {
  347.             return $rule->hasProductActions();
  348.         });
  349.         $cartRules array_filter($this->appliedPricingRules, function (RuleInterface $rule) {
  350.             return $rule->hasCartActions();
  351.         });
  352.         $itemRules array_merge($cartRules$itemRules);
  353.         $uniqueItemRules = [];
  354.         foreach ($itemRules as $rule) {
  355.             $uniqueItemRules[$rule->getId()] = $rule;
  356.         }
  357.         return array_values($uniqueItemRules);
  358.     }
  359.     /**
  360.      * @return bool
  361.      */
  362.     public function isCalculated(): bool
  363.     {
  364.         return $this->isCalculated;
  365.     }
  366. }