Passed
Pull Request — master (#34)
by Raúl
02:07
created

PagantisNotifyModuleFrontController   B

Complexity

Total Complexity 49

Size/Duplication

Total Lines 395
Duplicated Lines 0 %

Importance

Changes 6
Bugs 0 Features 1
Metric Value
wmc 49
eloc 171
c 6
b 0
f 1
dl 0
loc 395
rs 8.48

16 Methods

Rating   Name   Duplication   Size   Complexity  
A getMerchantOrder() 0 10 3
A getPagantisOrderId() 0 12 3
A getPagantisOrder() 0 6 2
B prepareVariables() 0 37 8
A checkConcurrency() 0 5 1
A postProcess() 0 40 4
A validateAmount() 0 7 2
A processMerchantOrder() 0 17 2
A checkOrderStatus() 0 15 4
A checkMerchantOrderStatus() 0 4 2
A confirmPagantisOrder() 0 17 4
A rollbackMerchantOrder() 0 2 1
A finishProcess() 0 14 4
A unblockConcurrency() 0 6 2
A blockConcurrency() 0 6 2
A cancelProcess() 0 31 5

How to fix   Complexity   

Complex Class

Complex classes like PagantisNotifyModuleFrontController often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use PagantisNotifyModuleFrontController, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * This file is part of the official Pagantis module for PrestaShop.
4
 *
5
 * @author    Pagantis <[email protected]>
6
 * @copyright 2019 Pagantis
7
 * @license   proprietary
8
 */
9
10
require_once('AbstractController.php');
11
12
use Pagantis\OrdersApiClient\Client as PagantisClient;
13
use Pagantis\OrdersApiClient\Model\Order as PagantisModelOrder;
14
use Pagantis\ModuleUtils\Exception\AmountMismatchException;
15
use Pagantis\ModuleUtils\Exception\ConcurrencyException;
16
use Pagantis\ModuleUtils\Exception\MerchantOrderNotFoundException;
17
use Pagantis\ModuleUtils\Exception\NoIdentificationException;
18
use Pagantis\ModuleUtils\Exception\OrderNotFoundException;
19
use Pagantis\ModuleUtils\Exception\QuoteNotFoundException;
20
use Pagantis\ModuleUtils\Exception\ConfigurationNotFoundException;
21
use Pagantis\ModuleUtils\Exception\UnknownException;
22
use Pagantis\ModuleUtils\Exception\WrongStatusException;
23
use Pagantis\ModuleUtils\Model\Response\JsonSuccessResponse;
24
use Pagantis\ModuleUtils\Model\Response\JsonExceptionResponse;
25
26
/**
27
 * Class PagantisNotifyModuleFrontController
28
 */
29
class PagantisNotifyModuleFrontController extends AbstractController
30
{
31
    /**
32
     * @var bool $processError
33
     */
34
    protected $processError;
35
36
    /**
37
     * @var string $merchantOrderId
38
     */
39
    protected $merchantOrderId;
40
41
    /**
42
     * @var \Cart $merchantOrder
0 ignored issues
show
Bug introduced by
The type Cart 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...
43
     */
44
    protected $merchantOrder;
45
46
    /**
47
     * @var string $pagantisOrderId
48
     */
49
    protected $pagantisOrderId;
50
51
    /**
52
     * @var \Pagantis\OrdersApiClient\Model\Order $pagantisOrder
53
     */
54
    protected $pagantisOrder;
55
56
    /**
57
     * @var Pagantis\OrdersApiClient\Client $orderClient
58
     */
59
    protected $orderClient;
60
61
    /**
62
     * @var mixed $config
63
     */
64
    protected $config;
65
66
    /**
67
     * @var Object $jsonResponse
68
     */
69
    protected $jsonResponse;
70
71
    /**
72
     * @throws Exception
73
     */
74
    public function postProcess()
75
    {
76
        try {
77
            $this->checkConcurrency();
78
            $this->getMerchantOrder();
79
            $this->getPagantisOrderId();
80
            $this->getPagantisOrder();
81
            $this->checkOrderStatus();
82
            $this->checkMerchantOrderStatus();
83
            $this->validateAmount();
84
            $this->processMerchantOrder();
85
        } catch (\Exception $exception) {
86
            $this->jsonResponse = new JsonExceptionResponse();
87
            $this->jsonResponse->setMerchantOrderId($this->merchantOrderId);
88
            $this->jsonResponse->setPagantisOrderId($this->pagantisOrderId);
89
            $this->jsonResponse->setException($exception);
90
            return $this->cancelProcess($this->jsonResponse);
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->cancelProcess($this->jsonResponse) targeting PagantisNotifyModuleFron...roller::cancelProcess() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
Bug introduced by
$this->jsonResponse of type Pagantis\ModuleUtils\Mod...e\JsonExceptionResponse is incompatible with the type null|string expected by parameter $response of PagantisNotifyModuleFron...roller::cancelProcess(). ( Ignorable by Annotation )

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

90
            return $this->cancelProcess(/** @scrutinizer ignore-type */ $this->jsonResponse);
Loading history...
91
        }
92
93
        try {
94
            $this->jsonResponse = new JsonSuccessResponse();
95
            $this->jsonResponse->setMerchantOrderId($this->merchantOrderId);
96
            $this->jsonResponse->setPagantisOrderId($this->pagantisOrderId);
97
            $this->confirmPagantisOrder();
98
        } catch (\Exception $exception) {
99
            $this->rollbackMerchantOrder();
100
            $this->jsonResponse = new JsonExceptionResponse();
101
            $this->jsonResponse->setMerchantOrderId($this->merchantOrderId);
102
            $this->jsonResponse->setPagantisOrderId($this->pagantisOrderId);
103
            $this->jsonResponse->setException($exception);
104
            return $this->cancelProcess($this->jsonResponse);
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->cancelProcess($this->jsonResponse) targeting PagantisNotifyModuleFron...roller::cancelProcess() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
105
        }
106
107
        try {
108
            $this->unblockConcurrency();
109
        } catch (\Exception $exception) {
110
            // Do nothing
111
        }
112
113
        return $this->finishProcess(false);
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->finishProcess(false) targeting PagantisNotifyModuleFron...roller::finishProcess() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
114
    }
115
116
    /**
117
     * Check the concurrency of the purchase
118
     *
119
     * @throws Exception
120
     */
121
    public function checkConcurrency()
122
    {
123
        $this->prepareVariables();
124
        $this->unblockConcurrency();
125
        $this->blockConcurrency($this->merchantOrderId);
126
    }
127
128
    /**
129
     * Find and init variables needed to process payment
130
     *
131
     * @throws Exception
132
     */
133
    public function prepareVariables()
134
    {
135
        $this->processError = false;
136
        $callbackOkUrl = $this->context->link->getPageLink(
137
            'order-confirmation',
138
            null,
139
            null
140
        );
141
        $callbackKoUrl = $this->context->link->getPageLink(
142
            'order',
143
            null,
144
            null,
145
            array('step'=>3)
146
        );
147
        try {
148
            $this->config = array(
149
                'urlOK' => (Pagantis::getExtraConfig('PAGANTIS_URL_OK') !== '') ?
150
                    Pagantis::getExtraConfig('PAGANTIS_URL_OK') : $callbackOkUrl,
151
                'urlKO' => (Pagantis::getExtraConfig('PAGANTIS_URL_KO') !== '') ?
152
                    Pagantis::getExtraConfig('PAGANTIS_URL_KO') : $callbackKoUrl,
153
                'publicKey' => Configuration::get('pagantis_public_key'),
0 ignored issues
show
Bug introduced by
The type Configuration 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...
154
                'privateKey' => Configuration::get('pagantis_private_key'),
155
                'secureKey' => Tools::getValue('key'),
0 ignored issues
show
Bug introduced by
The type Tools 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...
156
            );
157
        } catch (\Exception $exception) {
158
            throw new ConfigurationNotFoundException();
159
        }
160
161
        $this->merchantOrderId = Tools::getValue('id_cart');
162
        if ($this->merchantOrderId == '') {
163
            throw new QuoteNotFoundException();
164
        }
165
166
167
        if (!($this->config['secureKey'] && $this->merchantOrderId && Module::isEnabled(self::PAGANTIS_CODE))) {
0 ignored issues
show
Bug introduced by
The type Module 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...
168
            // This exception is only for Prestashop
169
            throw new UnknownException('Module may not be enabled');
170
        }
171
    }
172
173
    /**
174
     * Retrieve the merchant order by id
175
     *
176
     * @throws Exception
177
     */
178
    public function getMerchantOrder()
179
    {
180
        try {
181
            $this->merchantOrder = new Cart($this->merchantOrderId);
182
            if (!Validate::isLoadedObject($this->merchantOrder)) {
0 ignored issues
show
Bug introduced by
The type Validate 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...
183
                // This exception is only for Prestashop
184
                throw new UnknownException('Unable to load cart');
185
            }
186
        } catch (\Exception $exception) {
187
            throw new MerchantOrderNotFoundException();
188
        }
189
    }
190
191
    /**
192
     * Find PAGANTIS Order Id in AbstractController::PAGANTIS_ORDERS_TABLE
193
     *
194
     * @throws Exception
195
     */
196
    private function getPagantisOrderId()
197
    {
198
        try {
199
            $this->pagantisOrderId= Db::getInstance()->getValue(
0 ignored issues
show
Bug introduced by
The type Db 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...
200
                'select order_id from '._DB_PREFIX_.'pagantis_order where id = '.$this->merchantOrderId
0 ignored issues
show
Bug introduced by
The constant _DB_PREFIX_ was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
201
            );
202
203
            if (is_null($this->pagantisOrderId)) {
204
                throw new NoIdentificationException();
205
            }
206
        } catch (\Exception $exception) {
207
            throw new NoIdentificationException();
208
        }
209
    }
210
211
    /**
212
     * Find PAGANTIS Order in Orders Server using Pagantis\OrdersApiClient
213
     *
214
     * @throws Exception
215
     */
216
    private function getPagantisOrder()
217
    {
218
        $this->orderClient = new PagantisClient($this->config['publicKey'], $this->config['privateKey']);
219
        $this->pagantisOrder = $this->orderClient->getOrder($this->pagantisOrderId);
220
        if (!($this->pagantisOrder instanceof PagantisModelOrder)) {
0 ignored issues
show
introduced by
$this->pagantisOrder is always a sub-type of Pagantis\OrdersApiClient\Model\Order.
Loading history...
221
            throw new OrderNotFoundException();
222
        }
223
    }
224
225
    /**
226
     * Compare statuses of merchant order and PAGANTIS order, witch have to be the same.
227
     *
228
     * @throws Exception
229
     */
230
    public function checkOrderStatus()
231
    {
232
        if ($this->pagantisOrder->getStatus() === PagantisModelOrder::STATUS_CONFIRMED) {
233
            $this->jsonResponse = new JsonSuccessResponse();
234
            $this->jsonResponse->setMerchantOrderId($this->merchantOrderId);
235
            $this->jsonResponse->setPmtOrderId($this->pagantisOrderId);
0 ignored issues
show
Bug introduced by
The method setPmtOrderId() does not exist on Pagantis\ModuleUtils\Mod...nse\JsonSuccessResponse. ( Ignorable by Annotation )

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

235
            $this->jsonResponse->/** @scrutinizer ignore-call */ 
236
                                 setPmtOrderId($this->pagantisOrderId);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
236
            return $this->finishProcess(false);
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->finishProcess(false) targeting PagantisNotifyModuleFron...roller::finishProcess() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
237
        }
238
239
        if ($this->pagantisOrder->getStatus() !== PagantisModelOrder::STATUS_AUTHORIZED) {
240
            $status = '-';
241
            if ($this->pagantisOrder instanceof \Pagantis\OrdersApiClient\Model\Order) {
0 ignored issues
show
introduced by
$this->pagantisOrder is always a sub-type of Pagantis\OrdersApiClient\Model\Order.
Loading history...
242
                $status = $this->pagantisOrder->getStatus();
243
            }
244
            throw new WrongStatusException($status);
245
        }
246
    }
247
248
    /**
249
     * Check that the merchant order was not previously processes and is ready to be paid
250
     *
251
     * @throws Exception
252
     */
253
    public function checkMerchantOrderStatus()
254
    {
255
        if ($this->merchantOrder->orderExists() !== false) {
256
            throw new WrongStatusException('already_processed');
257
        }
258
    }
259
260
    /**
261
     * Check that the merchant order and the order in PAGANTIS have the same amount to prevent hacking
262
     *
263
     * @throws Exception
264
     */
265
    public function validateAmount()
266
    {
267
        $totalAmount = $this->pagantisOrder->getShoppingCart()->getTotalAmount();
268
        $merchantAmount = (int) (100 * $this->merchantOrder->getOrderTotal(true));
269
        if ($totalAmount != $merchantAmount) {
270
            $this->processError = true;
271
            throw new AmountMismatchException($totalAmount, $merchantAmount);
272
        }
273
    }
274
275
    /**
276
     * Process the merchant order and notify client
277
     *
278
     * @throws Exception
279
     */
280
    public function processMerchantOrder()
281
    {
282
        try {
283
            $this->module->validateOrder(
284
                $this->merchantOrderId,
285
                Configuration::get('PS_OS_PAYMENT'),
286
                $this->merchantOrder->getOrderTotal(true),
287
                $this->module->displayName,
288
                'pagantisOrderId: ' . $this->pagantisOrder->getId().
289
                'pagantisOrderStatus: '. $this->pagantisOrder->getStatus(),
290
                array('transaction_id' => $this->pagantisOrderId),
291
                null,
292
                false,
293
                $this->config['secureKey']
294
            );
295
        } catch (\Exception $exception) {
296
            throw new UnknownException($exception->getMessage());
297
        }
298
    }
299
300
    /**
301
     * Confirm the order in PAGANTIS
302
     *
303
     * @throws Exception
304
     */
305
    private function confirmPagantisOrder()
306
    {
307
        try {
308
            $this->orderClient->confirmOrder($this->pagantisOrderId);
309
            try {
310
                $mode = ($_SERVER['REQUEST_METHOD'] == 'POST') ? 'NOTIFICATION' : 'REDIRECTION';
311
                $message = 'Order CONFIRMED. The order was confirmed by a ' . $mode .
312
                    '. Pagantis OrderId=' . $this->pagantisOrderId .
313
                    '. Prestashop OrderId=' . $this->merchantOrderId;
314
                $this->saveLog(array(
315
                    'message' => $message
316
                ));
317
            } catch (\Exception $e) {
318
                // Do nothing
319
            }
320
        } catch (\Exception $exception) {
321
            throw new UnknownException($exception->getMessage());
322
        }
323
    }
324
325
    /**
326
     * Leave the merchant order as it was previously
327
     *
328
     * @throws Exception
329
     */
330
    public function rollbackMerchantOrder()
331
    {
332
        // Do nothing because the order is created only when the purchase was successfully
333
    }
334
335
    /**
336
     * Lock the concurrency to prevent duplicated inputs
337
     *
338
     * @param $orderId
339
     * @throws Exception
340
     */
341
    protected function blockConcurrency($orderId)
342
    {
343
        try {
344
            Db::getInstance()->insert('pagantis_cart_process', array('id' => $orderId, 'timestamp' => (time())));
345
        } catch (\Exception $exception) {
346
            throw new ConcurrencyException();
347
        }
348
    }
349
350
    /**
351
     * Unlock the concurrency
352
     *
353
     * @throws Exception
354
     */
355
    protected function unblockConcurrency()
356
    {
357
        try {
358
            Db::getInstance()->delete('pagantis_cart_process', 'timestamp < ' . (time() - 6));
359
        } catch (\Exception $exception) {
360
            throw new ConcurrencyException();
361
        }
362
    }
363
364
    /**
365
     * Do all the necessary actions to cancel the confirmation process in case of error
366
     * 1. Unblock concurrency
367
     * 2. Save log
368
     *
369
     * @param String|null $response Response as json
370
     *
371
     */
372
    public function cancelProcess($response = null)
373
    {
374
        if ($this->merchantOrder && $this->processError === true) {
375
            sleep(5);
376
            $id = (!is_null($this->pagantisOrder))?$this->pagantisOrder->getId():null;
377
            $status = (!is_null($this->pagantisOrder))?$this->pagantisOrder->getStatus():null;
378
            $this->module->validateOrder(
379
                $this->merchantOrderId,
380
                Configuration::get('PS_OS_ERROR'),
381
                $this->merchantOrder->getOrderTotal(true),
382
                $this->module->displayName,
383
                ' pagantisOrderId: ' . $id.
384
                ' pagantisOrderStatus: '. $status,
385
                null,
386
                null,
387
                false,
388
                $this->config['secureKey']
389
            );
390
        }
391
392
        $debug = debug_backtrace();
393
        $method = $debug[1]['function'];
394
        $line = $debug[1]['line'];
395
        $this->saveLog(array(
396
            'message' => $response,
397
            'method' => $method,
398
            'file' => __FILE__,
399
            'line' => $line,
400
            'code' => 200
401
        ));
402
        return $this->finishProcess(true);
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->finishProcess(true) targeting PagantisNotifyModuleFron...roller::finishProcess() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
403
    }
404
405
    /**
406
     * Redirect the request to the e-commerce or show the output in json
407
     *
408
     * @param bool $error
409
     */
410
    public function finishProcess($error = true)
411
    {
412
        if ($_SERVER['REQUEST_METHOD'] == 'POST') {
413
            $this->jsonResponse->printResponse();
414
        }
415
416
        $parameters = array(
417
            'id_cart' => $this->merchantOrderId,
418
            'key' => $this->config['secureKey'],
419
            'id_module' => $this->module->id,
420
            'id_order' => ($this->pagantisOrder)?$this->pagantisOrder->getId(): null,
421
        );
422
        $url = ($error)? $this->config['urlKO'] : $this->config['urlOK'];
423
        return $this->redirect($url, $parameters);
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->redirect($url, $parameters) targeting AbstractController::redirect() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
424
    }
425
}
426