Completed
Pull Request — master (#392)
by Christian
03:03
created

Checkout::translateConnectMessages()   B

Complexity

Conditions 4
Paths 3

Size

Total Lines 22
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

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