Passed
Push — trunk ( 1348b1...1ff154 )
by Christian
13:01 queued 13s
created

CheckoutController::cartJson()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 2
dl 0
loc 4
rs 10
c 0
b 0
f 0
1
<?php declare(strict_types=1);
2
3
namespace Shopware\Storefront\Controller;
4
5
use Shopware\Core\Checkout\Cart\Error\Error;
6
use Shopware\Core\Checkout\Cart\Error\ErrorCollection;
7
use Shopware\Core\Checkout\Cart\Exception\InvalidCartException;
8
use Shopware\Core\Checkout\Cart\SalesChannel\CartLoadRoute;
9
use Shopware\Core\Checkout\Cart\SalesChannel\CartService;
10
use Shopware\Core\Checkout\Customer\SalesChannel\AbstractLogoutRoute;
11
use Shopware\Core\Checkout\Order\Exception\EmptyCartException;
12
use Shopware\Core\Checkout\Order\SalesChannel\OrderService;
13
use Shopware\Core\Checkout\Payment\Exception\InvalidOrderException;
14
use Shopware\Core\Checkout\Payment\Exception\PaymentProcessException;
15
use Shopware\Core\Checkout\Payment\Exception\UnknownPaymentMethodException;
16
use Shopware\Core\Checkout\Payment\PaymentService;
17
use Shopware\Core\Framework\Log\Package;
18
use Shopware\Core\Framework\Validation\DataBag\RequestDataBag;
19
use Shopware\Core\Framework\Validation\Exception\ConstraintViolationException;
20
use Shopware\Core\Profiling\Profiler;
21
use Shopware\Core\System\SalesChannel\SalesChannelContext;
22
use Shopware\Core\System\SystemConfig\SystemConfigService;
23
use Shopware\Storefront\Checkout\Cart\Error\PaymentMethodChangedError;
24
use Shopware\Storefront\Checkout\Cart\Error\ShippingMethodChangedError;
25
use Shopware\Storefront\Framework\AffiliateTracking\AffiliateTrackingListener;
26
use Shopware\Storefront\Page\Checkout\Cart\CheckoutCartPageLoadedHook;
27
use Shopware\Storefront\Page\Checkout\Cart\CheckoutCartPageLoader;
28
use Shopware\Storefront\Page\Checkout\Confirm\CheckoutConfirmPageLoadedHook;
29
use Shopware\Storefront\Page\Checkout\Confirm\CheckoutConfirmPageLoader;
30
use Shopware\Storefront\Page\Checkout\Finish\CheckoutFinishPageLoadedHook;
31
use Shopware\Storefront\Page\Checkout\Finish\CheckoutFinishPageLoader;
32
use Shopware\Storefront\Page\Checkout\Offcanvas\CheckoutInfoWidgetLoadedHook;
33
use Shopware\Storefront\Page\Checkout\Offcanvas\CheckoutOffcanvasWidgetLoadedHook;
34
use Shopware\Storefront\Page\Checkout\Offcanvas\OffcanvasCartPageLoader;
35
use Symfony\Component\HttpFoundation\RedirectResponse;
36
use Symfony\Component\HttpFoundation\Request;
37
use Symfony\Component\HttpFoundation\Response;
38
use Symfony\Component\HttpFoundation\Session\SessionInterface;
39
use Symfony\Component\Routing\Annotation\Route;
40
41
/**
42
 * @internal
43
 * Do not use direct or indirect repository calls in a controller. Always use a store-api route to get or put datas
44
 */
45
#[Route(defaults: ['_routeScope' => ['storefront']])]
46
#[Package('storefront')]
47
class CheckoutController extends StorefrontController
48
{
49
    private const REDIRECTED_FROM_SAME_ROUTE = 'redirected';
50
51
    /**
52
     * @internal
53
     */
54
    public function __construct(
55
        private readonly CartService $cartService,
56
        private readonly CheckoutCartPageLoader $cartPageLoader,
57
        private readonly CheckoutConfirmPageLoader $confirmPageLoader,
58
        private readonly CheckoutFinishPageLoader $finishPageLoader,
59
        private readonly OrderService $orderService,
60
        private readonly PaymentService $paymentService,
61
        private readonly OffcanvasCartPageLoader $offcanvasCartPageLoader,
62
        private readonly SystemConfigService $config,
63
        private readonly AbstractLogoutRoute $logoutRoute,
64
        private readonly CartLoadRoute $cartLoadRoute
65
    ) {
66
    }
67
68
    #[Route(path: '/checkout/cart', name: 'frontend.checkout.cart.page', options: ['seo' => false], defaults: ['_noStore' => true], methods: ['GET'])]
69
    public function cartPage(Request $request, SalesChannelContext $context): Response
70
    {
71
        $page = $this->cartPageLoader->load($request, $context);
72
        $cart = $page->getCart();
73
        $cartErrors = $cart->getErrors();
74
75
        $this->hook(new CheckoutCartPageLoadedHook($page, $context));
76
77
        $this->addCartErrors($cart);
78
79
        if (!$request->query->getBoolean(self::REDIRECTED_FROM_SAME_ROUTE) && $this->routeNeedsReload($cartErrors)) {
80
            $cartErrors->clear();
81
82
            // To prevent redirect loops add the identifier that the request already got redirected from the same origin
83
            return $this->redirectToRoute(
84
                'frontend.checkout.cart.page',
85
                [...$request->query->all(), ...[self::REDIRECTED_FROM_SAME_ROUTE => true]],
86
            );
87
        }
88
        $cartErrors->clear();
89
90
        return $this->renderStorefront('@Storefront/storefront/page/checkout/cart/index.html.twig', ['page' => $page]);
91
    }
92
93
    #[Route(path: '/checkout/cart.json', name: 'frontend.checkout.cart.json', methods: ['GET'], options: ['seo' => false], defaults: ['XmlHttpRequest' => true])]
94
    public function cartJson(Request $request, SalesChannelContext $context): Response
95
    {
96
        return $this->cartLoadRoute->load($request, $context);
97
    }
98
99
    #[Route(path: '/checkout/confirm', name: 'frontend.checkout.confirm.page', options: ['seo' => false], defaults: ['XmlHttpRequest' => true, '_noStore' => true], methods: ['GET'])]
100
    public function confirmPage(Request $request, SalesChannelContext $context): Response
101
    {
102
        if (!$context->getCustomer()) {
103
            return $this->redirectToRoute('frontend.checkout.register.page');
104
        }
105
106
        if ($this->cartService->getCart($context->getToken(), $context)->getLineItems()->count() === 0) {
107
            return $this->redirectToRoute('frontend.checkout.cart.page');
108
        }
109
110
        $page = $this->confirmPageLoader->load($request, $context);
111
        $cart = $page->getCart();
112
        $cartErrors = $cart->getErrors();
113
114
        $this->hook(new CheckoutConfirmPageLoadedHook($page, $context));
115
116
        $this->addCartErrors($cart);
117
118
        if (!$request->query->getBoolean(self::REDIRECTED_FROM_SAME_ROUTE) && $this->routeNeedsReload($cartErrors)) {
119
            $cartErrors->clear();
120
121
            // To prevent redirect loops add the identifier that the request already got redirected from the same origin
122
            return $this->redirectToRoute(
123
                'frontend.checkout.confirm.page',
124
                [...$request->query->all(), ...[self::REDIRECTED_FROM_SAME_ROUTE => true]],
125
            );
126
        }
127
128
        return $this->renderStorefront('@Storefront/storefront/page/checkout/confirm/index.html.twig', ['page' => $page]);
129
    }
130
131
    #[Route(path: '/checkout/finish', name: 'frontend.checkout.finish.page', options: ['seo' => false], defaults: ['_noStore' => true], methods: ['GET'])]
132
    public function finishPage(Request $request, SalesChannelContext $context, RequestDataBag $dataBag): Response
133
    {
134
        if ($context->getCustomer() === null) {
135
            return $this->redirectToRoute('frontend.checkout.register.page');
136
        }
137
138
        $page = $this->finishPageLoader->load($request, $context);
139
140
        $this->hook(new CheckoutFinishPageLoadedHook($page, $context));
141
142
        if ($page->isPaymentFailed() === true) {
143
            return $this->redirectToRoute(
144
                'frontend.account.edit-order.page',
145
                [
146
                    'orderId' => $request->get('orderId'),
147
                    'error-code' => 'CHECKOUT__UNKNOWN_ERROR',
148
                ]
149
            );
150
        }
151
152
        if ($context->getCustomer()->getGuest() && $this->config->get('core.cart.logoutGuestAfterCheckout', $context->getSalesChannelId())) {
153
            $this->logoutRoute->logout($context, $dataBag);
154
        }
155
156
        return $this->renderStorefront('@Storefront/storefront/page/checkout/finish/index.html.twig', ['page' => $page]);
157
    }
158
159
    #[Route(path: '/checkout/order', name: 'frontend.checkout.finish.order', options: ['seo' => false], methods: ['POST'])]
160
    public function order(RequestDataBag $data, SalesChannelContext $context, Request $request): Response
161
    {
162
        if (!$context->getCustomer()) {
163
            return $this->redirectToRoute('frontend.checkout.register.page');
164
        }
165
166
        try {
167
            $this->addAffiliateTracking($data, $request->getSession());
168
169
            $orderId = Profiler::trace('checkout-order', fn () => $this->orderService->createOrder($data, $context));
170
        } catch (ConstraintViolationException $formViolations) {
171
            return $this->forwardToRoute('frontend.checkout.confirm.page', ['formViolations' => $formViolations]);
172
        } catch (InvalidCartException | Error | EmptyCartException) {
173
            $this->addCartErrors(
174
                $this->cartService->getCart($context->getToken(), $context)
175
            );
176
177
            return $this->forwardToRoute('frontend.checkout.confirm.page');
178
        }
179
180
        try {
181
            $finishUrl = $this->generateUrl('frontend.checkout.finish.page', ['orderId' => $orderId]);
182
            $errorUrl = $this->generateUrl('frontend.account.edit-order.page', ['orderId' => $orderId]);
183
184
            $response = Profiler::trace('handle-payment', fn (): ?RedirectResponse => $this->paymentService->handlePaymentByOrder($orderId, $data, $context, $finishUrl, $errorUrl));
185
186
            return $response ?? new RedirectResponse($finishUrl);
187
        } catch (PaymentProcessException | InvalidOrderException | UnknownPaymentMethodException) {
188
            return $this->forwardToRoute('frontend.checkout.finish.page', ['orderId' => $orderId, 'changedPayment' => false, 'paymentFailed' => true]);
189
        }
190
    }
191
192
    #[Route(path: '/widgets/checkout/info', name: 'frontend.checkout.info', defaults: ['XmlHttpRequest' => true], methods: ['GET'])]
193
    public function info(Request $request, SalesChannelContext $context): Response
194
    {
195
        $cart = $this->cartService->getCart($context->getToken(), $context);
196
        if ($cart->getLineItems()->count() <= 0) {
197
            return new Response(null, Response::HTTP_NO_CONTENT);
198
        }
199
200
        $page = $this->offcanvasCartPageLoader->load($request, $context);
201
202
        $this->hook(new CheckoutInfoWidgetLoadedHook($page, $context));
203
204
        $response = $this->renderStorefront('@Storefront/storefront/layout/header/actions/cart-widget.html.twig', ['page' => $page]);
205
        $response->headers->set('x-robots-tag', 'noindex');
206
207
        return $response;
208
    }
209
210
    #[Route(path: '/checkout/offcanvas', name: 'frontend.cart.offcanvas', options: ['seo' => false], defaults: ['XmlHttpRequest' => true], methods: ['GET'])]
211
    public function offcanvas(Request $request, SalesChannelContext $context): Response
212
    {
213
        $page = $this->offcanvasCartPageLoader->load($request, $context);
214
215
        $this->hook(new CheckoutOffcanvasWidgetLoadedHook($page, $context));
216
217
        $cart = $page->getCart();
218
        $this->addCartErrors($cart);
219
        $cartErrors = $cart->getErrors();
220
221
        if (!$request->query->getBoolean(self::REDIRECTED_FROM_SAME_ROUTE) && $this->routeNeedsReload($cartErrors)) {
222
            $cartErrors->clear();
223
224
            // To prevent redirect loops add the identifier that the request already got redirected from the same origin
225
            return $this->redirectToRoute(
226
                'frontend.cart.offcanvas',
227
                [...$request->query->all(), ...[self::REDIRECTED_FROM_SAME_ROUTE => true]],
228
            );
229
        }
230
231
        $cartErrors->clear();
232
233
        return $this->renderStorefront('@Storefront/storefront/component/checkout/offcanvas-cart.html.twig', ['page' => $page]);
234
    }
235
236
    private function addAffiliateTracking(RequestDataBag $dataBag, SessionInterface $session): void
237
    {
238
        $affiliateCode = $session->get(AffiliateTrackingListener::AFFILIATE_CODE_KEY);
239
        $campaignCode = $session->get(AffiliateTrackingListener::CAMPAIGN_CODE_KEY);
240
        if ($affiliateCode) {
241
            $dataBag->set(AffiliateTrackingListener::AFFILIATE_CODE_KEY, $affiliateCode);
242
        }
243
244
        if ($campaignCode) {
245
            $dataBag->set(AffiliateTrackingListener::CAMPAIGN_CODE_KEY, $campaignCode);
246
        }
247
    }
248
249
    private function routeNeedsReload(ErrorCollection $cartErrors): bool
250
    {
251
        foreach ($cartErrors as $error) {
252
            if ($error instanceof ShippingMethodChangedError || $error instanceof PaymentMethodChangedError) {
253
                return true;
254
            }
255
        }
256
257
        return false;
258
    }
259
}
260