Completed
Pull Request — master (#358)
by Simon
04:39
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
34
 * @package ShopwarePlugins\Connect\Subscribers
35
 */
36
class Checkout implements SubscriberInterface
37
{
38
    /**
39
     * @var Logger
40
     */
41
    protected $logger;
42
43
    /**
44
     * @var string
45
     */
46
    private $newSessionId;
47
48
    /**
49
     * @var  ConnectFactory
50
     */
51
    protected $factory;
52
53
    /**
54
     * @var ModelManager
55
     */
56
    protected $manager;
57
58
    /**
59
     * @var Enlight_Event_EventManager
60
     */
61
    protected $eventManager;
62
63
    /**
64
     * @var SDK
65
     */
66
    private $sdk;
67
68
    /**
69
     * @var BasketHelper
70
     */
71
    private $basketHelper;
72
73
    /**
74
     * @var Helper
75
     */
76
    private $helper;
77
78
    /**
79
     * @param ModelManager $manager
80
     * @param Enlight_Event_EventManager $eventManager
81
     * @param SDK $sdk
82
     * @param BasketHelper $basketHelper
83
     * @param Helper $helper
84
     */
85
    public function __construct(
86
        ModelManager $manager,
87
        Enlight_Event_EventManager $eventManager,
88
        SDK $sdk,
89
        BasketHelper $basketHelper,
90
        Helper $helper
91
    ) {
92
        $this->manager = $manager;
93
        $this->eventManager = $eventManager;
94
        $this->logger = new Logger(Shopware()->Db());
95
        $this->factory = new ConnectFactory();
96
        $this->sdk = $sdk;
97
        $this->basketHelper = $basketHelper;
98
        $this->helper = $helper;
99
    }
100
101
    /**
102
     * @return array
103
     */
104
    public static function getSubscribedEvents()
105
    {
106
        return [
107
            'Enlight_Controller_Action_PostDispatch_Frontend_Checkout' => [ 'fixBasketForConnect' => '-1' ],
108
            'Enlight_Controller_Action_PreDispatch_Frontend_Checkout' => 'reserveConnectProductsOnCheckoutFinish',
109
            'Shopware_Modules_Admin_Regenerate_Session_Id' => 'updateSessionId',
110
        ];
111
    }
112
113
    /**
114
     * @param \Enlight_Event_EventArgs $args
115
     */
116
    public function updateSessionId(\Enlight_Event_EventArgs $args)
117
    {
118
        $this->newSessionId = $args->get('newSessionId');
119
    }
120
121
    /**
122
     * @return string
123
     */
124
    protected function getCountryCode()
125
    {
126
        $countryCodeUtil = $this->factory->getCountryCodeResolver();
127
128
        return $countryCodeUtil->getIso3CountryCode();
129
    }
130
131
    /**
132
     * Event listener method for the checkout confirm- and cartAction.
133
     *
134
     * @param \Enlight_Event_EventArgs $args
135
     * @throws CheckoutException
136
     * @return void
137
     */
138
    public function fixBasketForConnect(\Enlight_Event_EventArgs $args)
139
    {
140
        /** @var $action \Enlight_Controller_Action */
141
        $action = $args->getSubject();
142
        $view = $action->View();
143
        $request = $action->Request();
144
        $actionName = $request->getActionName();
145
        $sessionId = Shopware()->SessionID();
146
147
        $userId = Shopware()->Session()->sUserId;
148
        $hasConnectProduct = $this->helper->hasBasketConnectProducts($sessionId, $userId);
149
150
        if ($hasConnectProduct === false && $this->newSessionId) {
151
            $hasConnectProduct = $this->helper->hasBasketConnectProducts($this->newSessionId);
152
        }
153
154
        $view->hasConnectProduct = $hasConnectProduct;
155
156
        if ($actionName === 'ajax_add_article') {
157
            $view->addTemplateDir('Views/responsive', 'connect');
158
            $view->extendsTemplate('frontend/connect/ajax_add_article.tpl');
159
        }
160
161
        // send order to connect
162
        // this method must be called after external payments (Sofort, Billsafe)
163
        if ($actionName === 'finish' && !empty($view->sOrderNumber)) {
164
            try {
165
                $this->checkoutReservedProducts($view->sOrderNumber);
166
            } catch (CheckoutException $e) {
167
                $this->setOrderStatusError($view->sOrderNumber);
168
                throw $e;
169
            }
170
        }
171
172
        // clear connect reserved products
173
        // sometimes with external payment methods
174
        // $hasConnectProduct will be false, because order is already finished
175
        // and information about connect products is not available.
176
        if (!$hasConnectProduct) {
177
            $this->helper->clearConnectReservation();
178
179
            return;
180
        }
181
182
        if (!in_array($actionName, ['confirm', 'shippingPayment', 'cart', 'finish'])) {
183
            return;
184
        }
185
186
        if (empty($view->sBasket) || !$request->isDispatched()) {
187
            return;
188
        }
189
190
        if (!empty($view->sOrderNumber)) {
191
            return;
192
        }
193
194
        if (Shopware()->Config()->get('requirePhoneField')) {
195
            $this->enforcePhoneNumber($view);
196
        }
197
198
        $view->addTemplateDir('Views/responsive', 'connect');
199
200
        // Wrap the basket array in order to make it some more readable
201
        $this->basketHelper->setBasket($view->sBasket);
202
203
        // If no messages are shown, yet, check products from remote shop and build message array
204
        if (($connectMessages = Shopware()->Session()->connectMessages) === null) {
205
            $connectMessages = [];
206
207
            $session = Shopware()->Session();
208
            $userData = $session['sOrderVariables']['sUserData'];
209
            // prepare an order to check products
210
            $order = new \Shopware\Connect\Struct\Order();
211
            $order->orderItems = [];
212
            $order->billingAddress = $order->deliveryAddress = $this->getDeliveryAddress($userData);
213
214
            $allProducts = [];
215
216
            foreach ($this->basketHelper->getConnectProducts() as $shopId => $products) {
217
                $products = $this->helper->prepareConnectUnit($products);
218
                $allProducts = array_merge($allProducts, $products);
219
                // add order items in connect order
220
                $order->orderItems = array_map(function (Product $product) {
221
                    return new OrderItem([
222
                        'product' => $product,
223
                        'count' => $this->basketHelper->getQuantityForProduct($product),
224
                    ]);
225
                }, $products);
226
            }
227
228
            $this->eventManager->notify(
229
                'Connect_Merchant_Create_Order_Before',
230
                [
231
                    //we use clone to not be able to modify the connect order
232
                    'order' => clone $order,
233
                    'basket' => $view->sBasket,
234
                ]
235
            );
236
237
            try {
238
                /** @var $checkResult \Shopware\Connect\Struct\CheckResult */
239
                $checkResult = $this->sdk->checkProducts($order);
240
                $this->basketHelper->setCheckResult($checkResult);
241
242
                if ($checkResult->hasErrors()) {
243
                    $connectMessages = $checkResult->errors;
244
                }
245
            } catch (\Exception $e) {
246
                $this->logger->write(true, 'Error during checkout', $e, 'checkout');
247
                // If the checkout results in an exception because the remote shop is not available
248
                // don't show the exception to the user but tell him to remove the products from that shop
249
                $connectMessages = $this->getNotAvailableMessageForProducts($allProducts);
250
            }
251
        }
252
253
        if ($connectMessages) {
254
            $connectMessages = $this->translateConnectMessages($connectMessages);
255
        }
256
257
        Shopware()->Session()->connectMessages = null;
258
259
        // If no products are bought from the local shop, move the first connect shop into
260
        // the content section. Also set that shop's id in the template
261
        $shopId = $this->basketHelper->fixBasket();
262
        if ($shopId) {
263
            $view->shopId = $shopId;
264
        }
265
        // Increase amount and shipping costs by the amount of connect shipping costs
266
        $this->basketHelper->recalculate($this->basketHelper->getCheckResult());
267
268
        $connectMessages = $this->getNotShippableMessages($this->basketHelper->getCheckResult(), $connectMessages);
269
270
        $view->assign($this->basketHelper->getDefaultTemplateVariables());
271
272
        // Set the sOrderVariables for the session based on the original content subarray of the basket array
273
        // @HL - docs?
274
        if ($actionName === 'confirm') {
275
            $session = Shopware()->Session();
276
            /** @var $variables \ArrayObject */
277
            $variables = $session->offsetGet('sOrderVariables');
278
279
            $session->offsetSet('sOrderVariables', $this->basketHelper->getOrderVariablesForSession($variables));
280
        }
281
282
        $view->assign($this->basketHelper->getConnectTemplateVariables($connectMessages));
283
        $view->assign('showShippingCostsSeparately', $this->factory->getConfigComponent()->getConfig('showShippingCostsSeparately', false));
284
    }
285
286
    /**
287
     * Helper to translate connect messages from the SDK. Will use the normalized message itself as namespace key
288
     *
289
     * @param $connectMessages
290
     * @return mixed
291
     */
292
    private function translateConnectMessages($connectMessages)
293
    {
294
        $namespace = Shopware()->Snippets()->getNamespace('frontend/checkout/connect');
295
296
        foreach ($connectMessages as &$connectMessage) {
297
            $message = trim($connectMessage->message);
298
            $normalized = strtolower(preg_replace('/[^a-zA-Z0-9]/', '_', $connectMessage->message));
299
            if (empty($normalized) || empty($message)) {
300
                $normalized = 'unknown-connect-error';
301
                $message = 'Unknown error';
302
            }
303
            $translation = $namespace->get(
304
                $normalized,
305
                $message,
306
                true
307
            );
308
309
            $connectMessage->message = $translation;
310
        }
311
312
        return $connectMessages;
313
    }
314
315
    /**
316
     * Event listener method for the checkout->finishAction. Will reserve products and redirect to
317
     * the confirm page if a product cannot be reserved
318
     *
319
     * @event Enlight_Controller_Action_PreDispatch_Frontend_Checkout
320
     * @param \Enlight_Event_EventArgs $args
321
     */
322
    public function reserveConnectProductsOnCheckoutFinish(\Enlight_Event_EventArgs $args)
323
    {
324
        /** @var $controller \Enlight_Controller_Action */
325
        $controller = $args->getSubject();
326
        $request = $controller->Request();
327
        $view = $controller->View();
328
        $session = Shopware()->Session();
329
        $userData = $session['sOrderVariables']['sUserData'];
330
        $paymentName = $userData['additional']['payment']['name'];
331
332
        if (($request->getActionName() !== 'finish' && $request->getActionName() !== 'payment')) {
333
            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...
334
                // BEP-1010 Fix for Klarna checkout
335
            } else {
336
                return;
337
            }
338
        }
339
340
        if (empty($session['sOrderVariables'])) {
341
            return;
342
        }
343
344
        if (!$this->helper->hasBasketConnectProducts(Shopware()->SessionID())) {
345
            return;
346
        }
347
348
        $userData = $session['sOrderVariables']['sUserData'];
349
        $paymentId = $userData['additional']['payment']['id'];
350
351
        if ($this->isPaymentAllowed($paymentId) === false) {
352
            $connectMessage = new \stdClass();
353
            $connectMessage->message = 'frontend_checkout_cart_connect_payment_not_allowed';
354
355
            $connectMessages = [
356
                0 => [
357
                    'connectmessage' => $connectMessage
358
                ]
359
            ];
360
361
            Shopware()->Session()->connectMessages = $this->translateConnectMessages($connectMessages);
362
            $controller->forward('confirm');
363
        }
364
365
        if (Shopware()->Config()->get('requirePhoneField')) {
366
            $this->enforcePhoneNumber($view);
367
        }
368
369
        $order = new \Shopware\Connect\Struct\Order();
370
        $order->orderItems = [];
371
        $order->deliveryAddress = $this->getDeliveryAddress($userData);
372
373
        $basket = $session['sOrderVariables']['sBasket'];
374
375
        /** @var \ShopwarePlugins\Connect\Components\Utils\OrderPaymentMapper $orderPaymentMapper */
376
        $orderPaymentMapper = new OrderPaymentMapper();
377
        $orderPaymentName = $userData['additional']['payment']['name'];
378
        $order->paymentType = $orderPaymentMapper->mapShopwareOrderPaymentToConnect($orderPaymentName);
379
380
        foreach ($basket['content'] as $row) {
381
            if (!empty($row['mode'])) {
382
                continue;
383
            }
384
385
            $articleDetailId = $row['additional_details']['articleDetailsID'];
386
            if ($this->helper->isRemoteArticleDetailDBAL($articleDetailId) === false) {
387
                continue;
388
            }
389
            $shopProductId = $this->helper->getShopProductId($articleDetailId);
390
391
            $products = $this->helper->getRemoteProducts([$shopProductId->sourceId], $shopProductId->shopId);
392
            $products = $this->helper->prepareConnectUnit($products);
393
394
            if (empty($products)) {
395
                continue;
396
            }
397
            $product = $products[0];
398
399
400
            if ($product === null || $product->shopId === null) {
401
                continue;
402
            }
403
404
            $orderItem = new \Shopware\Connect\Struct\OrderItem();
405
            $orderItem->product = $product;
406
            $orderItem->count = (int) $row['quantity'];
407
            $order->orderItems[] = $orderItem;
408
        }
409
410
        if (empty($order->orderItems)) {
411
            return;
412
        }
413
414
        try {
415
            $order = $this->eventManager->filter(
416
                'Connect_Subscriber_OrderReservation_OrderFilter',
417
                $order
418
            );
419
420
            /** @var $reservation \Shopware\Connect\Struct\Reservation */
421
            $reservation = $this->sdk->reserveProducts($order);
422
423
            if (!$reservation || !$reservation->success) {
424
                throw new \Exception('Error during reservation');
425
            }
426
427
            if (!empty($reservation->messages)) {
428
                $messages = $reservation->messages;
429
            }
430
        } catch (\Exception $e) {
431
            $this->logger->write(true, 'Error during reservation', $e, 'reservation');
432
            $messages = $this->getNotAvailableMessageForProducts(array_map(
433
                function ($orderItem) {
434
                    return $orderItem->product;
435
                },
436
                $order->orderItems
437
            ));
438
        }
439
440
        if (!empty($messages)) {
441
            Shopware()->Session()->connectMessages = $messages;
442
            $controller->forward('confirm');
443
        } else {
444
            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...
445
        }
446
    }
447
448
    /**
449
     * Helper method to create an address struct from shopware session info
450
     *
451
     * @param $userData
452
     * @return Address
453
     */
454
    private function getDeliveryAddress($userData)
455
    {
456
        if (!$userData) {
457
            return $this->createDummyAddress('DEU');
458
        }
459
        $shippingData = $userData['shippingaddress'];
460
        $address = new Address();
461
        $address->zip = $shippingData['zipcode'];
462
        $address->city = $shippingData['city'];
463
        $address->country = $userData['additional']['countryShipping']['iso3']; //when the user is not logged in
464
        $address->phone = $userData['billingaddress']['phone'];
465
        $address->email = $userData['additional']['user']['email'];
466
        if (!empty($userData['additional']['stateShipping']['shortcode'])) {
467
            $address->state = $userData['additional']['stateShipping']['shortcode'];
468
        }
469
        $address->firstName = $shippingData['firstname'];
470
        $address->surName = $shippingData['lastname'];
471
        if (!empty($shippingData['company'])) {
472
            $address->company = $shippingData['company'];
473
        }
474
        $address->street = $shippingData['street'];
475
        $address->streetNumber = (string) $shippingData['streetnumber'];
476
477
        return $address;
478
    }
479
480
    /**
481
     * @param string $country
482
     * @return Address
483
     */
484
    private function createDummyAddress($country = 'DEU')
485
    {
486
        return new Address([
487
            'country' => $country,
488
            'firstName' => 'Shopware',
489
            'surName' => 'AG',
490
            'street' => 'Eggeroder Str. 6',
491
            'zip' => '48624',
492
            'city' => 'Schöppingen',
493
            'phone' => '+49 (0) 2555 92885-0',
494
            'email' => '[email protected]'
495
        ]);
496
    }
497
498
    /**
499
     * Hooks the sSaveOrder frontend method and reserves the connect products
500
     *
501
     * @param $orderNumber
502
     * @throws \ShopwarePlugins\Connect\Components\Exceptions\CheckoutException
503
     */
504
    public function checkoutReservedProducts($orderNumber)
505
    {
506
        if (empty($orderNumber)) {
507
            return;
508
        }
509
510
        $reservation = unserialize(Shopware()->Session()->connectReservation);
511
        if ($reservation !== null && $reservation !== false) {
512
            $result = $this->sdk->checkout($reservation, $orderNumber);
513
            foreach ($result as $shopId => $success) {
514
                if (!$success) {
515
                    $e = new CheckoutException("Could not checkout from warehouse {$shopId}");
516
                    $this->logger->write(true, 'Error during checkout with this reservation: ' . json_encode($reservation, JSON_PRETTY_PRINT), $e, 'checkout');
517
                    throw $e;
518
                }
519
            }
520
            $this->helper->clearConnectReservation();
521
        }
522
    }
523
524
    /**
525
     * Asks the user to leave is phone number if connect products are in the basket and the
526
     * phone number was not configured, yet.
527
     *
528
     * @param \Enlight_View_Default $view
529
     * @return null
530
     */
531
    public function enforcePhoneNumber($view)
532
    {
533
        if (Shopware()->Session()->sUserId && $this->helper->hasBasketConnectProducts(Shopware()->SessionID())) {
534
            $id = Shopware()->Session()->sUserId;
535
536
            $sql = 'SELECT phone FROM s_user_billingaddress WHERE userID = :id';
537
            $result = Shopware()->Db()->fetchOne($sql, ['id' => $id]);
538
            if (!$result) {
539
                $view->assign('phoneMissing', true);
540
            }
541
        }
542
    }
543
544
    /**
545
     * @param Product[] $products
546
     * @return array
547
     */
548
    protected function getNotAvailableMessageForProducts($products)
549
    {
550
        $messages = [];
551
        foreach ($products as $product) {
552
            $messages[] = new Message([
553
                'message' => 'Due to technical reasons, product %product is not available.',
554
                'values' => [
555
                    'product' => $product->title,
556
                ]
557
            ]);
558
        }
559
560
        return $messages;
561
    }
562
563
    /**
564
     * @param \Shopware\Connect\Struct\CheckResult $checkResult
565
     * @param $connectMessages
566
     * @return mixed
567
     */
568
    protected function getNotShippableMessages($checkResult, $connectMessages)
569
    {
570
        if (!$checkResult instanceof CheckResult) {
571
            return $connectMessages;
572
        }
573
574
        $namespace = Shopware()->Snippets()->getNamespace('frontend/checkout/connect');
575
576
        foreach ($checkResult->shippingCosts as $shipping) {
577
            if ($shipping->isShippable === false) {
578
                $connectMessages[] = new Message([
579
                    'message' => $namespace->get(
580
                            'frontend_checkout_cart_connect_not_shippable',
581
                            'Ihre Bestellung kann nicht geliefert werden',
582
                            true
583
                        )
584
                ]);
585
            }
586
        }
587
588
        return $connectMessages;
589
    }
590
591
    /**
592
     * @param $orderNumber
593
     * @return void
594
     */
595
    private function setOrderStatusError($orderNumber)
596
    {
597
        $repo = $this->manager->getRepository(Order::class);
598
599
        /** @var Order $order */
600
        $order = $repo->findOneBy(['number' => $orderNumber]);
601
602
        $repoStatus = $this->manager->getRepository(Status::class);
603
        $status = $repoStatus->findOneBy(['name' => ConnectOrderUtil::ORDER_STATUS_ERROR, 'group' => Status::GROUP_STATE ]);
604
605
        $order->setOrderStatus($status);
606
        $this->manager->persist($order);
607
        $this->manager->flush();
608
    }
609
610
    /**
611
     * Check is allowed payment method with connect products
612
     * @param int $paymentId
613
     * @return bool
614
     */
615
    private function isPaymentAllowed($paymentId)
616
    {
617
        if ($paymentId < 1) {
618
            return false;
619
        }
620
621
        $paymentRepository = Shopware()->Models()->getRepository(Payment::class);
622
        /** @var PaymentSubscriber $payment */
623
        $payment = $paymentRepository->find($paymentId);
624
625
        if (!$payment) {
626
            return false;
627
        }
628
629
        if ($payment->getAttribute()->getConnectIsAllowed() == 0) {
630
            return false;
631
        }
632
633
        return true;
634
    }
635
}
636