<?php declare(strict_types=1);
namespace Wexo\Shipping\Subscriber;
use Shopware\Core\Checkout\Cart\Error\ErrorCollection;
use Shopware\Core\Checkout\Shipping\Aggregate\ShippingMethodPrice\ShippingMethodPriceCollection;
use Shopware\Core\Checkout\Shipping\ShippingMethodEntity;
use Shopware\Core\Framework\Uuid\Uuid;
use Shopware\Core\System\SystemConfig\SystemConfigService;
use Shopware\Storefront\Event\RouteRequest\ShippingMethodRouteRequestEvent;
use Shopware\Storefront\Page\Checkout\Confirm\CheckoutConfirmPageLoadedEvent;
use Symfony\Component\Asset\PackageInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use Wexo\Shipping\Core\Content\Cart\CartExtension;
use Wexo\Shipping\Core\Content\ShippingMethodDeadline\ShippingMethodDeadlineEntity;
use Wexo\Shipping\Core\Content\ShippingMethodType\ShippingMethodTypeEntity;
use Wexo\Shipping\Service\ShippingDeadlineFormatter;
use Wexo\Shipping\Service\ShippingPriceCalculator;
use Wexo\Shipping\Struct\ParcelShopConfiguration;
use Wexo\Shipping\Service\TypeOptionService;
use Wexo\Shipping\Struct\ShippingPriceRange;
/**
* Class Checkout
* @package Wexo\Shipping\Subscriber
*/
class Checkout implements EventSubscriberInterface
{
const UNIFIED_VIEW_TYPE_KEY = 'unified';
private TypeOptionService $typeOptionService;
private SystemConfigService $systemConfigService;
private TranslatorInterface $translator;
private ShippingPriceCalculator $shippingPriceCalculator;
private ShippingDeadlineFormatter $shippingDeadlineFormatter;
private PackageInterface $package;
/**
* Checkout constructor.
* @param TypeOptionService $typeOptionService
* @param SystemConfigService $systemConfigService
* @param TranslatorInterface $translator
* @param ShippingPriceCalculator $shippingPriceCalculator
* @param ShippingDeadlineFormatter $shippingDeadlineFormatter
* @param PackageInterface $package
*/
public function __construct(
TypeOptionService $typeOptionService,
SystemConfigService $systemConfigService,
TranslatorInterface $translator,
ShippingPriceCalculator $shippingPriceCalculator,
ShippingDeadlineFormatter $shippingDeadlineFormatter,
PackageInterface $package
) {
$this->typeOptionService = $typeOptionService;
$this->systemConfigService = $systemConfigService;
$this->translator = $translator;
$this->shippingPriceCalculator = $shippingPriceCalculator;
$this->shippingDeadlineFormatter = $shippingDeadlineFormatter;
$this->package = $package;
}
/**
* @return string[]
*/
public static function getSubscribedEvents()
{
return [
CheckoutConfirmPageLoadedEvent::class => 'addShippingMethodData',
ShippingMethodRouteRequestEvent::class => 'addShippingMethodType'
];
}
/**
* @param ShippingMethodRouteRequestEvent $event
*/
public function addShippingMethodType(ShippingMethodRouteRequestEvent $event): void
{
$event->getCriteria()->addAssociation('shippingMethodType');
$event->getCriteria()->addAssociation('prices');
}
/**
* @param CheckoutConfirmPageLoadedEvent $event
*/
public function addShippingMethodData(CheckoutConfirmPageLoadedEvent $event): void
{
$this->addShippingMethodDeadlines($event);
$this->addShippingMethodTypeConfiguration($event);
}
/**
* @param CheckoutConfirmPageLoadedEvent $event
*/
public function addShippingMethodDeadlines(CheckoutConfirmPageLoadedEvent $event): void
{
foreach ($event->getPage()->getShippingMethods() as $shippingMethod) {
if ($shippingMethod->hasExtension('shippingMethodDeadline')) {
/** @var ShippingMethodDeadlineEntity $deadline */
$deadline = $shippingMethod->getExtension('shippingMethodDeadline');
$formattedDeadline = $this->shippingDeadlineFormatter->getFormattedDeadline($deadline);
$shippingMethod->addExtension('formattedDeadline', $formattedDeadline);
}
}
}
/**
* @param CheckoutConfirmPageLoadedEvent $event
*/
public function addShippingMethodTypeConfiguration(CheckoutConfirmPageLoadedEvent $event): void
{
$shippingMethods = $event->getPage()->getSalesChannelShippingMethods();
$shippingAddress = $event->getSalesChannelContext()->getCustomer()->getActiveShippingAddress();
$parcelShopConfigurations = [];
$cart = $event->getPage()->getCart();
$cartErrors = $cart->getErrors();
$cart->setErrors(new ErrorCollection);
$clonedCart = clone $cart;
foreach ($shippingMethods as $shippingMethod) {
if (!$event->getPage()->getShippingMethods()->get($shippingMethod->getId())) {
continue;
}
$shippingCosts = $this->shippingPriceCalculator->getShippingCostsForCart(
$clonedCart,
$shippingMethod,
$this->shippingPriceCalculator
->createSalesChannelContextWithShippingMethod($event->getSalesChannelContext(), $shippingMethod)
);
if ($shippingCosts) {
$pageShippingMethod = $event->getPage()->getShippingMethods()->get($shippingMethod->getId());
if ($pageShippingMethod) {
$pageShippingMethod->addExtension('calculatedPrice', $shippingCosts);
}
}
$shippingMethodType = $shippingMethod->getExtension('shippingMethodType');
if (!$shippingMethodType) {
continue;
}
$typeKey = $shippingMethodType->getTypeKey();
$optionType = $this->typeOptionService->getType($typeKey);
$parcelShopConfigurations[$typeKey] = [
'shippingMethodId' => $shippingMethod->getId(),
'typeKey' => $typeKey,
'street' => $shippingAddress->getStreet(),
'zipcode' => $shippingAddress->getZipcode(),
'countryCode' => $shippingAddress->getCountry()->getIso(),
'amount' => $this->getNumberOfShopsToLoad($event->getSalesChannelContext()->getSalesChannel()
->getId()),
'images' => [
$typeKey => $this->package->getUrl($optionType ? $optionType->getLogoUrl() : '')
],
'shippingCosts' => [
$typeKey => [
'total' => $shippingCosts->getTotalPrice(),
'currency' => $event->getSalesChannelContext()->getCurrency()->getSymbol()
]
],
'translations' => $this->getTranslations()
];
}
$selectedParcelShop = null;
if ($event->getPage()->getCart()->hasExtension(CartExtension::KEY)) {
$selectedParcelShop = $event->getPage()->getCart()->getExtension(CartExtension::KEY)->parcelShop;
}
$salesChannelId = $event->getSalesChannelContext()->getSalesChannel()->getId();
if (!empty($parcelShopConfigurations) && $this->isUnifiedView($salesChannelId)) {
$this->addUnifiedShippingMethodConfiguration($event, $parcelShopConfigurations);
}
$event->getPage()->addExtension(
'shipping_method_type_configuration',
new ParcelShopConfiguration($parcelShopConfigurations, $selectedParcelShop)
);
$cart->setErrors($cartErrors);
}
/**
* Create a "fake" shipping method to add to the checkout
* @param CheckoutConfirmPageLoadedEvent $event
* @param $parcelShopConfigurations
*/
protected function addUnifiedShippingMethodConfiguration(
CheckoutConfirmPageLoadedEvent $event,
&$parcelShopConfigurations
): void {
$salesChannelId = $event->getSalesChannelContext()->getSalesChannel()->getId();
$unifiedShippingMethod = new ShippingMethodEntity(new ShippingMethodPriceCollection());
$unifiedShippingMethod->setUniqueIdentifier(Uuid::randomHex());
$unifiedShippingMethod->setId(Uuid::randomHex());
$unifiedShippingMethod->setTranslated(['name' => $this->getUnifiedViewName($salesChannelId)]);
$unifiedShippingMethod->setName($this->getUnifiedViewName($salesChannelId));
$unifiedShippingMethod->setActive(true);
$unifiedShippingMethodTypeEntity = new ShippingMethodTypeEntity();
$unifiedShippingMethodTypeEntity->setShippingMethod($unifiedShippingMethod);
$unifiedShippingMethodTypeEntity->setTypeKey(self::UNIFIED_VIEW_TYPE_KEY);
$unifiedShippingMethod->setExtensions(['shippingMethodType' => $unifiedShippingMethodTypeEntity]);
$shippingMethodIds = [];
$images = [];
$shippingCosts = [];
$minPrice = null;
$maxPrice = null;
foreach ($parcelShopConfigurations as $typeKey => $configuration) {
$shippingMethodIds[$typeKey] = $configuration['shippingMethodId'];
$images[$typeKey] = $configuration['images'][$typeKey];
$shippingCosts[$typeKey] = $configuration['shippingCosts'][$typeKey];
if (!isset($minPrice)) {
$minPrice = $shippingCosts[$typeKey]['total'];
$maxPrice = $shippingCosts[$typeKey]['total'];
continue;
}
$minPrice = min($minPrice, $shippingCosts[$typeKey]['total']);
$maxPrice = max($maxPrice, $shippingCosts[$typeKey]['total']);
}
$shippingPriceRange = new ShippingPriceRange($minPrice, $maxPrice);
$unifiedShippingMethod->addExtension('shippingPriceRange', $shippingPriceRange);
$event->getPage()->addExtension('unified_parcel_shop_method', $unifiedShippingMethod);
$shippingAddress = $event->getSalesChannelContext()->getCustomer()->getActiveShippingAddress();
$parcelShopConfigurations = [
self::UNIFIED_VIEW_TYPE_KEY => [
'shippingMethodIds' => $shippingMethodIds,
'typeKey' => self::UNIFIED_VIEW_TYPE_KEY,
'street' => $shippingAddress->getStreet(),
'zipcode' => $shippingAddress->getZipcode(),
'countryCode' => $shippingAddress->getCountry()->getIso(),
'amount' => $this->getNumberOfShopsToLoad($event->getSalesChannelContext()->getSalesChannel()->getId()),
'images' => $images,
'shippingCosts' => $shippingCosts,
'translations' => $this->getTranslations()
]
];
}
/**
* @param string|null $salesChannelId
* @return int
*/
protected function getNumberOfShopsToLoad(?string $salesChannelId): int
{
return $this->systemConfigService->getInt(
'WexoShipping.config.numberOfShopsToLoad',
$salesChannelId
);
}
/**
* @param string|null $salesChannelId
* @return bool
*/
protected function isUnifiedView(?string $salesChannelId): bool
{
return $this->systemConfigService->getBool(
'WexoShipping.config.unifiedView',
$salesChannelId
);
}
/**
* @param string|null $salesChannelId
* @return string
*/
protected function getUnifiedViewName(?string $salesChannelId): string
{
return $this->systemConfigService->getString(
'WexoShipping.config.unifiedViewName',
$salesChannelId
);
}
/**
* @return array
*/
protected function getTranslations(): array
{
return [
'select' => $this->translator->trans('shippingMethod.select'),
'selected' => $this->translator->trans('shippingMethod.selected'),
'showOnMap' => $this->translator->trans('shippingMethod.showOnMap'),
'freeLabel' => $this->translator->trans('labels.free'),
'validation' => [
'parcelShopMissing' => $this->translator->trans('shippingMethod.validation.parcelShopMissing'),
'shippingCommentMissing' => $this->translator->trans('shippingMethod.validation.shippingCommentMissing')
]
];
}
}