<?php declare(strict_types=1);
namespace Acris\Promotion\Subscriber;
use Acris\Promotion\Component\PriceRoundingService;
use Acris\Promotion\Core\Checkout\Cart\Price\Struct\AcrisListPrice;
use Acris\Promotion\Core\Checkout\Cart\Price\Struct\OriginalUnitPrice;
use Acris\Promotion\Core\Checkout\Cart\PromotionCartProcessor;
use Acris\Promotion\Core\Checkout\Promotion\LineItemPromotion;
use Acris\Promotion\Core\Checkout\Promotion\LineItemPromotionCollection;
use Acris\Rrp\Components\RrpPriceCalculatorService;
use Shopware\Core\Checkout\Cart\Cart;
use Shopware\Core\Checkout\Cart\CartBehavior;
use Shopware\Core\Checkout\Cart\LineItem\LineItem;
use Shopware\Core\Checkout\Cart\Price\CashRounding;
use Shopware\Core\Checkout\Cart\Price\Struct\CalculatedPrice;
use Shopware\Core\Checkout\Cart\Price\Struct\ListPrice;
use Shopware\Core\Checkout\Cart\Price\Struct\PriceCollection;
use Shopware\Core\Checkout\Cart\Price\Struct\ReferencePrice;
use Shopware\Core\Content\Product\Cart\ProductLineItemFactory;
use Shopware\Core\Content\Product\DataAbstractionLayer\CheapestPrice\CalculatedCheapestPrice;
use Shopware\Core\Content\Product\SalesChannel\SalesChannelProductEntity;
use Shopware\Core\Framework\DataAbstractionLayer\Pricing\CashRoundingConfig;
use Shopware\Core\Framework\Struct\ArrayEntity;
use Shopware\Core\Framework\Util\Random;
use Shopware\Core\System\SalesChannel\Entity\SalesChannelEntityLoadedEvent;
use Shopware\Core\System\SalesChannel\SalesChannelContext;
use Shopware\Core\System\SystemConfig\SystemConfigService;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class ProductLoadedSubscriber implements EventSubscriberInterface
{
const PRODUCT_PROMOTION_EXTENSION_NAME = 'acrisLineItemPromotionCollection';
const PRODUCT_ORG_UNIT_PRICE_EXTENSION_NAME = 'acrisPromotionOrgPrice';
const PRODUCT_PROMOTION_ID_EXTENSION_NAME = 'acrisPromotion';
/**
* @var ProductLineItemFactory
*/
private $productLineItemFactory;
/**
* @var PromotionCartProcessor
*/
private $promotionCartProcessor;
/**
* @var array|null
*/
private $promotionsAuto;
/**
* @var CashRounding
*/
private $cashRounding;
/**
* @var PriceRoundingService
*/
private $priceRoundingService;
/**
* @var SystemConfigService
*/
private $systemConfigService;
/**
* @var RrpPriceCalculatorService|null
*/
private $rrpPriceCalculatorService;
public function __construct(
ProductLineItemFactory $productLineItemFactory,
PromotionCartProcessor $promotionCartProcessor,
CashRounding $cashRounding,
PriceRoundingService $priceRoundingService,
SystemConfigService $systemConfigService,
?RrpPriceCalculatorService $rrpPriceCalculatorService
)
{
$this->productLineItemFactory = $productLineItemFactory;
$this->promotionCartProcessor = $promotionCartProcessor;
$this->promotionsAuto = null;
$this->cashRounding = $cashRounding;
$this->priceRoundingService = $priceRoundingService;
$this->systemConfigService = $systemConfigService;
$this->rrpPriceCalculatorService = $rrpPriceCalculatorService;
}
public static function getSubscribedEvents()
{
return [
'sales_channel.product.loaded' => ['loaded', -50],
];
}
public function loaded(SalesChannelEntityLoadedEvent $event): void
{
$salesChannelContext = $event->getSalesChannelContext();
if ($salesChannelContext->hasExtension('acrisProcessCart') === true) {
return;
}
$salesChannelContext->addExtension('acrisProcessCart', new ArrayEntity(['process' => true]));
$behavior = new CartBehavior($salesChannelContext->getPermissions());
/** @var SalesChannelProductEntity $product */
foreach ($event->getEntities() as $product) {
$originalProductToCalculate = clone $product;
// have cheapest price
if ($product->getCalculatedCheapestPrice()) {
$product->setCalculatedCheapestPrice($this->processProductCalculatedCheapestPrice($product->getCalculatedCheapestPrice(), $originalProductToCalculate, $product, $behavior, $salesChannelContext));
}
// have calculated prices
if ($product->getCalculatedPrices() && $product->getCalculatedPrices()->count() > 0) {
$calculatedPriceCollectionNew = new PriceCollection();
foreach ($product->getCalculatedPrices() as $calculatedPrice) {
$calculatedPriceCollectionNew->add($this->processProductCalculatedPrice($calculatedPrice, $originalProductToCalculate, $product, $behavior, $salesChannelContext));
}
$product->setCalculatedPrices($calculatedPriceCollectionNew);
}
if ($product->getCalculatedPrice()) {
$product->setCalculatedPrice($this->processProductCalculatedPrice($product->getCalculatedPrice(), $originalProductToCalculate, $product, $behavior, $salesChannelContext));
}
}
$salesChannelContext->removeExtension('acrisProcessCart');
}
private function processProductCalculatedPrice(CalculatedPrice $calculatedPrice, SalesChannelProductEntity $product, SalesChannelProductEntity $originalProduct, CartBehavior $behavior, SalesChannelContext $salesChannelContext): CalculatedPrice
{
$cart = new Cart($salesChannelContext->getSalesChannel()->getTypeId(), Random::getAlphanumericString(32));
$quantity = (int)$calculatedPrice->getQuantity();
if ($quantity < 1) {
$quantity = 1;
$calculatedPrice = new CalculatedPrice(
$calculatedPrice->getUnitPrice(),
$calculatedPrice->getUnitPrice(),
$calculatedPrice->getCalculatedTaxes(),
$calculatedPrice->getTaxRules(),
$quantity,
$calculatedPrice->getReferencePrice(),
$calculatedPrice->getListPrice(),
$calculatedPrice->getRegulationPrice()
);
}
$lineItem = $this->productLineItemFactory->create($product->getId(), ['quantity' => $quantity]);
$cart->add($lineItem);
// prevent calling of promotions from database duplicate times
if ($this->promotionsAuto !== null) {
$cart->getData()->set('promotions-auto', $this->promotionsAuto);
}
$cart = $this->promotionCartProcessor->process($cart, $salesChannelContext, $behavior, $product, $calculatedPrice);
// better compatibility with AcrisDiscountListPrice
if ($cart->getLineItems()->has($product->getId()) === true && $cart->getLineItems()->get($product->getId())->getPrice() instanceof CalculatedPrice) {
$calculatedPrice = $cart->getLineItems()->get($product->getId())->getPrice();
}
// save called automatic promotions for further use prevent calling again
$this->promotionsAuto = $cart->getData()->get('promotions-auto');
return $this->calculatePriceByPromotion($calculatedPrice, $quantity, $cart, $originalProduct, $salesChannelContext);
}
private function calculatePriceByPromotion(CalculatedPrice $calculatedPrice, int $quantity, Cart $cart, SalesChannelProductEntity $originalProduct, SalesChannelContext $salesChannelContext): CalculatedPrice
{
$lineItemPromotionCollection = new LineItemPromotionCollection();
foreach ($cart->getLineItems() as $lineItem) {
if ($lineItem->getType() === LineItem::PROMOTION_LINE_ITEM_TYPE) {
$composition = $lineItem->getPayloadValue('composition');
if ($composition === null) {
continue;
}
foreach ($composition as $lineItemDiscount) {
if (array_key_exists('id', $lineItemDiscount) === true && array_key_exists('discount', $lineItemDiscount) === true) {
if (!empty($lineItem->getPayloadValue('promotionId'))) {
if ($originalProduct->hasExtension(self::PRODUCT_PROMOTION_ID_EXTENSION_NAME) && $originalProduct->getExtension(self::PRODUCT_PROMOTION_ID_EXTENSION_NAME)->has('promotionIds')) {
$promotionIds = $originalProduct->getExtension(self::PRODUCT_PROMOTION_ID_EXTENSION_NAME)->get('promotionIds');
if (!in_array($lineItem->getPayloadValue('promotionId'), $promotionIds)) {
$promotionIds[] = $lineItem->getPayloadValue('promotionId');
$originalProduct->addExtension(self::PRODUCT_PROMOTION_ID_EXTENSION_NAME, new ArrayEntity([
'promotionIds' => $promotionIds
]));
}
} else {
$originalProduct->addExtension(self::PRODUCT_PROMOTION_ID_EXTENSION_NAME, new ArrayEntity([
'promotionIds' => [$lineItem->getPayloadValue('promotionId')]
]));
}
}
$discount = $lineItemDiscount['discount'];
if (!is_float($discount)) {
continue 2;
}
$discount = $this->roundDiscountIfNecessary($discount, $salesChannelContext->getItemRounding());
$lineItemPromotionCollection->add(new LineItemPromotion(
$lineItem->getPayloadValue('discountId'),
$lineItem->getPayloadValue('discountType'),
(float)$lineItem->getPayloadValue('value'),
$discount,
(float)$lineItem->getPayloadValue('maxValue'),
$lineItem->getPayloadValue('code'),
$lineItem->getLabel(),
$quantity
));
continue 2;
}
}
}
}
if ($lineItemPromotionCollection->count() > 0) {
return $this->getCalculatedPriceNew($calculatedPrice, $lineItemPromotionCollection, $salesChannelContext);
}
return $calculatedPrice;
}
private function getCalculatedPriceNew(CalculatedPrice $calculatedPrice, LineItemPromotionCollection $lineItemPromotionCollection, SalesChannelContext $salesChannelContext): CalculatedPrice
{
if ($calculatedPrice->getListPrice()) {
$totalListPrice = $calculatedPrice->getListPrice()->getPrice();
} else {
$totalListPrice = $calculatedPrice->getUnitPrice();
}
if ($this->rrpPriceCalculatorService != null && $this->rrpPriceCalculatorService->isRrpActive($calculatedPrice, $salesChannelContext)) {
[$totalDiscountPrice, $unitDiscountPrice] = $this->rrpPriceCalculatorService->calculateRrpBasedDiscount($calculatedPrice, $lineItemPromotionCollection, $salesChannelContext);
} else {
$totalDiscountPrice = $calculatedPrice->getTotalPrice();
$unitDiscountPrice = $calculatedPrice->getUnitPrice();
/** @var LineItemPromotion $lineItemPromotion */
foreach ($lineItemPromotionCollection as $lineItemPromotion) {
$totalDiscountPrice = $totalDiscountPrice - $lineItemPromotion->getDiscount();
$unitDiscountPrice = $unitDiscountPrice - ($lineItemPromotion->getDiscount() / $lineItemPromotion->getQuantity());
}
}
$newCalculatedPrice = new CalculatedPrice(
$unitDiscountPrice,
$totalDiscountPrice,
$calculatedPrice->getCalculatedTaxes(),
$calculatedPrice->getTaxRules(),
$calculatedPrice->getQuantity(),
$this->calculateReferencePriceByReferencePrice($unitDiscountPrice, $calculatedPrice->getReferencePrice(), $salesChannelContext->getItemRounding()),
ListPrice::createFromUnitPrice($unitDiscountPrice, $totalListPrice),
$calculatedPrice->getRegulationPrice()
);
$newCalculatedPrice->addExtension(self::PRODUCT_ORG_UNIT_PRICE_EXTENSION_NAME, new OriginalUnitPrice($calculatedPrice->getUnitPrice()));
return $this->roundCalculatedPrice($newCalculatedPrice, $salesChannelContext->getSalesChannelId());
}
private function isCalculatedListPriceInsideCalculatedPrices(CalculatedPrice $calculatedListPrice, PriceCollection $calculatedPrices, CalculatedPrice $calculatedPriceSingle): bool
{
foreach ($calculatedPrices as $calculatedPrice) {
if ($calculatedListPrice->getUnitPrice() === $calculatedPrice->getUnitPrice()) {
return true;
}
}
if ($calculatedPriceSingle) {
if ($calculatedListPrice->getUnitPrice() === $calculatedPriceSingle->getUnitPrice()) {
return true;
}
}
return false;
}
private function processProductCalculatedCheapestPrice(CalculatedCheapestPrice $calculatedCheapestPrice, SalesChannelProductEntity $product, SalesChannelProductEntity $originalProduct, CartBehavior $behavior, SalesChannelContext $salesChannelContext): CalculatedCheapestPrice
{
$cheapestPriceNew = CalculatedCheapestPrice::createFrom($this->processProductCalculatedPrice($calculatedCheapestPrice, $product, $originalProduct, $behavior, $salesChannelContext));
$cheapestPriceNew->setHasRange($product->getCalculatedCheapestPrice()->hasRange());
return $cheapestPriceNew;
}
private function roundDiscountIfNecessary(float $discount, CashRoundingConfig $cashRoundingConfig): float
{
return $this->cashRounding->cashRound($discount, $cashRoundingConfig);
}
/**
* Copied and adapted from GrossPriceCalculator and NetPriceCalculator
*/
private function calculateReferencePriceByReferencePrice(float $price, ?ReferencePrice $referencePrice, CashRoundingConfig $config): ?ReferencePrice
{
if (!$referencePrice instanceof ReferencePrice) {
return $referencePrice;
}
if ($referencePrice->getPurchaseUnit() <= 0 || $referencePrice->getReferenceUnit() <= 0) {
return null;
}
$price = $price / $referencePrice->getPurchaseUnit() * $referencePrice->getReferenceUnit();
$price = $this->cashRounding->mathRound($price, $config);
return new ReferencePrice(
$price,
$referencePrice->getPurchaseUnit(),
$referencePrice->getReferenceUnit(),
$referencePrice->getUnitName()
);
}
private function roundCalculatedPrice(CalculatedPrice $calculatedPrice, string $salesChannelId): CalculatedPrice
{
if ($listPrice = $calculatedPrice->getListPrice()) {
$roundingType = $this->systemConfigService->getString('AcrisPromotionCS.config.typeOfRounding', $salesChannelId);
$decimalPlaces = intval($this->systemConfigService->get('AcrisPromotionCS.config.decimalPlaces', $salesChannelId));
$percentageRounded = $this->priceRoundingService->round($listPrice->getPercentage(), $decimalPlaces, $roundingType);
$listPriceNew = new AcrisListPrice($listPrice->getPrice(), $listPrice->getDiscount(), $percentageRounded);
$calculatedPrice = new CalculatedPrice(
$calculatedPrice->getUnitPrice(),
$calculatedPrice->getTotalPrice(),
$calculatedPrice->getCalculatedTaxes(),
$calculatedPrice->getTaxRules(),
$calculatedPrice->getQuantity(),
$calculatedPrice->getReferencePrice(),
$listPriceNew,
$calculatedPrice->getRegulationPrice()
);
}
return $calculatedPrice;
}
}