Passed
Push — trunk ( f8d7b0...989dfd )
by Christian
10:03 queued 13s
created

CheckoutController::order()   B

Complexity

Conditions 8
Paths 14

Size

Total Lines 38
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

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