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

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.      * @throws UnsupportedException
  129.      */
  130.     public function calculate($ignorePricingRules false)
  131.     {
  132.         // sum up all item prices
  133.         $subTotalNet Decimal::zero();
  134.         $subTotalGross Decimal::zero();
  135.         /** @var Currency|null $currency */
  136.         $currency null;
  137.         /** @var TaxEntry[] $subTotalTaxes */
  138.         $subTotalTaxes = [];
  139.         /** @var TaxEntry[] $grandTotalTaxes */
  140.         $grandTotalTaxes = [];
  141.         foreach ($this->cart->getItems() as $item) {
  142.             if (!is_object($item->getPrice())) {
  143.                 continue;
  144.             }
  145.             if (null === $currency) {
  146.                 $currency $item->getPrice()->getCurrency();
  147.             }
  148.             if ($currency->getShortName() !== $item->getPrice()->getCurrency()->getShortName()) {
  149.                 throw new UnsupportedException(sprintf(
  150.                     'Different currencies within one cart are not supported. See cart %s and product %s)',
  151.                     $this->cart->getId(),
  152.                     $item->getProduct()->getId()
  153.                 ));
  154.             }
  155.             $itemPrice $item->getTotalPrice();
  156.             $subTotalNet $subTotalNet->add($itemPrice->getNetAmount());
  157.             $subTotalGross $subTotalGross->add($itemPrice->getGrossAmount());
  158.             $taxEntries $item->getTotalPrice()->getTaxEntries();
  159.             foreach ($taxEntries as $taxEntry) {
  160.                 $taxId $taxEntry->getTaxId();
  161.                 if (empty($subTotalTaxes[$taxId])) {
  162.                     $subTotalTaxes[$taxId] = clone $taxEntry;
  163.                     $grandTotalTaxes[$taxId] = clone $taxEntry;
  164.                 } else {
  165.                     $subTotalTaxes[$taxId]->setAmount(
  166.                         $subTotalTaxes[$taxId]->getAmount()->add($taxEntry->getAmount())
  167.                     );
  168.                     $grandTotalTaxes[$taxId]->setAmount(
  169.                         $grandTotalTaxes[$taxId]->getAmount()->add($taxEntry->getAmount())
  170.                     );
  171.                 }
  172.             }
  173.         }
  174.         // by default currency is retrieved from item prices. if there are no items, its loaded from the default locale
  175.         // defined in the environment
  176.         if (null === $currency) {
  177.             $currency $this->getDefaultCurrency();
  178.         }
  179.         // populate subTotal price, set net and gross amount, set tax entries and set tax entry combination mode to fixed
  180.         $this->subTotal $this->getDefaultPriceObject($subTotalGross$currency);
  181.         $this->subTotal->setNetAmount($subTotalNet);
  182.         $this->subTotal->setTaxEntries($subTotalTaxes);
  183.         $this->subTotal->setTaxEntryCombinationMode(TaxEntry::CALCULATION_MODE_FIXED);
  184.         // consider all price modificators
  185.         $currentSubTotal $this->getDefaultPriceObject($subTotalGross$currency);
  186.         $currentSubTotal->setNetAmount($subTotalNet);
  187.         $currentSubTotal->setTaxEntryCombinationMode(TaxEntry::CALCULATION_MODE_FIXED);
  188.         $this->modifications = [];
  189.         foreach ($this->getModificators() as $modificator) {
  190.             $modification $modificator->modify($currentSubTotal$this->cart);
  191.             if ($modification !== null) {
  192.                 $this->modifications[$modificator->getName()] = $modification;
  193.                 $currentSubTotal->setNetAmount(
  194.                     $currentSubTotal->getNetAmount()->add($modification->getNetAmount())
  195.                 );
  196.                 $currentSubTotal->setGrossAmount(
  197.                     $currentSubTotal->getGrossAmount()->add($modification->getGrossAmount())
  198.                 );
  199.                 $taxEntries $modification->getTaxEntries();
  200.                 foreach ($taxEntries as $taxEntry) {
  201.                     $taxId $taxEntry->getTaxId();
  202.                     if (empty($grandTotalTaxes[$taxId])) {
  203.                         $grandTotalTaxes[$taxId] = clone $taxEntry;
  204.                     } else {
  205.                         $grandTotalTaxes[$taxId]->setAmount(
  206.                             $grandTotalTaxes[$taxId]->getAmount()->add($taxEntry->getAmount())
  207.                         );
  208.                     }
  209.                 }
  210.             }
  211.         }
  212.         $currentSubTotal->setTaxEntries($grandTotalTaxes);
  213.         $this->grandTotal $currentSubTotal;
  214.         $this->isCalculated true;
  215.         if (!$ignorePricingRules) {
  216.             // apply pricing rules
  217.             $this->appliedPricingRules $this->getPricingManager()->applyCartRules($this->cart);
  218.             // @phpstan-ignore-next-line check if some pricing rule needs recalculation of sums
  219.             if (!$this->isCalculated) {
  220.                 $this->calculate(true);
  221.             }
  222.         }
  223.     }
  224.     public function setPricingManager(PricingManagerInterface $pricingManager)
  225.     {
  226.         $this->pricingManager $pricingManager;
  227.     }
  228.     public function getPricingManager()
  229.     {
  230.         if (empty($this->pricingManager)) {
  231.             $this->pricingManager Factory::getInstance()->getPricingManager();
  232.         }
  233.         return $this->pricingManager;
  234.     }
  235.     /**
  236.      * gets default currency object based on the default currency locale defined in the environment
  237.      *
  238.      * @return Currency
  239.      */
  240.     protected function getDefaultCurrency()
  241.     {
  242.         return $this->environment->getDefaultCurrency();
  243.     }
  244.     /**
  245.      * Possibility to overwrite the price object that should be used
  246.      *
  247.      * @param Decimal $amount
  248.      * @param Currency $currency
  249.      *
  250.      * @return PriceInterface
  251.      */
  252.     protected function getDefaultPriceObject(Decimal $amountCurrency $currency): PriceInterface
  253.     {
  254.         return new Price($amount$currency);
  255.     }
  256.     /**
  257.      * @return PriceInterface $price
  258.      */
  259.     public function getGrandTotal(): PriceInterface
  260.     {
  261.         if (!$this->isCalculated) {
  262.             $this->calculate();
  263.         }
  264.         return $this->grandTotal;
  265.     }
  266.     /**
  267.      * @return ModificatedPriceInterface[] $priceModification
  268.      */
  269.     public function getPriceModifications(): array
  270.     {
  271.         if (!$this->isCalculated) {
  272.             $this->calculate();
  273.         }
  274.         return $this->modifications;
  275.     }
  276.     /**
  277.      * @return PriceInterface $price
  278.      */
  279.     public function getSubTotal(): PriceInterface
  280.     {
  281.         if (!$this->isCalculated) {
  282.             $this->calculate();
  283.         }
  284.         return $this->subTotal;
  285.     }
  286.     /**
  287.      * @return void
  288.      */
  289.     public function reset()
  290.     {
  291.         $this->isCalculated false;
  292.     }
  293.     /**
  294.      * @param CartPriceModificatorInterface $modificator
  295.      *
  296.      * @return CartPriceCalculatorInterface
  297.      */
  298.     public function addModificator(CartPriceModificatorInterface $modificator)
  299.     {
  300.         $this->reset();
  301.         $this->modificators[] = $modificator;
  302.         return $this;
  303.     }
  304.     /**
  305.      * @return CartPriceModificatorInterface[]
  306.      */
  307.     public function getModificators(): array
  308.     {
  309.         return $this->modificators;
  310.     }
  311.     /**
  312.      * @param CartPriceModificatorInterface $modificator
  313.      *
  314.      * @return CartPriceCalculatorInterface
  315.      */
  316.     public function removeModificator(CartPriceModificatorInterface $modificator)
  317.     {
  318.         foreach ($this->modificators as $key => $mod) {
  319.             if ($mod === $modificator) {
  320.                 unset($this->modificators[$key]);
  321.             }
  322.         }
  323.         return $this;
  324.     }
  325.     /**
  326.      * @return RuleInterface[]
  327.      *
  328.      * @throws UnsupportedException
  329.      */
  330.     public function getAppliedPricingRules(): array
  331.     {
  332.         if (!$this->isCalculated) {
  333.             $this->calculate();
  334.         }
  335.         $itemRules = [];
  336.         foreach ($this->cart->getItems() as $item) {
  337.             $priceInfo $item->getPriceInfo();
  338.             if ($priceInfo instanceof PriceInfoInterface) {
  339.                 $itemRules array_merge($itemRules$priceInfo->getRules());
  340.             }
  341.         }
  342.         $itemRules array_filter($itemRules, function (RuleInterface $rule) {
  343.             return $rule->hasProductActions();
  344.         });
  345.         $cartRules array_filter($this->appliedPricingRules, function (RuleInterface $rule) {
  346.             return $rule->hasCartActions();
  347.         });
  348.         $itemRules array_merge($cartRules$itemRules);
  349.         $uniqueItemRules = [];
  350.         foreach ($itemRules as $rule) {
  351.             $uniqueItemRules[$rule->getId()] = $rule;
  352.         }
  353.         return array_values($uniqueItemRules);
  354.     }
  355.     /**
  356.      * @return bool
  357.      */
  358.     public function isCalculated(): bool
  359.     {
  360.         return $this->isCalculated;
  361.     }
  362. }