custom/plugins/AcrisPromotionCS/src/Subscriber/ProductLoadedSubscriber.php line 99

Open in your IDE?
  1. <?php declare(strict_types=1);
  2. namespace Acris\Promotion\Subscriber;
  3. use Acris\Promotion\Component\PriceRoundingService;
  4. use Acris\Promotion\Core\Checkout\Cart\Price\Struct\AcrisListPrice;
  5. use Acris\Promotion\Core\Checkout\Cart\Price\Struct\OriginalUnitPrice;
  6. use Acris\Promotion\Core\Checkout\Cart\PromotionCartProcessor;
  7. use Acris\Promotion\Core\Checkout\Promotion\LineItemPromotion;
  8. use Acris\Promotion\Core\Checkout\Promotion\LineItemPromotionCollection;
  9. use Acris\Rrp\Components\RrpPriceCalculatorService;
  10. use Shopware\Core\Checkout\Cart\Cart;
  11. use Shopware\Core\Checkout\Cart\CartBehavior;
  12. use Shopware\Core\Checkout\Cart\LineItem\LineItem;
  13. use Shopware\Core\Checkout\Cart\Price\CashRounding;
  14. use Shopware\Core\Checkout\Cart\Price\Struct\CalculatedPrice;
  15. use Shopware\Core\Checkout\Cart\Price\Struct\ListPrice;
  16. use Shopware\Core\Checkout\Cart\Price\Struct\PriceCollection;
  17. use Shopware\Core\Checkout\Cart\Price\Struct\ReferencePrice;
  18. use Shopware\Core\Content\Product\Cart\ProductLineItemFactory;
  19. use Shopware\Core\Content\Product\DataAbstractionLayer\CheapestPrice\CalculatedCheapestPrice;
  20. use Shopware\Core\Content\Product\SalesChannel\SalesChannelProductEntity;
  21. use Shopware\Core\Framework\DataAbstractionLayer\Pricing\CashRoundingConfig;
  22. use Shopware\Core\Framework\Struct\ArrayEntity;
  23. use Shopware\Core\Framework\Util\Random;
  24. use Shopware\Core\System\SalesChannel\Entity\SalesChannelEntityLoadedEvent;
  25. use Shopware\Core\System\SalesChannel\SalesChannelContext;
  26. use Shopware\Core\System\SystemConfig\SystemConfigService;
  27. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  28. class ProductLoadedSubscriber implements EventSubscriberInterface
  29. {
  30.     const PRODUCT_PROMOTION_EXTENSION_NAME 'acrisLineItemPromotionCollection';
  31.     const PRODUCT_ORG_UNIT_PRICE_EXTENSION_NAME 'acrisPromotionOrgPrice';
  32.     const PRODUCT_PROMOTION_ID_EXTENSION_NAME 'acrisPromotion';
  33.     /**
  34.      * @var ProductLineItemFactory
  35.      */
  36.     private $productLineItemFactory;
  37.     /**
  38.      * @var PromotionCartProcessor
  39.      */
  40.     private $promotionCartProcessor;
  41.     /**
  42.      * @var array|null
  43.      */
  44.     private $promotionsAuto;
  45.     /**
  46.      * @var CashRounding
  47.      */
  48.     private $cashRounding;
  49.     /**
  50.      * @var PriceRoundingService
  51.      */
  52.     private $priceRoundingService;
  53.     /**
  54.      * @var SystemConfigService
  55.      */
  56.     private $systemConfigService;
  57.     /**
  58.      * @var RrpPriceCalculatorService|null
  59.      */
  60.     private $rrpPriceCalculatorService;
  61.     public function __construct(
  62.         ProductLineItemFactory     $productLineItemFactory,
  63.         PromotionCartProcessor     $promotionCartProcessor,
  64.         CashRounding               $cashRounding,
  65.         PriceRoundingService       $priceRoundingService,
  66.         SystemConfigService        $systemConfigService,
  67.         ?RrpPriceCalculatorService $rrpPriceCalculatorService
  68.     )
  69.     {
  70.         $this->productLineItemFactory $productLineItemFactory;
  71.         $this->promotionCartProcessor $promotionCartProcessor;
  72.         $this->promotionsAuto null;
  73.         $this->cashRounding $cashRounding;
  74.         $this->priceRoundingService $priceRoundingService;
  75.         $this->systemConfigService $systemConfigService;
  76.         $this->rrpPriceCalculatorService $rrpPriceCalculatorService;
  77.     }
  78.     public static function getSubscribedEvents()
  79.     {
  80.         return [
  81.             'sales_channel.product.loaded' => ['loaded', -50],
  82.         ];
  83.     }
  84.     public function loaded(SalesChannelEntityLoadedEvent $event): void
  85.     {
  86.         $salesChannelContext $event->getSalesChannelContext();
  87.         if ($salesChannelContext->hasExtension('acrisProcessCart') === true) {
  88.             return;
  89.         }
  90.         $salesChannelContext->addExtension('acrisProcessCart', new ArrayEntity(['process' => true]));
  91.         $behavior = new CartBehavior($salesChannelContext->getPermissions());
  92.         /** @var SalesChannelProductEntity $product */
  93.         foreach ($event->getEntities() as $product) {
  94.             $originalProductToCalculate = clone $product;
  95.             // have cheapest price
  96.             if ($product->getCalculatedCheapestPrice()) {
  97.                 $product->setCalculatedCheapestPrice($this->processProductCalculatedCheapestPrice($product->getCalculatedCheapestPrice(), $originalProductToCalculate$product$behavior$salesChannelContext));
  98.             }
  99.             // have calculated prices
  100.             if ($product->getCalculatedPrices() && $product->getCalculatedPrices()->count() > 0) {
  101.                 $calculatedPriceCollectionNew = new PriceCollection();
  102.                 foreach ($product->getCalculatedPrices() as $calculatedPrice) {
  103.                     $calculatedPriceCollectionNew->add($this->processProductCalculatedPrice($calculatedPrice$originalProductToCalculate$product$behavior$salesChannelContext));
  104.                 }
  105.                 $product->setCalculatedPrices($calculatedPriceCollectionNew);
  106.             }
  107.             if ($product->getCalculatedPrice()) {
  108.                 $product->setCalculatedPrice($this->processProductCalculatedPrice($product->getCalculatedPrice(), $originalProductToCalculate$product$behavior$salesChannelContext));
  109.             }
  110.         }
  111.         $salesChannelContext->removeExtension('acrisProcessCart');
  112.     }
  113.     private function processProductCalculatedPrice(CalculatedPrice $calculatedPriceSalesChannelProductEntity $productSalesChannelProductEntity $originalProductCartBehavior $behaviorSalesChannelContext $salesChannelContext): CalculatedPrice
  114.     {
  115.         $cart = new Cart($salesChannelContext->getSalesChannel()->getTypeId(), Random::getAlphanumericString(32));
  116.         $quantity = (int)$calculatedPrice->getQuantity();
  117.         if ($quantity 1) {
  118.             $quantity 1;
  119.             $calculatedPrice = new CalculatedPrice(
  120.                 $calculatedPrice->getUnitPrice(),
  121.                 $calculatedPrice->getUnitPrice(),
  122.                 $calculatedPrice->getCalculatedTaxes(),
  123.                 $calculatedPrice->getTaxRules(),
  124.                 $quantity,
  125.                 $calculatedPrice->getReferencePrice(),
  126.                 $calculatedPrice->getListPrice(),
  127.                 $calculatedPrice->getRegulationPrice()
  128.             );
  129.         }
  130.         $lineItem $this->productLineItemFactory->create($product->getId(), ['quantity' => $quantity]);
  131.         $cart->add($lineItem);
  132.         // prevent calling of promotions from database duplicate times
  133.         if ($this->promotionsAuto !== null) {
  134.             $cart->getData()->set('promotions-auto'$this->promotionsAuto);
  135.         }
  136.         $cart $this->promotionCartProcessor->process($cart$salesChannelContext$behavior$product$calculatedPrice);
  137.         // better compatibility with AcrisDiscountListPrice
  138.         if ($cart->getLineItems()->has($product->getId()) === true && $cart->getLineItems()->get($product->getId())->getPrice() instanceof CalculatedPrice) {
  139.             $calculatedPrice $cart->getLineItems()->get($product->getId())->getPrice();
  140.         }
  141.         // save called automatic promotions for further use prevent calling again
  142.         $this->promotionsAuto $cart->getData()->get('promotions-auto');
  143.         return $this->calculatePriceByPromotion($calculatedPrice$quantity$cart$originalProduct$salesChannelContext);
  144.     }
  145.     private function calculatePriceByPromotion(CalculatedPrice $calculatedPriceint $quantityCart $cartSalesChannelProductEntity $originalProductSalesChannelContext $salesChannelContext): CalculatedPrice
  146.     {
  147.         $lineItemPromotionCollection = new LineItemPromotionCollection();
  148.         foreach ($cart->getLineItems() as $lineItem) {
  149.             if ($lineItem->getType() === LineItem::PROMOTION_LINE_ITEM_TYPE) {
  150.                 $composition $lineItem->getPayloadValue('composition');
  151.                 if ($composition === null) {
  152.                     continue;
  153.                 }
  154.                 foreach ($composition as $lineItemDiscount) {
  155.                     if (array_key_exists('id'$lineItemDiscount) === true && array_key_exists('discount'$lineItemDiscount) === true) {
  156.                         if (!empty($lineItem->getPayloadValue('promotionId'))) {
  157.                             if ($originalProduct->hasExtension(self::PRODUCT_PROMOTION_ID_EXTENSION_NAME) && $originalProduct->getExtension(self::PRODUCT_PROMOTION_ID_EXTENSION_NAME)->has('promotionIds')) {
  158.                                 $promotionIds $originalProduct->getExtension(self::PRODUCT_PROMOTION_ID_EXTENSION_NAME)->get('promotionIds');
  159.                                 if (!in_array($lineItem->getPayloadValue('promotionId'), $promotionIds)) {
  160.                                     $promotionIds[] = $lineItem->getPayloadValue('promotionId');
  161.                                     $originalProduct->addExtension(self::PRODUCT_PROMOTION_ID_EXTENSION_NAME, new ArrayEntity([
  162.                                         'promotionIds' => $promotionIds
  163.                                     ]));
  164.                                 }
  165.                             } else {
  166.                                 $originalProduct->addExtension(self::PRODUCT_PROMOTION_ID_EXTENSION_NAME, new ArrayEntity([
  167.                                     'promotionIds' => [$lineItem->getPayloadValue('promotionId')]
  168.                                 ]));
  169.                             }
  170.                         }
  171.                         $discount $lineItemDiscount['discount'];
  172.                         if (!is_float($discount)) {
  173.                             continue 2;
  174.                         }
  175.                         $discount $this->roundDiscountIfNecessary($discount$salesChannelContext->getItemRounding());
  176.                         $lineItemPromotionCollection->add(new LineItemPromotion(
  177.                             $lineItem->getPayloadValue('discountId'),
  178.                             $lineItem->getPayloadValue('discountType'),
  179.                             (float)$lineItem->getPayloadValue('value'),
  180.                             $discount,
  181.                             (float)$lineItem->getPayloadValue('maxValue'),
  182.                             $lineItem->getPayloadValue('code'),
  183.                             $lineItem->getLabel(),
  184.                             $quantity
  185.                         ));
  186.                         continue 2;
  187.                     }
  188.                 }
  189.             }
  190.         }
  191.         if ($lineItemPromotionCollection->count() > 0) {
  192.             return $this->getCalculatedPriceNew($calculatedPrice$lineItemPromotionCollection$salesChannelContext);
  193.         }
  194.         return $calculatedPrice;
  195.     }
  196.     private function getCalculatedPriceNew(CalculatedPrice $calculatedPriceLineItemPromotionCollection $lineItemPromotionCollectionSalesChannelContext $salesChannelContext): CalculatedPrice
  197.     {
  198.         if ($calculatedPrice->getListPrice()) {
  199.             $totalListPrice $calculatedPrice->getListPrice()->getPrice();
  200.         } else {
  201.             $totalListPrice $calculatedPrice->getUnitPrice();
  202.         }
  203.         if ($this->rrpPriceCalculatorService != null && $this->rrpPriceCalculatorService->isRrpActive($calculatedPrice$salesChannelContext)) {
  204.             [$totalDiscountPrice$unitDiscountPrice] = $this->rrpPriceCalculatorService->calculateRrpBasedDiscount($calculatedPrice$lineItemPromotionCollection$salesChannelContext);
  205.         } else {
  206.             $totalDiscountPrice $calculatedPrice->getTotalPrice();
  207.             $unitDiscountPrice $calculatedPrice->getUnitPrice();
  208.             /** @var LineItemPromotion $lineItemPromotion */
  209.             foreach ($lineItemPromotionCollection as $lineItemPromotion) {
  210.                 $totalDiscountPrice $totalDiscountPrice $lineItemPromotion->getDiscount();
  211.                 $unitDiscountPrice $unitDiscountPrice - ($lineItemPromotion->getDiscount() / $lineItemPromotion->getQuantity());
  212.             }
  213.         }
  214.         $newCalculatedPrice = new CalculatedPrice(
  215.             $unitDiscountPrice,
  216.             $totalDiscountPrice,
  217.             $calculatedPrice->getCalculatedTaxes(),
  218.             $calculatedPrice->getTaxRules(),
  219.             $calculatedPrice->getQuantity(),
  220.             $this->calculateReferencePriceByReferencePrice($unitDiscountPrice$calculatedPrice->getReferencePrice(), $salesChannelContext->getItemRounding()),
  221.             ListPrice::createFromUnitPrice($unitDiscountPrice$totalListPrice),
  222.             $calculatedPrice->getRegulationPrice()
  223.         );
  224.         $newCalculatedPrice->addExtension(self::PRODUCT_ORG_UNIT_PRICE_EXTENSION_NAME, new OriginalUnitPrice($calculatedPrice->getUnitPrice()));
  225.         return $this->roundCalculatedPrice($newCalculatedPrice$salesChannelContext->getSalesChannelId());
  226.     }
  227.     private function isCalculatedListPriceInsideCalculatedPrices(CalculatedPrice $calculatedListPricePriceCollection $calculatedPricesCalculatedPrice $calculatedPriceSingle): bool
  228.     {
  229.         foreach ($calculatedPrices as $calculatedPrice) {
  230.             if ($calculatedListPrice->getUnitPrice() === $calculatedPrice->getUnitPrice()) {
  231.                 return true;
  232.             }
  233.         }
  234.         if ($calculatedPriceSingle) {
  235.             if ($calculatedListPrice->getUnitPrice() === $calculatedPriceSingle->getUnitPrice()) {
  236.                 return true;
  237.             }
  238.         }
  239.         return false;
  240.     }
  241.     private function processProductCalculatedCheapestPrice(CalculatedCheapestPrice $calculatedCheapestPriceSalesChannelProductEntity $productSalesChannelProductEntity $originalProductCartBehavior $behaviorSalesChannelContext $salesChannelContext): CalculatedCheapestPrice
  242.     {
  243.         $cheapestPriceNew CalculatedCheapestPrice::createFrom($this->processProductCalculatedPrice($calculatedCheapestPrice$product$originalProduct$behavior$salesChannelContext));
  244.         $cheapestPriceNew->setHasRange($product->getCalculatedCheapestPrice()->hasRange());
  245.         return $cheapestPriceNew;
  246.     }
  247.     private function roundDiscountIfNecessary(float $discountCashRoundingConfig $cashRoundingConfig): float
  248.     {
  249.         return $this->cashRounding->cashRound($discount$cashRoundingConfig);
  250.     }
  251.     /**
  252.      * Copied and adapted from GrossPriceCalculator and NetPriceCalculator
  253.      */
  254.     private function calculateReferencePriceByReferencePrice(float $price, ?ReferencePrice $referencePriceCashRoundingConfig $config): ?ReferencePrice
  255.     {
  256.         if (!$referencePrice instanceof ReferencePrice) {
  257.             return $referencePrice;
  258.         }
  259.         if ($referencePrice->getPurchaseUnit() <= || $referencePrice->getReferenceUnit() <= 0) {
  260.             return null;
  261.         }
  262.         $price $price $referencePrice->getPurchaseUnit() * $referencePrice->getReferenceUnit();
  263.         $price $this->cashRounding->mathRound($price$config);
  264.         return new ReferencePrice(
  265.             $price,
  266.             $referencePrice->getPurchaseUnit(),
  267.             $referencePrice->getReferenceUnit(),
  268.             $referencePrice->getUnitName()
  269.         );
  270.     }
  271.     private function roundCalculatedPrice(CalculatedPrice $calculatedPricestring $salesChannelId): CalculatedPrice
  272.     {
  273.         if ($listPrice $calculatedPrice->getListPrice()) {
  274.             $roundingType $this->systemConfigService->getString('AcrisPromotionCS.config.typeOfRounding'$salesChannelId);
  275.             $decimalPlaces intval($this->systemConfigService->get('AcrisPromotionCS.config.decimalPlaces'$salesChannelId));
  276.             $percentageRounded $this->priceRoundingService->round($listPrice->getPercentage(), $decimalPlaces$roundingType);
  277.             $listPriceNew = new AcrisListPrice($listPrice->getPrice(), $listPrice->getDiscount(), $percentageRounded);
  278.             $calculatedPrice = new CalculatedPrice(
  279.                 $calculatedPrice->getUnitPrice(),
  280.                 $calculatedPrice->getTotalPrice(),
  281.                 $calculatedPrice->getCalculatedTaxes(),
  282.                 $calculatedPrice->getTaxRules(),
  283.                 $calculatedPrice->getQuantity(),
  284.                 $calculatedPrice->getReferencePrice(),
  285.                 $listPriceNew,
  286.                 $calculatedPrice->getRegulationPrice()
  287.             );
  288.         }
  289.         return $calculatedPrice;
  290.     }
  291. }