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

PagantisNotifyModuleFrontController   B

Complexity

Total Complexity 45

Size/Duplication

Total Lines 372
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
eloc 155
c 1
b 0
f 1
dl 0
loc 372
rs 8.8
wmc 45

16 Methods

Rating   Name   Duplication   Size   Complexity  
A validateAmount() 0 6 2
A getMerchantOrder() 0 10 3
A getPagantisOrderId() 0 12 3
B prepareVariables() 0 34 8
A checkConcurrency() 0 5 1
A checkOrderStatus() 0 8 3
A postProcess() 0 42 4
A checkMerchantOrderStatus() 0 4 2
A getPagantisOrder() 0 6 2
A finishProcess() 0 14 4
A unblockConcurrency() 0 6 2
A blockConcurrency() 0 6 2
A processMerchantOrder() 0 17 2
A cancelProcess() 0 31 4
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
    /**
33
     * @var string $merchantOrderId
34
     */
35
    protected $merchantOrderId;
36
37
    /**
38
     * @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...
39
     */
40
    protected $merchantOrder;
41
42
    /**
43
     * @var string $pagantisOrderId
44
     */
45
    protected $pagantisOrderId;
46
47
    /**
48
     * @var \Pagantis\OrdersApiClient\Model\Order $pagantisOrder
49
     */
50
    protected $pagantisOrder;
51
52
    /**
53
     * @var Pagantis\OrdersApiClient\Client $orderClient
54
     */
55
    protected $orderClient;
56
57
    /**
58
     * @var mixed $config
59
     */
60
    protected $config;
61
62
    /**
63
     * @var Object $jsonResponse
64
     */
65
    protected $jsonResponse;
66
67
    /**
68
     * @throws Exception
69
     */
70
    public function postProcess()
71
    {
72
        try {
73
            $this->checkConcurrency();
74
            $this->getMerchantOrder();
75
            $this->getPagantisOrderId();
76
            $this->getPagantisOrder();
77
            $this->checkOrderStatus();
78
            $this->checkMerchantOrderStatus();
79
            $this->validateAmount();
80
            $this->processMerchantOrder();
81
        } catch (\Exception $exception) {
82
            $this->jsonResponse = new JsonExceptionResponse();
83
            $this->jsonResponse->setMerchantOrderId($this->merchantOrderId);
84
            $this->jsonResponse->setPagantisOrderId($this->pagantisOrderId);
85
            $this->jsonResponse->setException($exception);
86
            $response = $this->jsonResponse->toJson();
87
            return $this->cancelProcess($response);
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->cancelProcess($response) 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...
88
        }
89
90
        try {
91
            $this->jsonResponse = new JsonSuccessResponse();
92
            $this->jsonResponse->setMerchantOrderId($this->merchantOrderId);
93
            $this->jsonResponse->setPagantisOrderId($this->pagantisOrderId);
94
            $this->confirmPagantisOrder();
95
        } catch (\Exception $exception) {
96
            $this->rollbackMerchantOrder();
97
            $this->jsonResponse = new JsonExceptionResponse();
98
            $this->jsonResponse->setMerchantOrderId($this->merchantOrderId);
99
            $this->jsonResponse->setPagantisOrderId($this->pagantisOrderId);
100
            $this->jsonResponse->setException($exception);
101
            $response = $this->jsonResponse->toJson();
102
            return $this->cancelProcess($response);
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->cancelProcess($response) 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...
103
        }
104
105
        try {
106
            $this->unblockConcurrency();
107
        } catch (\Exception $exception) {
108
            // Do nothing
109
        }
110
111
        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...
112
    }
113
114
    /**
115
     * Find and init variables needed to process payment
116
     *
117
     * @throws Exception
118
     */
119
    public function prepareVariables()
120
    {
121
        $callbackOkUrl = $this->context->link->getPageLink(
122
            'order-confirmation',
123
            null,
124
            null
125
        );
126
        $callbackKoUrl = $this->context->link->getPageLink(
127
            'order',
128
            null,
129
            null,
130
            array('step'=>3)
131
        );
132
        try {
133
            $this->config = array(
134
                'urlOK' => (getenv('PAGANTIS_URL_OK') !== '') ? getenv('PAGANTIS_URL_OK') : $callbackOkUrl,
135
                'urlKO' => (getenv('PAGANTIS_URL_KO') !== '') ? getenv('PAGANTIS_URL_KO') : $callbackKoUrl,
136
                '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...
137
                'privateKey' => Configuration::get('pagantis_private_key'),
138
                '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...
139
            );
140
        } catch (\Exception $exception) {
141
            throw new ConfigurationNotFoundException();
142
        }
143
144
        $this->merchantOrderId = Tools::getValue('id_cart');
145
        if ($this->merchantOrderId == '') {
146
            throw new QuoteNotFoundException();
147
        }
148
149
150
        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...
151
            // This exception is only for Prestashop
152
            throw new UnknownException('Module may not be enabled');
153
        }
154
    }
155
156
    /**
157
     * Check the concurrency of the purchase
158
     *
159
     * @throws Exception
160
     */
161
    public function checkConcurrency()
162
    {
163
        $this->prepareVariables();
164
        $this->unblockConcurrency();
165
        $this->blockConcurrency($this->merchantOrderId);
166
    }
167
168
    /**
169
     * Retrieve the merchant order by id
170
     *
171
     * @throws Exception
172
     */
173
    public function getMerchantOrder()
174
    {
175
        try {
176
            $this->merchantOrder = new Cart($this->merchantOrderId);
177
            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...
178
                // This exception is only for Prestashop
179
                throw new UnknownException('Unable to load cart');
180
            }
181
        } catch (\Exception $exception) {
182
            throw new MerchantOrderNotFoundException();
183
        }
184
    }
185
186
    /**
187
     * Find PAGANTIS Order Id in AbstractController::PAGANTIS_ORDERS_TABLE
188
     *
189
     * @throws Exception
190
     */
191
    private function getPagantisOrderId()
192
    {
193
        try {
194
            $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...
195
                '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...
196
            );
197
198
            if (is_null($this->pagantisOrderId)) {
199
                throw new NoIdentificationException();
200
            }
201
        } catch (\Exception $exception) {
202
            throw new NoIdentificationException();
203
        }
204
    }
205
206
    /**
207
     * Find PAGANTIS Order in Orders Server using Pagantis\OrdersApiClient
208
     *
209
     * @throws Exception
210
     */
211
    private function getPagantisOrder()
212
    {
213
        $this->orderClient = new PagantisClient($this->config['publicKey'], $this->config['privateKey']);
214
        $this->pagantisOrder = $this->orderClient->getOrder($this->pagantisOrderId);
215
        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...
216
            throw new OrderNotFoundException();
217
        }
218
    }
219
220
    /**
221
     * Compare statuses of merchant order and PAGANTIS order, witch have to be the same.
222
     *
223
     * @throws Exception
224
     */
225
    public function checkOrderStatus()
226
    {
227
        if ($this->pagantisOrder->getStatus() !== PagantisModelOrder::STATUS_AUTHORIZED) {
228
            $status = '-';
229
            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...
230
                $status = $this->pagantisOrder->getStatus();
231
            }
232
            throw new WrongStatusException($status);
233
        }
234
    }
235
236
    /**
237
     * Check that the merchant order was not previously processes and is ready to be paid
238
     *
239
     * @throws Exception
240
     */
241
    public function checkMerchantOrderStatus()
242
    {
243
        if ($this->merchantOrder->orderExists() !== false) {
244
            throw new WrongStatusException('already_processed');
245
        }
246
    }
247
248
    /**
249
     * Check that the merchant order and the order in PAGANTIS have the same amount to prevent hacking
250
     *
251
     * @throws Exception
252
     */
253
    public function validateAmount()
254
    {
255
        $totalAmount = $this->pagantisOrder->getShoppingCart()->getTotalAmount();
256
        $merchantAmount = (int)((string) (100 * $this->merchantOrder->getOrderTotal(true)));
257
        if ($totalAmount != $merchantAmount) {
258
            throw new AmountMismatchException($totalAmount, $merchantAmount);
259
        }
260
    }
261
262
    /**
263
     * Process the merchant order and notify client
264
     *
265
     * @throws Exception
266
     */
267
    public function processMerchantOrder()
268
    {
269
        try {
270
            $this->module->validateOrder(
271
                $this->merchantOrderId,
272
                Configuration::get('PS_OS_PAYMENT'),
273
                $this->merchantOrder->getOrderTotal(true),
274
                $this->module->displayName,
275
                'pagantisOrderId: ' . $this->pagantisOrder->getId().
276
                'pagantisOrderStatus: '. $this->pagantisOrder->getStatus(),
277
                array('transaction_id' => $this->pagantisOrderId),
278
                null,
279
                false,
280
                $this->config['secureKey']
281
            );
282
        } catch (\Exception $exception) {
283
            throw new UnknownException($exception->getMessage());
284
        }
285
    }
286
287
    /**
288
     * Confirm the order in PAGANTIS
289
     *
290
     * @throws Exception
291
     */
292
    private function confirmPagantisOrder()
293
    {
294
        try {
295
            $this->orderClient->confirmOrder($this->pagantisOrderId);
296
        } catch (\Exception $exception) {
297
            throw new UnknownException($exception->getMessage());
298
        }
299
    }
300
301
    /**
302
     * Leave the merchant order as it was previously
303
     *
304
     * @throws Exception
305
     */
306
    public function rollbackMerchantOrder()
307
    {
308
        // Do nothing because the order is created only when the purchase was successfully
309
    }
310
311
312
    /**
313
     * Lock the concurrency to prevent duplicated inputs
314
     *
315
     * @param $orderId
316
     * @throws Exception
317
     */
318
    protected function blockConcurrency($orderId)
319
    {
320
        try {
321
            Db::getInstance()->insert('pagantis_cart_process', array('id' => $orderId, 'timestamp' => (time())));
322
        } catch (\Exception $exception) {
323
            throw new ConcurrencyException();
324
        }
325
    }
326
327
    /**
328
     * Unlock the concurrency
329
     *
330
     * @throws Exception
331
     */
332
    protected function unblockConcurrency()
333
    {
334
        try {
335
            Db::getInstance()->delete('pagantis_cart_process', 'timestamp < ' . (time() - 6));
336
        } catch (\Exception $exception) {
337
            throw new ConcurrencyException();
338
        }
339
    }
340
341
    /**
342
     * Do all the necessary actions to cancel the confirmation process in case of error
343
     * 1. Unblock concurrency
344
     * 2. Save log
345
     *
346
     * @param String|null $response Response as json
347
     *
348
     */
349
    public function cancelProcess($response = null)
350
    {
351
        sleep(5);
352
        if ($this->merchantOrder) {
353
            $id = (!is_null($this->pagantisOrder))?$this->pagantisOrder->getId():null;
354
            $status = (!is_null($this->pagantisOrder))?$this->pagantisOrder->getStatus():null;
355
            $this->module->validateOrder(
356
                $this->merchantOrderId,
357
                Configuration::get('PS_OS_ERROR'),
358
                $this->merchantOrder->getOrderTotal(true),
359
                $this->module->displayName,
360
                'pagantisOrderId: ' . $id.
361
                'pagantisOrderStatus: '. $status,
362
                null,
363
                null,
364
                false,
365
                $this->config['secureKey']
366
            );
367
        }
368
369
        $debug = debug_backtrace();
370
        $method = $debug[1]['function'];
371
        $line = $debug[1]['line'];
372
        $this->saveLog(array(
373
            'message' => $response,
374
            'method' => $method,
375
            'file' => __FILE__,
376
            'line' => $line,
377
            'code' => 200
378
        ));
379
        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...
380
    }
381
382
    /**
383
     * Redirect the request to the e-commerce or show the output in json
384
     *
385
     * @param bool $error
386
     */
387
    public function finishProcess($error = true)
388
    {
389
        if ($_SERVER['REQUEST_METHOD'] == 'POST') {
390
            $this->jsonResponse->printResponse();
391
        }
392
393
        $parameters = array(
394
            'id_cart' => $this->merchantOrderId,
395
            'key' => $this->config['secureKey'],
396
            'id_module' => $this->module->id,
397
            'id_order' => ($this->pagantisOrder)?$this->pagantisOrder->getId(): null,
398
        );
399
        $url = ($error)? $this->config['urlKO'] : $this->config['urlOK'];
400
        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...
401
    }
402
}
403