Passed
Pull Request — master (#16)
by
unknown
03:22
created

Index::execute()   A

Complexity

Conditions 5
Paths 96

Size

Total Lines 43
Code Lines 35

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 35
dl 0
loc 43
rs 9.0488
c 0
b 0
f 0
cc 5
nc 96
nop 0
1
<?php
2
3
namespace DigitalOrigin\Pmt\Controller\Notify;
4
5
use Magento\Quote\Model\QuoteManagement;
6
use Magento\Quote\Api\Data\PaymentInterface;
7
use Magento\Sales\Api\Data\OrderInterface;
8
use Magento\Sales\Api\OrderRepositoryInterface;
9
use Magento\Quote\Model\Quote;
10
use Magento\Quote\Model\QuoteRepository;
11
use Magento\Framework\App\Action\Context;
12
use Magento\Framework\App\Action\Action;
13
use PagaMasTarde\ModuleUtils\Exception\MerchantOrderNotFoundException;
14
use PagaMasTarde\OrdersApiClient\Client;
15
use DigitalOrigin\Pmt\Helper\Config;
16
use DigitalOrigin\Pmt\Helper\ExtraConfig;
17
use Magento\Framework\App\ResourceConnection;
18
use Magento\Checkout\Model\Session;
19
use Magento\Framework\DB\Ddl\Table;
20
use PagaMasTarde\ModuleUtils\Exception\AmountMismatchException;
21
use PagaMasTarde\ModuleUtils\Exception\ConcurrencyException;
22
use PagaMasTarde\ModuleUtils\Exception\MerchantNoOrderFoundException;
0 ignored issues
show
Bug introduced by
The type PagaMasTarde\ModuleUtils...ntNoOrderFoundException was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
23
use PagaMasTarde\ModuleUtils\Exception\NoIdentificationException;
24
use PagaMasTarde\ModuleUtils\Exception\OrderNotFoundException;
25
use PagaMasTarde\ModuleUtils\Exception\QuoteNotFoundException;
26
use PagaMasTarde\ModuleUtils\Exception\UnknownException;
27
use PagaMasTarde\ModuleUtils\Exception\WrongStatusException;
28
use PagaMasTarde\ModuleUtils\Model\Response\JsonSuccessResponse;
29
use PagaMasTarde\ModuleUtils\Model\Response\JsonExceptionResponse;
30
use PagaMasTarde\ModuleUtils\Exception\AlreadyProcessedException;
31
use PagaMasTarde\ModuleUtils\Model\Log\LogEntry;
32
33
/**
34
 * Class Index
35
 * @package DigitalOrigin\Pmt\Controller\Notify
36
 */
37
class Index extends Action
38
{
39
    /** Orders tablename */
40
    const ORDERS_TABLE = 'cart_process';
41
42
    /** Concurrency tablename */
43
    const CONCURRENCY_TABLE = 'pmt_orders';
44
45
    /** Concurrency tablename */
46
    const LOGS_TABLE = 'pmt_logs';
47
48
    /** Payment code */
49
    const PAYMENT_METHOD = 'paylater';
50
51
    /**
52
     * EXCEPTION RESPONSES
53
     */
54
    const CPO_ERR_MSG = 'Order not confirmed';
55
    const CPO_OK_MSG = 'Order confirmed';
56
57
    /** @var QuoteManagement */
58
    protected $quoteManagement;
59
60
    /** @var PaymentInterface $paymentInterface */
61
    protected $paymentInterface;
62
63
    /** @var OrderRepositoryInterface $orderRepositoryInterface */
64
    protected $orderRepositoryInterface;
65
66
    /** @var Quote $quote */
67
    protected $quote;
68
69
    /** @var QuoteRepository $quoteRepository */
70
    protected $quoteRepository;
71
72
    /** @var mixed $config */
73
    protected $config;
74
75
    /** @var mixed $quoteId */
76
    protected $quoteId;
77
78
    /** @var array $notifyResult */
79
    protected $notifyResult;
80
81
    /** @var mixed $magentoOrderId */
82
    protected $magentoOrderId;
83
84
    /** @var mixed $pmtOrder */
85
    protected $pmtOrder;
86
87
    /** @var ResourceConnection $dbObject */
88
    protected $dbObject;
89
90
    /** @var Session $checkoutSession */
91
    protected $checkoutSession;
92
93
    /** @var Client $orderClient */
94
    protected $orderClient;
95
96
    /** @var mixed $pmtOrderId */
97
    protected $pmtOrderId;
98
99
    /** @var  OrderInterface $magentoOrder */
100
    protected $magentoOrder;
101
102
    /** @var ExtraConfig $extraConfig */
103
    protected $extraConfig;
104
105
    /**
106
     * Index constructor.
107
     *
108
     * @param Context                  $context
109
     * @param Quote                    $quote
110
     * @param QuoteManagement          $quoteManagement
111
     * @param PaymentInterface         $paymentInterface
112
     * @param Config                   $config
113
     * @param QuoteRepository          $quoteRepository
114
     * @param OrderRepositoryInterface $orderRepositoryInterface
115
     * @param ResourceConnection       $dbObject
116
     * @param Session                  $checkoutSession
117
     * @param ExtraConfig              $extraConfig
118
     */
119
    public function __construct(
120
        Context $context,
121
        Quote $quote,
122
        QuoteManagement $quoteManagement,
123
        PaymentInterface $paymentInterface,
124
        Config $config,
125
        QuoteRepository $quoteRepository,
126
        OrderRepositoryInterface $orderRepositoryInterface,
127
        ResourceConnection $dbObject,
128
        Session $checkoutSession,
129
        ExtraConfig $extraConfig
130
    ) {
131
        parent::__construct($context);
132
        $this->quote = $quote;
133
        $this->quoteManagement = $quoteManagement;
134
        $this->paymentInterface = $paymentInterface;
135
        $this->extraConfig = $extraConfig->getExtraConfig();
0 ignored issues
show
Documentation Bug introduced by
It seems like $extraConfig->getExtraConfig() of type Zend_Db_Statement_Interface is incompatible with the declared type DigitalOrigin\Pmt\Helper\ExtraConfig of property $extraConfig.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
136
        $this->config = $config->getConfig();
137
        $this->quoteRepository = $quoteRepository;
138
        $this->orderRepositoryInterface = $orderRepositoryInterface;
139
        $this->dbObject = $dbObject;
140
        $this->checkoutSession = $checkoutSession;
141
    }
142
143
    //MAIN FUNCTION
144
    public function execute()
145
    {
146
        try {
147
            $this->checkConcurrency();
148
            $this->getPmtOrderId();
149
            $this->getPmtOrder();
150
            $this->checkOrderStatus();
151
            $this->checkMerchantOrderStatus();
152
            $this->validateAmount();
153
            $this->processMerchantOrder();
154
        } catch (\Exception $exception) {
155
            $jsonResponse = new JsonExceptionResponse();
156
            $jsonResponse->setMerchantOrderId($this->magentoOrderId);
157
            $jsonResponse->setPmtOrderId($this->pmtOrderId);
158
            $jsonResponse->setException($exception);
159
            $response = $jsonResponse->toJson();
160
            $this->insertLog($exception);
161
        }
162
163
        try {
164
            if (!isset($response)) {
165
                $this->confirmPmtOrder();
166
                $jsonResponse = new JsonSuccessResponse();
167
                $jsonResponse->setMerchantOrderId($this->magentoOrderId);
168
                $jsonResponse->setPmtOrderId($this->pmtOrderId);
169
            }
170
        } catch (\Exception $exception) {
171
            $this->rollbackMerchantOrder();
172
            $jsonResponse = new JsonExceptionResponse();
173
            $jsonResponse->setMerchantOrderId($this->magentoOrderId);
174
            $jsonResponse->setPmtOrderId($this->pmtOrderId);
175
            $jsonResponse->setException($exception);
176
            $jsonResponse->toJson();
177
            $this->insertLog($exception);
178
        }
179
180
        $this->unblockConcurrency(true);
181
182
        if ($_SERVER['REQUEST_METHOD'] == 'POST') {
183
            $jsonResponse->printResponse();
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $jsonResponse does not seem to be defined for all execution paths leading up to this point.
Loading history...
184
        } else {
185
            $returnUrl = $this->getRedirectUrl();
186
            $this->_redirect($returnUrl);
187
        }
188
    }
189
190
    /**
191
     * COMMON FUNCTIONS
192
     */
193
194
    private function checkConcurrency()
195
    {
196
        $this->getQuoteId();
197
        $this->checkDbTable();
198
        $this->unblockConcurrency();
199
        $this->blockConcurrency();
200
    }
201
202
    private function getMerchantOrder()
0 ignored issues
show
Unused Code introduced by
The method getMerchantOrder() is not used, and could be removed.

This check looks for private methods that have been defined, but are not used inside the class.

Loading history...
203
    {
204
        try {
205
            $this->quote = $this->quoteRepository->get($this->quoteId);
206
        } catch (\Exception $e) {
207
            throw new MerchantOrderNotFoundException();
208
        }
209
    }
210
211
    private function getPmtOrderId()
212
    {
213
        try {
214
            /** @var \Magento\Framework\DB\Adapter\AdapterInterface $dbConnection */
215
            $dbConnection     = $this->dbObject->getConnection();
216
            $tableName        = $this->dbObject->getTableName(self::ORDERS_TABLE);
217
            $query            = "select order_id from $tableName where id='$this->quoteId'";
218
            $queryResult      = $dbConnection->fetchRow($query);
219
            $this->pmtOrderId = $queryResult['order_id'];
220
            if ($this->pmtOrderId == '') {
221
                throw new NoIdentificationException();
222
            }
223
        } catch (\Exception $e) {
224
            throw new UnknownException($e->getMessage());
225
        }
226
    }
227
228
    private function getPmtOrder()
229
    {
230
        try {
231
            $this->orderClient = new Client($this->config['pmt_public_key'], $this->config['pmt_private_key']);
232
            $this->pmtOrder = $this->orderClient->getOrder($this->pmtOrderId);
233
        } catch (\Exception $e) {
234
            throw new OrderNotFoundException();
235
        }
236
    }
237
238
    private function checkOrderStatus()
239
    {
240
        try {
241
            $this->checkPmtStatus(array('AUTHORIZED'));
242
        } catch (\Exception $e) {
243
            $this->getMagentoOrderId();
244
            if ($this->magentoOrderId!='') {
245
                throw new AlreadyProcessedException();
246
            } else {
247
                throw new WrongStatusException($this->pmtOrder->getStatus());
248
            }
249
        }
250
    }
251
252
    private function checkMerchantOrderStatus()
253
    {
254
        if ($this->quote->getIsActive()=='0') {
255
            $this->getMagentoOrderId();
256
            throw new AlreadyProcessedException();
257
        }
258
    }
259
260
    private function validateAmount()
261
    {
262
        $pmtAmount = $this->pmtOrder->getShoppingCart()->getTotalAmount();
263
        $merchantAmount = intval(strval(100 * $this->quote->getGrandTotal()));
264
        if ($pmtAmount != $merchantAmount) {
265
            throw new AmountMismatchException($pmtAmount, $merchantAmount);
266
        }
267
    }
268
269
    private function processMerchantOrder()
270
    {
271
        try {
272
            $this->saveOrder();
273
            $this->updateBdInfo();
274
        } catch (\Exception $e) {
275
            throw new UnknownException($e->getMessage());
276
        }
277
    }
278
279
    private function confirmPmtOrder()
280
    {
281
        try {
282
            $this->pmtOrder = $this->orderClient->confirmOrder($this->pmtOrderId);
283
        } catch (\Exception $e) {
284
            throw new UnknownException($e->getMessage());
285
        }
286
287
        $jsonResponse = new JsonSuccessResponse();
288
        $jsonResponse->setStatusCode(200);
289
        $jsonResponse->setMerchantOrderId($this->magentoOrderId);
290
        $jsonResponse->setPmtOrderId($this->pmtOrderId);
291
        $jsonResponse->setResult(self::CPO_OK_MSG);
292
        return $jsonResponse->toJson();
293
    }
294
295
    /**
296
     * UTILS FUNCTIONS
297
     */
298
299
    /** STEP 1 CC - Check concurrency
300
     * @throws QuoteNotFoundException
301
     */
302
    private function getQuoteId()
303
    {
304
        $this->quoteId = $this->getRequest()->getParam('quoteId');
305
        if ($this->quoteId == '') {
306
            throw new QuoteNotFoundException();
307
        }
308
    }
309
310
    /**
311
     * @return \Zend_Db_Statement_Interface
312
     * @throws UnknownException
313
     */
314
    private function checkDbTable()
315
    {
316
        try {
317
            /** @var \Magento\Framework\DB\Adapter\AdapterInterface $dbConnection */
318
            $dbConnection = $this->dbObject->getConnection();
319
            $tableName    = $this->dbObject->getTableName(self::CONCURRENCY_TABLE);
320
            $query        = "CREATE TABLE IF NOT EXISTS $tableName(`id` int not null,`timestamp` int not null,PRIMARY KEY (`id`))";
321
322
            return $dbConnection->query($query);
323
        } catch (\Exception $e) {
324
            throw new UnknownException($e->getMessage());
325
        }
326
    }
327
328
    /**
329
     * @return void|\Zend_Db_Statement_Interface
330
     * @throws UnknownException
331
     */
332
    private function checkDbLogTable()
333
    {
334
        try {
335
            /** @var \Magento\Framework\DB\Adapter\AdapterInterface $dbConnection */
336
            $dbConnection = $this->dbObject->getConnection();
337
            $tableName = $this->dbObject->getTableName(self::LOGS_TABLE);
338
            if (!$dbConnection->isTableExists($tableName)) {
339
                $table = $dbConnection
340
                    ->newTable($tableName)
341
                    ->addColumn('id', Table::TYPE_SMALLINT, null, array('nullable'=>false, 'auto_increment'=>true, 'primary'=>true))
342
                    ->addColumn('log', Table::TYPE_TEXT, null, array('nullable'=>false))
343
                    ->addColumn('createdAt', Table::TYPE_TIMESTAMP, null, array('nullable'=>false, 'default'=>Table::TIMESTAMP_INIT));
344
                return $dbConnection->createTable($table);
345
            }
346
347
            return;
348
        } catch (\Exception $e) {
349
            throw new UnknownException($e->getMessage());
350
        }
351
    }
352
353
    /**
354
     * @param bool $mode
355
     *
356
     * @throws \Exception
357
     */
358
    private function unblockConcurrency($mode = false)
359
    {
360
        try {
361
            /** @var \Magento\Framework\DB\Adapter\AdapterInterface $dbConnection */
362
            $dbConnection = $this->dbObject->getConnection();
363
            $tableName    = $this->dbObject->getTableName(self::CONCURRENCY_TABLE);
364
            if ($mode == false) {
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
365
                $dbConnection->delete($tableName, "timestamp<".(time() - 5));
366
            } elseif ($this->quoteId!='') {
367
                $dbConnection->delete($tableName, "id=".$this->quoteId);
368
            }
369
        } catch (Exception $exception) {
0 ignored issues
show
Bug introduced by
The type DigitalOrigin\Pmt\Controller\Notify\Exception was not found. Did you mean Exception? If so, make sure to prefix the type with \.
Loading history...
370
            throw new ConcurrencyException();
371
        }
372
    }
373
374
    /**
375
     * @throws \Exception
376
     */
377
    private function blockConcurrency()
378
    {
379
        try {
380
            /** @var \Magento\Framework\DB\Adapter\AdapterInterface $dbConnection */
381
            $dbConnection = $this->dbObject->getConnection();
382
            $tableName    = $this->dbObject->getTableName(self::CONCURRENCY_TABLE);
383
            $dbConnection->insert($tableName, array('id'=>$this->quoteId, 'timestamp'=>time()));
384
        } catch (Exception $exception) {
385
            throw new ConcurrencyException();
386
        }
387
    }
388
389
    /** STEP 2 GMO - Get Merchant Order */
390
    /** STEP 3 GPOI - Get Pmt OrderId */
391
    /** STEP 4 GPO - Get Pmt Order */
392
    /** STEP 5 COS - Check Order Status */
393
    /**
394
     * @param $statusArray
395
     *
396
     * @throws \Exception
397
     */
398
    private function checkPmtStatus($statusArray)
399
    {
400
        $pmtStatus = array();
401
        foreach ($statusArray as $status) {
402
            $pmtStatus[] = constant("\PagaMasTarde\OrdersApiClient\Model\Order::STATUS_$status");
403
        }
404
405
        $payed = in_array($this->pmtOrder->getStatus(), $pmtStatus);
406
        if (!$payed) {
407
            throw new WrongStatusException($this->pmtOrder->getStatus());
408
        }
409
    }
410
411
    /** STEP 6 CMOS - Check Merchant Order Status */
412
    /**
413
     * @throws \Exception
414
     */
415
    private function getMagentoOrderId()
416
    {
417
        try {
418
            /** @var \Magento\Framework\DB\Adapter\AdapterInterface $dbConnection */
419
            $dbConnection = $this->dbObject->getConnection();
420
            $tableName    = $this->dbObject->getTableName(self::ORDERS_TABLE);
421
            $pmtOrderId   = $this->pmtOrderId;
422
423
            $query        = "select mg_order_id from $tableName where id='$this->quoteId' and order_id='$pmtOrderId'";
424
            $queryResult  = $dbConnection->fetchRow($query);
425
            $this->magentoOrderId = $queryResult['mg_order_id'];
426
        } catch (\Exception $e) {
427
            throw new UnknownException($e->getMessage());
428
        }
429
    }
430
431
    /** STEP 7 VA - Validate Amount */
432
    /** STEP 8 PMO - Process Merchant Order */
433
    /**
434
     * @throws UnknownException
435
     */
436
    private function saveOrder()
437
    {
438
        try {
439
            $this->paymentInterface->setMethod(self::PAYMENT_METHOD);
440
            $this->magentoOrderId = $this->quoteManagement->placeOrder($this->quoteId, $this->paymentInterface);
441
            /** @var \Magento\Sales\Api\Data\OrderInterface magentoOrder */
442
            $this->magentoOrder = $this->orderRepositoryInterface->get($this->magentoOrderId);
443
444
            if ($this->magentoOrderId == '') {
445
                throw new UnknownException('Order can not be saved');
446
            }
447
        } catch (\Exception $e) {
448
            throw new UnknownException($e->getMessage());
449
        }
450
    }
451
452
    /**
453
     * @throws UnknownException
454
     */
455
    private function updateBdInfo()
456
    {
457
        try {
458
            /** @var \Magento\Framework\DB\Adapter\AdapterInterface $dbConnection */
459
            $dbConnection = $this->dbObject->getConnection();
460
            $tableName    = $this->dbObject->getTableName(self::ORDERS_TABLE);
461
            $pmtOrderId   = $this->pmtOrder->getId();
462
            $dbConnection->update(
463
                $tableName,
464
                array('mg_order_id' => $this->magentoOrderId),
465
                "order_id='$pmtOrderId' and id='$this->quoteId'"
466
            );
467
        } catch (\Exception $e) {
468
            throw new UnknownException($e->getMessage());
469
        }
470
    }
471
472
    /** STEP 9 CPO - Confirmation Pmt Order */
473
    /**
474
     * @throws UnknownException
475
     */
476
    private function rollbackMerchantOrder()
477
    {
478
        try {
479
            $this->magentoOrder->setState(\Magento\Sales\Model\Order::STATE_PENDING_PAYMENT, true);
0 ignored issues
show
Unused Code introduced by
The call to Magento\Sales\Api\Data\OrderInterface::setState() has too many arguments starting with true. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

479
            $this->magentoOrder->/** @scrutinizer ignore-call */ 
480
                                 setState(\Magento\Sales\Model\Order::STATE_PENDING_PAYMENT, true);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
480
            $this->magentoOrder->setStatus(\Magento\Sales\Model\Order::STATE_PENDING_PAYMENT);
481
            $this->magentoOrder->save();
482
        } catch (\Exception $e) {
483
            throw new UnknownException($e->getMessage());
484
        }
485
    }
486
487
    /**
488
     * @return string
489
     */
490
    private function getRedirectUrl()
491
    {
492
        $returnUrl = 'checkout/#payment';
493
        if ($this->magentoOrderId!='') {
494
            /** @var Order $this->magentoOrder */
495
            $this->magentoOrder = $this->orderRepositoryInterface->get($this->magentoOrderId);
496
            if (!$this->_objectManager->get(\Magento\Checkout\Model\Session\SuccessValidator::class)->isValid()) {
497
                $this->checkoutSession
498
                    ->setLastOrderId($this->magentoOrderId)
499
                    ->setLastRealOrderId($this->magentoOrder->getIncrementId())
500
                    ->setLastQuoteId($this->quoteId)
501
                    ->setLastSuccessQuoteId($this->quoteId)
502
                    ->setLastOrderStatus($this->magentoOrder->getStatus());
503
            }
504
505
            //Magento status flow => https://docs.magento.com/m2/ce/user_guide/sales/order-status-workflow.html
506
            //Order Workflow => https://docs.magento.com/m2/ce/user_guide/sales/order-workflow.html
507
            $orderStatus    = strtolower($this->magentoOrder->getStatus());
508
            $acceptedStatus = array('processing', 'completed');
509
            if (in_array($orderStatus, $acceptedStatus)) {
510
                if ($this->extraConfig['PMT_OK_URL'] != '') {
511
                    $returnUrl = $this->extraConfig['PMT_OK_URL'];
512
                } else {
513
                    $returnUrl = 'checkout/onepage/success';
514
                }
515
            } else {
516
                if ($this->extraConfig['PMT_KO_URL'] != '') {
517
                    $returnUrl = $this->extraConfig['PMT_KO_URL'];
518
                } else {
519
                    $returnUrl = 'checkout/#payment';
520
                }
521
            }
522
        }
523
        return $returnUrl;
524
    }
525
526
    /**
527
     * @param $exceptionMessage
528
     *
529
     * @throws UnknownException
530
     */
531
    private function insertLog($exceptionMessage)
532
    {
533
        try {
534
            if ($exceptionMessage instanceof \Exception) {
535
                $this->checkDbLogTable();
536
                $logEntry = new LogEntry();
537
                $logEntryJson = $logEntry->error($exceptionMessage)->toJson();
538
539
                /** @var \Magento\Framework\DB\Adapter\AdapterInterface $dbConnection */
540
                $dbConnection = $this->dbObject->getConnection();
541
                $tableName    = $this->dbObject->getTableName(self::LOGS_TABLE);
542
                $dbConnection->insert($tableName, array('log' => $logEntryJson));
543
            }
544
        } catch (\Exception $e) {
545
            throw new UnknownException($e->getMessage());
546
        }
547
    }
548
}
549