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

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