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

PagantisNotifyModuleFrontController   B

Complexity

Total Complexity 47

Size/Duplication

Total Lines 383
Duplicated Lines 0 %

Importance

Changes 3
Bugs 0 Features 1
Metric Value
eloc 161
c 3
b 0
f 1
dl 0
loc 383
rs 8.64
wmc 47

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 finishProcess() 0 14 4
A validateAmount() 0 7 2
A unblockConcurrency() 0 6 2
A blockConcurrency() 0 6 2
A processMerchantOrder() 0 17 2
A checkOrderStatus() 0 13 4
A cancelProcess() 0 31 5
A checkMerchantOrderStatus() 0 4 2
A confirmPagantisOrder() 0 6 2
A rollbackMerchantOrder() 0 2 1

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);
105
        }
106
107
        try {
108
            $this->unblockConcurrency();
109
        } catch (\Exception $exception) {
110
            // Do nothing
111
        }
112
113
        return $this->finishProcess(false);
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
            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...
234
            exit;
0 ignored issues
show
Unused Code introduced by
ExitNode is not reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

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