<?php declare(strict_types=1);
namespace Shopware\Core\Checkout\Promotion\Subscriber\Storefront;
use Shopware\Core\Checkout\Cart\Cart;
use Shopware\Core\Checkout\Cart\Event\BeforeLineItemAddedEvent;
use Shopware\Core\Checkout\Cart\Event\BeforeLineItemRemovedEvent;
use Shopware\Core\Checkout\Cart\Event\CheckoutOrderPlacedEvent;
use Shopware\Core\Checkout\Cart\Exception\LineItemNotFoundException;
use Shopware\Core\Checkout\Cart\Exception\LineItemNotRemovableException;
use Shopware\Core\Checkout\Cart\Exception\PayloadKeyNotFoundException;
use Shopware\Core\Checkout\Cart\LineItem\LineItem;
use Shopware\Core\Checkout\Cart\SalesChannel\CartService;
use Shopware\Core\Checkout\Promotion\Aggregate\PromotionDiscount\PromotionDiscountEntity;
use Shopware\Core\Checkout\Promotion\Cart\Extension\CartExtension;
use Shopware\Core\Checkout\Promotion\Cart\PromotionProcessor;
use Shopware\Core\System\SalesChannel\SalesChannelContext;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\RequestStack;
class StorefrontCartSubscriber implements EventSubscriberInterface
{
public const SESSION_KEY_PROMOTION_CODES = 'cart-promotion-codes';
private CartService $cartService;
private RequestStack $requestStack;
/**
* @internal
*/
public function __construct(CartService $cartService, RequestStack $requestStack)
{
$this->cartService = $cartService;
$this->requestStack = $requestStack;
}
public static function getSubscribedEvents(): array
{
return [
BeforeLineItemAddedEvent::class => 'onLineItemAdded',
BeforeLineItemRemovedEvent::class => 'onLineItemRemoved',
CheckoutOrderPlacedEvent::class => 'resetCodes',
];
}
public function resetCodes(): void
{
$mainRequest = $this->requestStack->getMainRequest();
if ($mainRequest === null) {
return;
}
if (!$mainRequest->hasSession()) {
return;
}
$mainRequest->getSession()->set(self::SESSION_KEY_PROMOTION_CODES, []);
}
/**
* This function is called whenever a new line item has been
* added to the cart from within the controllers.
* We verify if we have a placeholder line item for a promotion
* and add that code to our extension list.
*/
public function onLineItemAdded(BeforeLineItemAddedEvent $event): void
{
if ($event->getLineItem()->getType() === PromotionProcessor::LINE_ITEM_TYPE) {
$code = $event->getLineItem()->getReferencedId();
if ($code !== null && $code !== '') {
$this->addCode($code, $event->getCart());
}
}
}
/**
* This function is called whenever a line item is being removed
* from the cart from within a controller.
* We verify if it is a promotion item, and also remove that
* code from our extension, if existing.
*/
public function onLineItemRemoved(BeforeLineItemRemovedEvent $event): void
{
$cart = $event->getCart();
if ($event->getLineItem()->getType() !== PromotionProcessor::LINE_ITEM_TYPE) {
return;
}
$lineItem = $event->getLineItem();
$code = $lineItem->getReferencedId();
if (!empty($code)) {
// promotion with code
$this->checkFixedDiscountItems($cart, $lineItem);
//remove other discounts of the promotion that should be deleted
$this->removeOtherDiscountsOfPromotion($cart, $lineItem, $event->getSalesChannelContext());
$this->removeCode($code, $cart);
return;
}
// the user wants to remove an automatic added
// promotions, so lets do this
if ($lineItem->hasPayloadValue('promotionId')) {
$promotionId = (string) $lineItem->getPayloadValue('promotionId');
$this->blockPromotion($promotionId, $cart);
}
}
/**
* @throws LineItemNotFoundException
* @throws LineItemNotRemovableException
* @throws PayloadKeyNotFoundException
*/
private function checkFixedDiscountItems(Cart $cart, LineItem $lineItem): void
{
$lineItems = $cart->getLineItems()->filterType(PromotionProcessor::LINE_ITEM_TYPE);
if ($lineItems->count() < 1) {
return;
}
if (!$lineItem->hasPayloadValue('discountType')) {
return;
}
if ($lineItem->getPayloadValue('discountType') !== PromotionDiscountEntity::TYPE_FIXED_UNIT) {
return;
}
if (!$lineItem->hasPayloadValue('discountId')) {
return;
}
$discountId = $lineItem->getPayloadValue('discountId');
$removeThisDiscounts = $lineItems->filter(static function (LineItem $lineItem) use ($discountId) {
return $lineItem->hasPayloadValue('discountId') && $lineItem->getPayloadValue('discountId') === $discountId;
});
foreach ($removeThisDiscounts as $discountItem) {
$cart->remove($discountItem->getId());
}
}
private function removeOtherDiscountsOfPromotion(Cart $cart, LineItem $lineItem, SalesChannelContext $context): void
{
// ge all promotions from cart
$lineItems = $cart->getLineItems()->filterType(PromotionProcessor::LINE_ITEM_TYPE);
if ($lineItems->count() < 1) {
return;
}
//filter them by the promotion which discounts should be deleted
$lineItems = $lineItems->filter(function (LineItem $promotionLineItem) use ($lineItem) {
return $promotionLineItem->getPayloadValue('promotionId') === $lineItem->getPayloadValue('promotionId');
});
if ($lineItems->count() < 1) {
return;
}
$promotionLineItem = $lineItems->first();
if ($promotionLineItem instanceof LineItem) {
// this is recursive because we are listening on LineItemRemovedEvent, it will stop if there
// are no discounts in the cart, that belong to the promotion that should be deleted
$this->cartService->remove($cart, $promotionLineItem->getId(), $context);
}
}
private function addCode(string $code, Cart $cart): void
{
$extension = $this->getExtension($cart);
$extension->addCode($code);
$cart->addExtension(CartExtension::KEY, $extension);
}
private function removeCode(string $code, Cart $cart): void
{
$extension = $this->getExtension($cart);
$extension->removeCode($code);
$cart->addExtension(CartExtension::KEY, $extension);
}
private function blockPromotion(string $id, Cart $cart): void
{
$extension = $this->getExtension($cart);
$extension->blockPromotion($id);
$cart->addExtension(CartExtension::KEY, $extension);
}
private function getExtension(Cart $cart): CartExtension
{
if (!$cart->hasExtension(CartExtension::KEY)) {
$cart->addExtension(CartExtension::KEY, new CartExtension());
}
/** @var CartExtension $extension */
$extension = $cart->getExtension(CartExtension::KEY);
return $extension;
}
}