Completed
Pull Request — master (#358)
by Stefan
03:07
created

Checkout::getSubscribedEvents()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 8
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 5
nc 1
nop 0
1
<?php
2
/**
3
 * (c) shopware AG <[email protected]>
4
 * For the full copyright and license information, please view the LICENSE
5
 * file that was distributed with this source code.
6
 */
7
8
namespace ShopwarePlugins\Connect\Subscribers;
9
10
use Enlight\Event\SubscriberInterface;
11
use Enlight_Event_EventManager;
12
use Shopware\Components\Model\ModelManager;
13
use Shopware\Connect\SDK;
14
use Shopware\Connect\Struct\Address;
15
use Shopware\Connect\Struct\CheckResult;
16
use Shopware\Connect\Struct\Message;
17
use Shopware\Connect\Struct\OrderItem;
18
use Shopware\Connect\Struct\Product;
19
use Shopware\Models\Order\Status;
20
use ShopwarePlugins\Connect\Components\BasketHelper;
21
use ShopwarePlugins\Connect\Components\ConnectFactory;
22
use ShopwarePlugins\Connect\Components\Exceptions\CheckoutException;
23
use ShopwarePlugins\Connect\Components\Helper;
24
use ShopwarePlugins\Connect\Components\Logger;
25
use ShopwarePlugins\Connect\Components\Utils\ConnectOrderUtil;
26
use ShopwarePlugins\Connect\Components\Utils\OrderPaymentMapper;
27
use Shopware\Models\Order\Order;
28
use Shopware\Models\Payment\Payment;
29
30
/**
31
 * Handles the whole checkout manipulation, which is required for the connect checkout
32
 */
33
class Checkout implements SubscriberInterface
34
{
35
    /**
36
     * @var Logger
37
     */
38
    protected $logger;
39
40
    /**
41
     * @var string
42
     */
43
    private $newSessionId;
44
45
    /**
46
     * @var  ConnectFactory
47
     */
48
    protected $factory;
49
50
    /**
51
     * @var ModelManager
52
     */
53
    protected $manager;
54
55
    /**
56
     * @var Enlight_Event_EventManager
57
     */
58
    protected $eventManager;
59
60
    /**
61
     * @var SDK
62
     */
63
    private $sdk;
64
65
    /**
66
     * @var BasketHelper
67
     */
68
    private $basketHelper;
69
70
    /**
71
     * @var Helper
72
     */
73
    private $helper;
74
75
    /**
76
     * @param ModelManager $manager
77
     * @param Enlight_Event_EventManager $eventManager
78
     * @param SDK $sdk
79
     * @param BasketHelper $basketHelper
80
     * @param Helper $helper
81
     */
82
    public function __construct(
83
        ModelManager $manager,
84
        Enlight_Event_EventManager $eventManager,
85
        SDK $sdk,
86
        BasketHelper $basketHelper,
87
        Helper $helper
88
    ) {
89
        $this->manager = $manager;
90
        $this->eventManager = $eventManager;
91
        $this->logger = new Logger(Shopware()->Db());
92
        $this->factory = new ConnectFactory();
93
        $this->sdk = $sdk;
94
        $this->basketHelper = $basketHelper;
95
        $this->helper = $helper;
96
    }
97
98
    /**
99
     * {@inheritdoc}
100
     */
101
    public static function getSubscribedEvents()
102
    {
103
        return [
104
            'Enlight_Controller_Action_PostDispatch_Frontend_Checkout' => [ 'fixBasketForConnect', '-1' ],
105
            'Enlight_Controller_Action_PreDispatch_Frontend_Checkout' => 'reserveConnectProductsOnCheckoutFinish',
106
            'Shopware_Modules_Admin_Regenerate_Session_Id' => 'updateSessionId',
107
        ];
108
    }
109
110
    /**
111
     * @param \Enlight_Event_EventArgs $args
112
     */
113
    public function updateSessionId(\Enlight_Event_EventArgs $args)
114
    {
115
        $this->newSessionId = $args->get('newSessionId');
116
    }
117
118
    /**
119
     * @return string
120
     */
121
    protected function getCountryCode()
122
    {
123
        $countryCodeUtil = $this->factory->getCountryCodeResolver();
124
125
        return $countryCodeUtil->getIso3CountryCode();
126
    }
127
128
    /**
129
     * Event listener method for the checkout confirm- and cartAction.
130
     *
131
     * @param \Enlight_Event_EventArgs $args
132
     * @throws CheckoutException
133
     * @return void
134
     */
135
    public function fixBasketForConnect(\Enlight_Event_EventArgs $args)
136
    {
137
        /** @var $action \Enlight_Controller_Action */
138
        $action = $args->getSubject();
139
        $view = $action->View();
140
        $request = $action->Request();
141
        $actionName = $request->getActionName();
142
        $sessionId = Shopware()->SessionID();
143
144
        $userId = Shopware()->Session()->sUserId;
145
        $hasConnectProduct = $this->helper->hasBasketConnectProducts($sessionId, $userId);
146
147
        if ($hasConnectProduct === false && $this->newSessionId) {
148
            $hasConnectProduct = $this->helper->hasBasketConnectProducts($this->newSessionId);
149
        }
150
151
        $view->hasConnectProduct = $hasConnectProduct;
152
153
        if ($actionName === 'ajax_add_article') {
154
            $view->extendsTemplate('frontend/connect/ajax_add_article.tpl');
155
        }
156
157
        // send order to connect
158
        // this method must be called after external payments (Sofort, Billsafe)
159
        if ($actionName === 'finish' && !empty($view->sOrderNumber)) {
160
            try {
161
                $this->checkoutReservedProducts($view->sOrderNumber);
162
            } catch (CheckoutException $e) {
163
                $this->setOrderStatusError($view->sOrderNumber);
164
                throw $e;
165
            }
166
        }
167
168
        // clear connect reserved products
169
        // sometimes with external payment methods
170
        // $hasConnectProduct will be false, because order is already finished
171
        // and information about connect products is not available.
172
        if (!$hasConnectProduct) {
173
            $this->helper->clearConnectReservation();
174
175
            return;
176
        }
177
178
        if (!in_array($actionName, ['confirm', 'shippingPayment', 'cart', 'finish'])) {
179
            return;
180
        }
181
182
        if (empty($view->sBasket) || !$request->isDispatched()) {
183
            return;
184
        }
185
186
        if (!empty($view->sOrderNumber)) {
187
            return;
188
        }
189
190
        if (Shopware()->Config()->get('requirePhoneField')) {
191
            $this->enforcePhoneNumber($view);
192
        }
193
194
        // Wrap the basket array in order to make it some more readable
195
        $this->basketHelper->setBasket($view->sBasket);
196
197
        // If no messages are shown, yet, check products from remote shop and build message array
198
        if (($connectMessages = Shopware()->Session()->connectMessages) === null) {
199
            $connectMessages = [];
200
201
            $session = Shopware()->Session();
202
            $userData = $session['sOrderVariables']['sUserData'];
203
            // prepare an order to check products
204
            $order = new \Shopware\Connect\Struct\Order();
205
            $order->orderItems = [];
206
            $order->billingAddress = $order->deliveryAddress = $this->getDeliveryAddress($userData);
207
208
            $allProducts = [];
209
210
            foreach ($this->basketHelper->getConnectProducts() as $shopId => $products) {
211
                $products = $this->helper->prepareConnectUnit($products);
212
                $allProducts = array_merge($allProducts, $products);
213
                // add order items in connect order
214
                $order->orderItems = array_map(function (Product $product) {
215
                    return new OrderItem([
216
                        'product' => $product,
217
                        'count' => $this->basketHelper->getQuantityForProduct($product),
218
                    ]);
219
                }, $products);
220
            }
221
222
            $this->eventManager->notify(
223
                'Connect_Merchant_Create_Order_Before',
224
                [
225
                    //we use clone to not be able to modify the connect order
226
                    'order' => clone $order,
227
                    'basket' => $view->sBasket,
228
                ]
229
            );
230
231
            try {
232
                /** @var $checkResult \Shopware\Connect\Struct\CheckResult */
233
                $checkResult = $this->sdk->checkProducts($order);
234
                $this->basketHelper->setCheckResult($checkResult);
235
236
                if ($checkResult->hasErrors()) {
237
                    $connectMessages = $checkResult->errors;
238
                }
239
            } catch (\Exception $e) {
240
                $this->logger->write(true, 'Error during checkout', $e, 'checkout');
241
                // If the checkout results in an exception because the remote shop is not available
242
                // don't show the exception to the user but tell him to remove the products from that shop
243
                $connectMessages = $this->getNotAvailableMessageForProducts($allProducts);
244
            }
245
        }
246
247
        if ($connectMessages) {
248
            $connectMessages = $this->translateConnectMessages($connectMessages);
249
        }
250
251
        Shopware()->Session()->connectMessages = null;
252
253
        // If no products are bought from the local shop, move the first connect shop into
254
        // the content section. Also set that shop's id in the template
255
        $shopId = $this->basketHelper->fixBasket();
256
        if ($shopId) {
257
            $view->shopId = $shopId;
258
        }
259
        // Increase amount and shipping costs by the amount of connect shipping costs
260
        $this->basketHelper->recalculate($this->basketHelper->getCheckResult());
261
262
        $connectMessages = $this->getNotShippableMessages($this->basketHelper->getCheckResult(), $connectMessages);
263
264
        $view->assign($this->basketHelper->getDefaultTemplateVariables());
265
266
        // Set the sOrderVariables for the session based on the original content subarray of the basket array
267
        // @HL - docs?
268
        if ($actionName === 'confirm') {
269
            $session = Shopware()->Session();
270
            /** @var $variables \ArrayObject */
271
            $variables = $session->offsetGet('sOrderVariables');
272
273
            $session->offsetSet('sOrderVariables', $this->basketHelper->getOrderVariablesForSession($variables));
274
        }
275
276
        $view->assign($this->basketHelper->getConnectTemplateVariables($connectMessages));
277
        $view->assign('showShippingCostsSeparately', $this->factory->getConfigComponent()->getConfig('showShippingCostsSeparately', false));
278
    }
279
280
    /**
281
     * Helper to translate connect messages from the SDK. Will use the normalized message itself as namespace key
282
     *
283
     * @param $connectMessages
284
     * @return mixed
285
     */
286
    private function translateConnectMessages($connectMessages)
287
    {
288
        $namespace = Shopware()->Snippets()->getNamespace('frontend/checkout/connect');
289
290
        foreach ($connectMessages as &$connectMessage) {
291
            $message = trim($connectMessage->message);
292
            $normalized = strtolower(preg_replace('/[^a-zA-Z0-9]/', '_', $connectMessage->message));
293
            if (empty($normalized) || empty($message)) {
294
                $normalized = 'unknown-connect-error';
295
                $message = 'Unknown error';
296
            }
297
            $translation = $namespace->get(
298
                $normalized,
299
                $message,
300
                true
301
            );
302
303
            $connectMessage->message = $translation;
304
        }
305
306
        return $connectMessages;
307
    }
308
309
    /**
310
     * Event listener method for the checkout->finishAction. Will reserve products and redirect to
311
     * the confirm page if a product cannot be reserved
312
     *
313
     * @event Enlight_Controller_Action_PreDispatch_Frontend_Checkout
314
     * @param \Enlight_Event_EventArgs $args
315
     */
316
    public function reserveConnectProductsOnCheckoutFinish(\Enlight_Event_EventArgs $args)
317
    {
318
        /** @var $controller \Enlight_Controller_Action */
319
        $controller = $args->getSubject();
320
        $request = $controller->Request();
321
        $view = $controller->View();
322
        $session = Shopware()->Session();
323
        $userData = $session['sOrderVariables']['sUserData'];
324
        $paymentName = $userData['additional']['payment']['name'];
325
326
        if (($request->getActionName() !== 'finish' && $request->getActionName() !== 'payment')) {
327
            if (($request->getActionName() === 'confirm' && $paymentName === 'klarna_checkout')) {
0 ignored issues
show
Unused Code introduced by
This if statement is empty and can be removed.

This check looks for the bodies of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These if bodies can be removed. If you have an empty if but statements in the else branch, consider inverting the condition.

if (rand(1, 6) > 3) {
//print "Check failed";
} else {
    print "Check succeeded";
}

could be turned into

if (rand(1, 6) <= 3) {
    print "Check succeeded";
}

This is much more concise to read.

Loading history...
328
                // BEP-1010 Fix for Klarna checkout
329
            } else {
330
                return;
331
            }
332
        }
333
334
        if (empty($session['sOrderVariables'])) {
335
            return;
336
        }
337
338
        if (!$this->helper->hasBasketConnectProducts(Shopware()->SessionID())) {
339
            return;
340
        }
341
342
        $userData = $session['sOrderVariables']['sUserData'];
343
        $paymentId = $userData['additional']['payment']['id'];
344
345
        if ($this->isPaymentAllowed($paymentId) === false) {
346
            $connectMessage = new \stdClass();
347
            $connectMessage->message = 'frontend_checkout_cart_connect_payment_not_allowed';
348
349
            $connectMessages = [
350
                0 => [
351
                    'connectmessage' => $connectMessage
352
                ]
353
            ];
354
355
            Shopware()->Session()->connectMessages = $this->translateConnectMessages($connectMessages);
356
            $controller->forward('confirm');
357
        }
358
359
        if (Shopware()->Config()->get('requirePhoneField')) {
360
            $this->enforcePhoneNumber($view);
361
        }
362
363
        $order = new \Shopware\Connect\Struct\Order();
364
        $order->orderItems = [];
365
        $order->deliveryAddress = $this->getDeliveryAddress($userData);
366
367
        $basket = $session['sOrderVariables']['sBasket'];
368
369
        /** @var \ShopwarePlugins\Connect\Components\Utils\OrderPaymentMapper $orderPaymentMapper */
370
        $orderPaymentMapper = new OrderPaymentMapper();
371
        $orderPaymentName = $userData['additional']['payment']['name'];
372
        $order->paymentType = $orderPaymentMapper->mapShopwareOrderPaymentToConnect($orderPaymentName);
373
374
        foreach ($basket['content'] as $row) {
375
            if (!empty($row['mode'])) {
376
                continue;
377
            }
378
379
            $articleDetailId = $row['additional_details']['articleDetailsID'];
380
            if ($this->helper->isRemoteArticleDetailDBAL($articleDetailId) === false) {
381
                continue;
382
            }
383
            $shopProductId = $this->helper->getShopProductId($articleDetailId);
384
385
            $products = $this->helper->getRemoteProducts([$shopProductId->sourceId], $shopProductId->shopId);
386
            $products = $this->helper->prepareConnectUnit($products);
387
388
            if (empty($products)) {
389
                continue;
390
            }
391
            $product = $products[0];
392
393
394
            if ($product === null || $product->shopId === null) {
395
                continue;
396
            }
397
398
            $orderItem = new \Shopware\Connect\Struct\OrderItem();
399
            $orderItem->product = $product;
400
            $orderItem->count = (int) $row['quantity'];
401
            $order->orderItems[] = $orderItem;
402
        }
403
404
        if (empty($order->orderItems)) {
405
            return;
406
        }
407
408
        try {
409
            $order = $this->eventManager->filter(
410
                'Connect_Subscriber_OrderReservation_OrderFilter',
411
                $order
412
            );
413
414
            /** @var $reservation \Shopware\Connect\Struct\Reservation */
415
            $reservation = $this->sdk->reserveProducts($order);
416
417
            if (!$reservation || !$reservation->success) {
418
                throw new \Exception('Error during reservation');
419
            }
420
421
            if (!empty($reservation->messages)) {
422
                $messages = $reservation->messages;
423
            }
424
        } catch (\Exception $e) {
425
            $this->logger->write(true, 'Error during reservation', $e, 'reservation');
426
            $messages = $this->getNotAvailableMessageForProducts(array_map(
427
                function ($orderItem) {
428
                    return $orderItem->product;
429
                },
430
                $order->orderItems
431
            ));
432
        }
433
434
        if (!empty($messages)) {
435
            Shopware()->Session()->connectMessages = $messages;
436
            $controller->forward('confirm');
437
        } else {
438
            Shopware()->Session()->connectReservation = serialize($reservation);
0 ignored issues
show
Bug introduced by
The variable $reservation does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
439
        }
440
    }
441
442
    /**
443
     * Helper method to create an address struct from shopware session info
444
     *
445
     * @param $userData
446
     * @return Address
447
     */
448
    private function getDeliveryAddress($userData)
449
    {
450
        if (!$userData) {
451
            return $this->createDummyAddress('DEU');
452
        }
453
        $shippingData = $userData['shippingaddress'];
454
        $address = new Address();
455
        $address->zip = $shippingData['zipcode'];
456
        $address->city = $shippingData['city'];
457
        $address->country = $userData['additional']['countryShipping']['iso3']; //when the user is not logged in
458
        $address->phone = $userData['billingaddress']['phone'];
459
        $address->email = $userData['additional']['user']['email'];
460
        if (!empty($userData['additional']['stateShipping']['shortcode'])) {
461
            $address->state = $userData['additional']['stateShipping']['shortcode'];
462
        }
463
        $address->firstName = $shippingData['firstname'];
464
        $address->surName = $shippingData['lastname'];
465
        if (!empty($shippingData['company'])) {
466
            $address->company = $shippingData['company'];
467
        }
468
        $address->street = $shippingData['street'];
469
        $address->streetNumber = (string) $shippingData['streetnumber'];
470
471
        return $address;
472
    }
473
474
    /**
475
     * @param string $country
476
     * @return Address
477
     */
478
    private function createDummyAddress($country = 'DEU')
479
    {
480
        return new Address([
481
            'country' => $country,
482
            'firstName' => 'Shopware',
483
            'surName' => 'AG',
484
            'street' => 'Eggeroder Str. 6',
485
            'zip' => '48624',
486
            'city' => 'Schöppingen',
487
            'phone' => '+49 (0) 2555 92885-0',
488
            'email' => '[email protected]'
489
        ]);
490
    }
491
492
    /**
493
     * Hooks the sSaveOrder frontend method and reserves the connect products
494
     *
495
     * @param $orderNumber
496
     * @throws \ShopwarePlugins\Connect\Components\Exceptions\CheckoutException
497
     */
498
    public function checkoutReservedProducts($orderNumber)
499
    {
500
        if (empty($orderNumber)) {
501
            return;
502
        }
503
504
        $reservation = unserialize(Shopware()->Session()->connectReservation);
505
        if ($reservation !== null && $reservation !== false) {
506
            $result = $this->sdk->checkout($reservation, $orderNumber);
507
            foreach ($result as $shopId => $success) {
508
                if (!$success) {
509
                    $e = new CheckoutException("Could not checkout from warehouse {$shopId}");
510
                    $this->logger->write(true, 'Error during checkout with this reservation: ' . json_encode($reservation, JSON_PRETTY_PRINT), $e, 'checkout');
511
                    throw $e;
512
                }
513
            }
514
            $this->helper->clearConnectReservation();
515
        }
516
    }
517
518
    /**
519
     * Asks the user to leave is phone number if connect products are in the basket and the
520
     * phone number was not configured, yet.
521
     *
522
     * @param \Enlight_View_Default $view
523
     * @return null
524
     */
525
    public function enforcePhoneNumber($view)
526
    {
527
        if (Shopware()->Session()->sUserId && $this->helper->hasBasketConnectProducts(Shopware()->SessionID())) {
528
            $id = Shopware()->Session()->sUserId;
529
530
            $sql = 'SELECT phone FROM s_user_billingaddress WHERE userID = :id';
531
            $result = Shopware()->Db()->fetchOne($sql, ['id' => $id]);
532
            if (!$result) {
533
                $view->assign('phoneMissing', true);
534
            }
535
        }
536
    }
537
538
    /**
539
     * @param Product[] $products
540
     * @return array
541
     */
542
    protected function getNotAvailableMessageForProducts($products)
543
    {
544
        $messages = [];
545
        foreach ($products as $product) {
546
            $messages[] = new Message([
547
                'message' => 'Due to technical reasons, product %product is not available.',
548
                'values' => [
549
                    'product' => $product->title,
550
                ]
551
            ]);
552
        }
553
554
        return $messages;
555
    }
556
557
    /**
558
     * @param \Shopware\Connect\Struct\CheckResult $checkResult
559
     * @param $connectMessages
560
     * @return mixed
561
     */
562
    protected function getNotShippableMessages($checkResult, $connectMessages)
563
    {
564
        if (!$checkResult instanceof CheckResult) {
565
            return $connectMessages;
566
        }
567
568
        $namespace = Shopware()->Snippets()->getNamespace('frontend/checkout/connect');
569
570
        foreach ($checkResult->shippingCosts as $shipping) {
571
            if ($shipping->isShippable === false) {
572
                $connectMessages[] = new Message([
573
                    'message' => $namespace->get(
574
                            'frontend_checkout_cart_connect_not_shippable',
575
                            'Ihre Bestellung kann nicht geliefert werden',
576
                            true
577
                        )
578
                ]);
579
            }
580
        }
581
582
        return $connectMessages;
583
    }
584
585
    /**
586
     * @param $orderNumber
587
     * @return void
588
     */
589
    private function setOrderStatusError($orderNumber)
590
    {
591
        $repo = $this->manager->getRepository(Order::class);
592
593
        /** @var Order $order */
594
        $order = $repo->findOneBy(['number' => $orderNumber]);
595
596
        $repoStatus = $this->manager->getRepository(Status::class);
597
        $status = $repoStatus->findOneBy(['name' => ConnectOrderUtil::ORDER_STATUS_ERROR, 'group' => Status::GROUP_STATE ]);
598
599
        $order->setOrderStatus($status);
600
        $this->manager->persist($order);
601
        $this->manager->flush();
602
    }
603
604
    /**
605
     * Check is allowed payment method with connect products
606
     * @param int $paymentId
607
     * @return bool
608
     */
609
    private function isPaymentAllowed($paymentId)
610
    {
611
        if ($paymentId < 1) {
612
            return false;
613
        }
614
615
        $paymentRepository = $this->manager->getRepository(Payment::class);
616
        /** @var Payment $payment */
617
        $payment = $paymentRepository->find($paymentId);
618
619
        if (!$payment) {
620
            return false;
621
        }
622
623
        if ($payment->getAttribute()->getConnectIsAllowed() == 0) {
624
            return false;
625
        }
626
627
        return true;
628
    }
629
}
630