<?php
declare(strict_types=1);
namespace GrimmTheme\Core\Storefront\Controller;
use Shopware\Storefront\Controller\CheckoutController;
use Shopware\Core\Checkout\Cart\Cart;
use Shopware\Core\Checkout\Cart\Error\Error;
use Shopware\Core\Checkout\Cart\Error\ErrorCollection;
use Shopware\Core\Checkout\Cart\Exception\InvalidCartException;
use Shopware\Core\Checkout\Cart\SalesChannel\CartService;
use Shopware\Core\Checkout\Customer\SalesChannel\AbstractLogoutRoute;
use Shopware\Core\Checkout\Order\Exception\EmptyCartException;
use Shopware\Core\Checkout\Order\SalesChannel\OrderService;
use Shopware\Core\Checkout\Payment\Exception\InvalidOrderException;
use Shopware\Core\Checkout\Payment\Exception\PaymentProcessException;
use Shopware\Core\Checkout\Payment\Exception\UnknownPaymentMethodException;
use Shopware\Core\Checkout\Payment\PaymentService;
use Shopware\Core\Framework\Feature;
use Shopware\Core\Framework\Routing\Annotation\RouteScope;
use Shopware\Core\Framework\Routing\Annotation\Since;
use Shopware\Core\Framework\Validation\DataBag\RequestDataBag;
use Shopware\Core\Framework\Validation\Exception\ConstraintViolationException;
use Shopware\Core\Profiling\Profiler;
use Shopware\Core\System\SalesChannel\SalesChannelContext;
use Shopware\Core\System\SystemConfig\SystemConfigService;
use Shopware\Storefront\Checkout\Cart\Error\PaymentMethodChangedError;
use Shopware\Storefront\Checkout\Cart\Error\ShippingMethodChangedError;
use Shopware\Storefront\Framework\AffiliateTracking\AffiliateTrackingListener;
use Shopware\Storefront\Framework\Routing\Annotation\NoStore;
use Shopware\Storefront\Page\Checkout\Cart\CheckoutCartPageLoadedHook;
use Shopware\Storefront\Page\Checkout\Cart\CheckoutCartPageLoader;
use Shopware\Storefront\Page\Checkout\Confirm\CheckoutConfirmPageLoadedHook;
use Shopware\Storefront\Page\Checkout\Confirm\CheckoutConfirmPageLoader;
use Shopware\Storefront\Page\Checkout\Finish\CheckoutFinishPageLoadedHook;
use Shopware\Storefront\Page\Checkout\Finish\CheckoutFinishPageLoader;
use Shopware\Storefront\Page\Checkout\Offcanvas\CheckoutInfoWidgetLoadedHook;
use Shopware\Storefront\Page\Checkout\Offcanvas\CheckoutOffcanvasWidgetLoadedHook;
use Shopware\Storefront\Page\Checkout\Offcanvas\OffcanvasCartPageLoader;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\Routing\Annotation\Route;
use Shopware\Storefront\Page\Address\Listing\AddressListingPageLoader;
use Shopware\Storefront\Page\Address\AddressEditorModalStruct;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsAnyFilter;
use Shopware\Core\System\SalesChannel\Entity\SalesChannelRepositoryInterface;
use Shopware\Core\Framework\DataAbstractionLayer\Search\EntitySearchResult;
use Shopware\Core\Content\Product\Aggregate\ProductCrossSelling\ProductCrossSellingEntity;
/**
* @RouteScope(scopes={"storefront"})
*/
class ExtendedCheckoutController extends CheckoutController {
private const REDIRECTED_FROM_SAME_ROUTE = 'redirected';
private CartService $cartService;
private CheckoutCartPageLoader $cartPageLoader;
private CheckoutConfirmPageLoader $confirmPageLoader;
private CheckoutFinishPageLoader $finishPageLoader;
private OrderService $orderService;
private PaymentService $paymentService;
private OffcanvasCartPageLoader $offcanvasCartPageLoader;
private SystemConfigService $config;
private AbstractLogoutRoute $logoutRoute;
private AddressListingPageLoader $addressListingPageLoader;
/**
* @var SalesChannelRepositoryInterface
*/
protected $productRepository;
public function __construct(
CartService $cartService,
CheckoutCartPageLoader $cartPageLoader,
CheckoutConfirmPageLoader $confirmPageLoader,
CheckoutFinishPageLoader $finishPageLoader,
OrderService $orderService,
PaymentService $paymentService,
OffcanvasCartPageLoader $offcanvasCartPageLoader,
SystemConfigService $config,
AbstractLogoutRoute $logoutRoute,
AddressListingPageLoader $addressListingPageLoader,
SalesChannelRepositoryInterface $productRepository
) {
$this->cartService = $cartService;
$this->cartPageLoader = $cartPageLoader;
$this->confirmPageLoader = $confirmPageLoader;
$this->finishPageLoader = $finishPageLoader;
$this->orderService = $orderService;
$this->paymentService = $paymentService;
$this->offcanvasCartPageLoader = $offcanvasCartPageLoader;
$this->config = $config;
$this->logoutRoute = $logoutRoute;
$this->addressListingPageLoader = $addressListingPageLoader;
$this->productRepository = $productRepository;
parent::__construct($cartService, $cartPageLoader, $confirmPageLoader,
$finishPageLoader, $orderService, $paymentService, $offcanvasCartPageLoader,
$config, $logoutRoute);
}
/**
* @Since("6.0.0.0")
* @NoStore
* @Route("/checkout/cart", name="frontend.checkout.cart.page", options={"seo"="false"}, methods={"GET"})
*/
public function cartPage(Request $request, SalesChannelContext $context): Response
{
$page = $this->cartPageLoader->load($request, $context);
$cart = $page->getCart();
$cartErrors = $cart->getErrors();
$this->hook(new CheckoutCartPageLoadedHook($page, $context));
$this->addCartErrors($cart);
if (!$request->query->getBoolean(self::REDIRECTED_FROM_SAME_ROUTE) && $this->routeNeedsReload($cartErrors)) {
$cartErrors->clear();
// To prevent redirect loops add the identifier that the request already got redirected from the same origin
return $this->redirectToRoute(
'frontend.checkout.cart.page',
\array_merge(
$request->query->all(),
[self::REDIRECTED_FROM_SAME_ROUTE => true]
),
);
}
$cartErrors->clear();
foreach ($cart->getLineItems() as $lineItem) {
$productId = $lineItem->getId();
$productEntity = $this->getProductById($productId, $context)->first();
if($productEntity == null) continue;
if(count($productEntity->getCrossSellings()) === 0) continue;
$crossSellings = $productEntity->getCrossSellings();
$crossSellings->sort(function (ProductCrossSellingEntity $crossSellingEntityA, ProductCrossSellingEntity $crossSellingEntityB) {
return $crossSellingEntityA->getPosition() <=> $crossSellingEntityB->getPosition();
});
if(count($crossSellings->first()->getAssignedProducts()) === 0) continue;
$assignedProductIds = [];
foreach($crossSellings->first()->getAssignedProducts() as $assignedProduct) {
$assignedProductIds[] = $assignedProduct->getProductId();
}
$resolvedProducts = $this->resolveAssignedProducts($assignedProductIds, $context);
$lineItem->resolvedProducts = $resolvedProducts;
}
return $this->renderStorefront('@Storefront/storefront/page/checkout/cart/index.html.twig', ['page' => $page]);
}
/**
* @Since("6.0.0.0")
* @Route("/checkout/offcanvas", name="frontend.cart.offcanvas", options={"seo"="false"}, methods={"GET"}, defaults={"XmlHttpRequest"=true})
*/
public function offcanvas(Request $request, SalesChannelContext $context): Response
{
$page = $this->offcanvasCartPageLoader->load($request, $context);
$this->hook(new CheckoutOffcanvasWidgetLoadedHook($page, $context));
$cart = $page->getCart();
$this->addCartErrors($cart);
$cartErrors = $cart->getErrors();
if (!$request->query->getBoolean(self::REDIRECTED_FROM_SAME_ROUTE) && $this->routeNeedsReload($cartErrors)) {
$cartErrors->clear();
// To prevent redirect loops add the identifier that the request already got redirected from the same origin
return $this->redirectToRoute(
'frontend.cart.offcanvas',
\array_merge(
$request->query->all(),
[self::REDIRECTED_FROM_SAME_ROUTE => true]
),
);
}
$cartErrors->clear();
// Add Crosssellings for newest Product
$resolvedProducts = [];
foreach ($cart->getLineItems() as $lineItem) {
$productId = $lineItem->getId();
$productEntity = $this->getProductById($productId, $context)->first();
if($productEntity == null) continue;
if(count($productEntity->getCrossSellings()) === 0) continue;
$crossSellings = $productEntity->getCrossSellings();
$crossSellings->sort(function (ProductCrossSellingEntity $crossSellingEntityA, ProductCrossSellingEntity $crossSellingEntityB) {
return $crossSellingEntityA->getPosition() <=> $crossSellingEntityB->getPosition();
});
if(count($crossSellings->first()->getAssignedProducts()) === 0) continue;
$assignedProductIds = [];
foreach($crossSellings->first()->getAssignedProducts() as $assignedProduct) {
$assignedProductIds[] = $assignedProduct->getProductId();
}
$assignedProducts = $this->resolveAssignedProducts($assignedProductIds, $context);
array_push($resolvedProducts, $assignedProducts);
}
$page->resolvedProducts = $resolvedProducts;
return $this->renderStorefront('@Storefront/storefront/component/checkout/offcanvas-cart.html.twig', ['page' => $page]);
}
/**
* @Route("/checkout/confirm", name="frontend.checkout.confirm.page", options={"seo"="false"}, methods={"GET"}, defaults={"XmlHttpRequest"=true})
*/
public function confirmPage(Request $request, SalesChannelContext $context): Response
{
if (!$context->getCustomer()) {
return $this->redirectToRoute('frontend.checkout.register.page');
}
if ($this->cartService->getCart($context->getToken(), $context)->getLineItems()->count() === 0) {
return $this->redirectToRoute('frontend.checkout.cart.page');
}
$page = $this->confirmPageLoader->load($request, $context);
$cart = $page->getCart();
$cartErrors = $cart->getErrors();
$this->hook(new CheckoutConfirmPageLoadedHook($page, $context));
$this->addCartErrors($cart);
if (!$request->query->getBoolean(self::REDIRECTED_FROM_SAME_ROUTE) && $this->routeNeedsReload($cartErrors)) {
$cartErrors->clear();
// To prevent redirect loops add the identifier that the request already got redirected from the same origin
return $this->redirectToRoute(
'frontend.checkout.confirm.page',
\array_merge(
$request->query->all(),
[self::REDIRECTED_FROM_SAME_ROUTE => true]
),
);
}
$cartErrors->clear();
$page->addresses = $this->addressListingPageLoader->load($request, $context, $context->getCustomer())->getAddresses();
$totalShippingCosts = 0;
$deliveries = $cart->getDeliveries();
foreach ($deliveries as $delivery) {
$totalShippingCosts += $delivery->getShippingCosts()->getTotalPrice();
}
$page->freeShipping = $totalShippingCosts == 0;
return $this->renderStorefront('@Storefront/storefront/page/checkout/confirm/index.html.twig', ['page' => $page]);
}
private function routeNeedsReload(ErrorCollection $cartErrors): bool
{
foreach ($cartErrors as $error) {
if ($error instanceof ShippingMethodChangedError || $error instanceof PaymentMethodChangedError) {
return true;
}
}
return false;
}
private function getProductById(String $productId, SalesChannelContext $context):EntitySearchResult
{
$criteria = (new Criteria())
->addFilter(new EqualsFilter('id', $productId))
->addAssociation('crossSellings')
->addAssociation('crossSellings.assignedProducts');
// EntitySearchResult
return $this->productRepository->search($criteria, $context);
}
private function resolveAssignedProducts(Array $productIds, SalesChannelContext $context):EntitySearchResult
{
$criteria = (new Criteria())
->addFilter(new EqualsAnyFilter('id', $productIds))
->addAssociation('deliveryTime')
->addAssociation('cover');
// EntitySearchResult
return $this->productRepository->search($criteria, $context);
}
}