Passed
Pull Request — master (#64)
by Raúl
04:30
created

PagantisNotifyModuleFrontController::checkOrderStatus()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 17
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 4
Bugs 0 Features 0
Metric Value
cc 4
eloc 11
c 4
b 0
f 0
nc 4
nop 0
dl 0
loc 17
rs 9.9
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\ConcurrencyException;
15
use Pagantis\ModuleUtils\Exception\MerchantOrderNotFoundException;
16
use Pagantis\ModuleUtils\Exception\NoIdentificationException;
17
use Pagantis\ModuleUtils\Exception\OrderNotFoundException;
18
use Pagantis\ModuleUtils\Exception\QuoteNotFoundException;
19
use Pagantis\ModuleUtils\Exception\ConfigurationNotFoundException;
20
use Pagantis\ModuleUtils\Exception\UnknownException;
21
use Pagantis\ModuleUtils\Exception\WrongStatusException;
22
use Pagantis\ModuleUtils\Model\Response\JsonSuccessResponse;
23
use Pagantis\ModuleUtils\Model\Response\JsonExceptionResponse;
24
25
/**
26
 * Class PagantisNotifyModuleFrontController
27
 */
28
class PagantisNotifyModuleFrontController extends AbstractController
29
{
30
    /**
31
     * Seconds to expire a locked request
32
     */
33
    const CONCURRENCY_TIMEOUT = 10;
34
35
    /**
36
     * @var string $merchantOrderId
37
     */
38
    protected $merchantOrderId;
39
40
    /**
41
     * @var \Cart $merchantOrder
42
     */
43
    protected $merchantOrder;
44
45
    /**
46
     * @var string $pagantisOrderId
47
     */
48
    protected $pagantisOrderId;
49
50
    /**
51
     * @var string $amountMismatchError
52
     */
53
    protected $amountMismatchError = '';
54
55
    /**
56
     * @var \Pagantis\OrdersApiClient\Model\Order $pagantisOrder
57
     */
58
    protected $pagantisOrder;
59
60
    /**
61
     * @var Pagantis\OrdersApiClient\Client $orderClient
62
     */
63
    protected $orderClient;
64
65
    /**
66
     * @var mixed $config
67
     */
68
    protected $config;
69
70
    /**
71
     * @var Object $jsonResponse
72
     */
73
    protected $jsonResponse;
74
75
    /**
76
     * @throws Exception
77
     */
78
    public function postProcess()
79
    {
80
        try {
81
            if ($_SERVER['REQUEST_METHOD'] == 'POST') {
82
                // prevent colision between POST and GET requests
83
                sleep(15);
84
            }
85
            if (Tools::getValue('origin') == 'notification' && $_SERVER['REQUEST_METHOD'] == 'GET') {
86
                return $this->cancelProcess();
87
            }
88
            $this->prepareVariables();
89
            $this->checkConcurrency();
90
            $this->getMerchantOrder();
91
            $this->getPagantisOrderId();
92
            $this->getPagantisOrder();
93
            if ($this->checkOrderStatus()) {
94
                return $this->finishProcess(false);
95
            }
96
            $this->validateAmount();
97
            if ($this->checkMerchantOrderStatus()) {
98
                $this->processMerchantOrder();
99
            }
100
        } catch (\Exception $exception) {
101
            if ($_SERVER['REQUEST_METHOD'] == 'POST') {
102
                $this->jsonResponse = new JsonExceptionResponse();
103
                $this->jsonResponse->setMerchantOrderId($this->merchantOrderId);
104
                $this->jsonResponse->setPagantisOrderId($this->pagantisOrderId);
105
                $this->jsonResponse->setException($exception);
106
            }
107
            return $this->cancelProcess($exception);
108
        }
109
110
        try {
111
            $this->jsonResponse = new JsonSuccessResponse();
112
            $this->jsonResponse->setMerchantOrderId($this->merchantOrderId);
113
            $this->jsonResponse->setPagantisOrderId($this->pagantisOrderId);
114
            $this->confirmPagantisOrder();
115
        } catch (\Exception $exception) {
116
            $this->rollbackMerchantOrder();
117
            if ($_SERVER['REQUEST_METHOD'] == 'POST') {
118
                $this->jsonResponse = new JsonExceptionResponse();
119
                $this->jsonResponse->setMerchantOrderId($this->merchantOrderId);
120
                $this->jsonResponse->setPagantisOrderId($this->pagantisOrderId);
121
                $this->jsonResponse->setException($exception);
122
            }
123
            return $this->cancelProcess($exception);
124
        }
125
126
        try {
127
            $this->unblockConcurrency($this->merchantOrderId);
128
        } catch (\Exception $exception) {
129
            // Do nothing
130
        }
131
132
        return $this->finishProcess(false);
133
    }
134
135
    /**
136
     * Check the concurrency of the purchase
137
     *
138
     * @throws Exception
139
     */
140
    public function checkConcurrency()
141
    {
142
        $this->unblockConcurrency();
143
        $this->blockConcurrency($this->merchantOrderId);
144
    }
145
146
    /**
147
     * Find and init variables needed to process payment
148
     *
149
     * @throws Exception
150
     */
151
    public function prepareVariables()
152
    {
153
        $callbackOkUrl = $this->context->link->getPageLink(
154
            'order-confirmation',
155
            null,
156
            null
157
        );
158
        $callbackKoUrl = $this->context->link->getPageLink(
159
            'order',
160
            null,
161
            null,
162
            array('step'=>3)
163
        );
164
        try {
165
            $this->config = array(
166
                'urlOK' => (Pagantis::getExtraConfig('PAGANTIS_URL_OK') !== '') ?
167
                    Pagantis::getExtraConfig('PAGANTIS_URL_OK') : $callbackOkUrl,
168
                'urlKO' => (Pagantis::getExtraConfig('PAGANTIS_URL_KO') !== '') ?
169
                    Pagantis::getExtraConfig('PAGANTIS_URL_KO') : $callbackKoUrl,
170
                'publicKey' => Configuration::get('pagantis_public_key'),
171
                'privateKey' => Configuration::get('pagantis_private_key'),
172
                'secureKey' => Tools::getValue('key'),
173
            );
174
        } catch (\Exception $exception) {
175
            throw new ConfigurationNotFoundException();
176
        }
177
178
        $this->merchantOrderId = Tools::getValue('id_cart');
179
        if ($this->merchantOrderId == '') {
180
            throw new QuoteNotFoundException();
181
        }
182
183
184
        if (!($this->config['secureKey'] && $this->merchantOrderId && Module::isEnabled(self::PAGANTIS_CODE))) {
185
            // This exception is only for Prestashop
186
            throw new UnknownException('Module may not be enabled');
187
        }
188
    }
189
190
    /**
191
     * Retrieve the merchant order by id
192
     *
193
     * @throws Exception
194
     */
195
    public function getMerchantOrder()
196
    {
197
        try {
198
            $this->merchantOrder = new Cart($this->merchantOrderId);
199
            if (!Validate::isLoadedObject($this->merchantOrder)) {
200
                // This exception is only for Prestashop
201
                throw new UnknownException('Unable to load cart');
202
            }
203
        } catch (\Exception $exception) {
204
            throw new MerchantOrderNotFoundException();
205
        }
206
    }
207
208
    /**
209
     * Find PAGANTIS Order Id in AbstractController::PAGANTIS_ORDERS_TABLE
210
     *
211
     * @throws Exception
212
     */
213
    private function getPagantisOrderId()
214
    {
215
        try {
216
            $this->pagantisOrderId= Db::getInstance()->getValue(
217
                'select order_id from '._DB_PREFIX_.'pagantis_order where id = '.$this->merchantOrderId
218
            );
219
220
            if (is_null($this->pagantisOrderId)) {
221
                throw new NoIdentificationException();
222
            }
223
        } catch (\Exception $exception) {
224
            throw new NoIdentificationException();
225
        }
226
    }
227
228
    /**
229
     * Find PAGANTIS Order in Orders Server using Pagantis\OrdersApiClient
230
     *
231
     * @throws Exception
232
     */
233
    private function getPagantisOrder()
234
    {
235
        $this->orderClient = new PagantisClient($this->config['publicKey'], $this->config['privateKey']);
236
        $this->pagantisOrder = $this->orderClient->getOrder($this->pagantisOrderId);
237
        if (!($this->pagantisOrder instanceof PagantisModelOrder)) {
238
            throw new OrderNotFoundException();
239
        }
240
    }
241
242
    /**
243
     * Compare statuses of merchant order and PAGANTIS order, witch have to be the same.
244
     *
245
     * @throws Exception
246
     */
247
    public function checkOrderStatus()
248
    {
249
        if ($this->pagantisOrder->getStatus() === PagantisModelOrder::STATUS_CONFIRMED) {
250
            $this->jsonResponse = new JsonSuccessResponse();
251
            $this->jsonResponse->setMerchantOrderId($this->merchantOrderId);
252
            $this->jsonResponse->setPagantisOrderId($this->pagantisOrderId);
253
            return true;
254
        }
255
256
        if ($this->pagantisOrder->getStatus() !== PagantisModelOrder::STATUS_AUTHORIZED) {
257
            $status = '-';
258
            if ($this->pagantisOrder instanceof \Pagantis\OrdersApiClient\Model\Order) {
259
                $status = $this->pagantisOrder->getStatus();
260
            }
261
            throw new WrongStatusException($status);
262
        }
263
        return false;
264
    }
265
266
    /**
267
     * Check that the merchant order and the order in PAGANTIS have the same amount to prevent hacking
268
     *
269
     * @throws Exception
270
     */
271
    public function validateAmount()
272
    {
273
        $totalAmount = (string) $this->pagantisOrder->getShoppingCart()->getTotalAmount();
274
        $merchantAmount = (string) (100 * $this->merchantOrder->getOrderTotal(true));
275
        $merchantAmount = explode('.', explode(',', $merchantAmount)[0])[0];
276
        if ($totalAmount != $merchantAmount) {
277
            try {
278
                $psTotalAmount = substr_replace($merchantAmount, '.', (Tools::strlen($merchantAmount) -2), 0);
279
280
                $pgTotalAmountInCents = (string) $this->pagantisOrder->getShoppingCart()->getTotalAmount();
281
                $pgTotalAmount = substr_replace(
282
                    $pgTotalAmountInCents,
283
                    '.',
284
                    (Tools::strlen($pgTotalAmountInCents) -2),
285
                    0
286
                );
287
288
                $this->amountMismatchError = '. Amount mismatch in PrestaShop Order #'. $this->merchantOrderId .
289
                    ' compared with Pagantis Order: ' . $this->pagantisOrderId .
290
                    '. The order in PrestaShop has an amount of ' . $psTotalAmount . ' and in Pagantis ' .
291
                    $pgTotalAmount . ' PLEASE REVIEW THE ORDER';
292
                $this->saveLog(array(
293
                    'message' => $this->amountMismatchError
294
                ));
295
            } catch (\Exception $exception) {
296
                // Do nothing
297
            }
298
        }
299
    }
300
301
    /**
302
     * Check that the merchant order was not previously processes and is ready to be paid
303
     *
304
     * @throws Exception
305
     */
306
    public function checkMerchantOrderStatus()
307
    {
308
        try {
309
            if ($this->merchantOrder->orderExists() !== false) {
310
                throw new WrongStatusException('PS->orderExists() cart_id = '
311
                    . $this->merchantOrderId . ' pagantis_id = '
312
                    . $this->pagantisOrderId . '): already_processed');
313
            }
314
315
            // Double check
316
            $tableName = _DB_PREFIX_ . 'pagantis_order';
317
            $sql = ('select ps_order_id from `' . $tableName . '` where `id` = ' . $this->merchantOrderId
318
                . ' and `order_id` = \'' . $this->pagantisOrderId . '\''
319
                . ' and `ps_order_id` is not null');
320
            $results = Db::getInstance()->ExecuteS($sql);
321
            if (is_array($results) && count($results) === 1) {
322
                throw new WrongStatusException('PS->record found in ' . $tableName
323
                    . ' (cart_id = ' . $this->merchantOrderId . ' pagantis_id = '
324
                    . $this->pagantisOrderId . '): already_processed');
325
            }
326
        } catch (\Exception $exception) {
327
            throw new UnknownException($exception->getMessage());
328
        }
329
        return true;
330
    }
331
332
    /**
333
     * Process the merchant order and notify client
334
     *
335
     * @throws Exception
336
     */
337
    public function processMerchantOrder()
338
    {
339
        try {
340
            $metadataOrder = $this->pagantisOrder->getMetadata();
341
            $metadataInfo = '';
342
            foreach ($metadataOrder as $metadataKey => $metadataValue) {
343
                if ($metadataKey == 'promotedProduct') {
344
                    $metadataInfo .= $metadataValue;
345
                }
346
            }
347
348
            $this->module->validateOrder(
349
                $this->merchantOrderId,
350
                Configuration::get('PS_OS_PAYMENT'),
351
                $this->merchantOrder->getOrderTotal(true),
352
                $this->module->displayName,
353
                'pagantisOrderId: ' . $this->pagantisOrder->getId() . ' ' .
354
                'pagantisOrderStatus: '. $this->pagantisOrder->getStatus() .
355
                $this->amountMismatchError .
356
                $metadataInfo,
357
                array('transaction_id' => $this->pagantisOrderId),
358
                null,
359
                false,
360
                $this->config['secureKey']
361
            );
362
        } catch (\Exception $exception) {
363
            throw new UnknownException($exception->getMessage());
364
        }
365
        try {
366
            Db::getInstance()->update(
367
                'pagantis_order',
368
                array('ps_order_id' => $this->module->currentOrder),
369
                'id = \''. $this->merchantOrderId . '\' and order_id = \'' . $this->pagantisOrderId . '\''
370
            );
371
        } catch (\Exception $exception) {
372
            // Do nothing
373
        }
374
    }
375
376
    /**
377
     * Confirm the order in PAGANTIS
378
     *
379
     * @throws Exception
380
     */
381
    private function confirmPagantisOrder()
382
    {
383
        try {
384
            $this->orderClient->confirmOrder($this->pagantisOrderId);
385
            try {
386
                $mode = ($_SERVER['REQUEST_METHOD'] == 'POST') ? 'NOTIFICATION' : 'REDIRECTION';
387
                $message = 'Order CONFIRMED. The order was confirmed by a ' . $mode .
388
                    '. Pagantis OrderId=' . $this->pagantisOrderId .
389
                    '. Prestashop OrderId=' . $this->module->currentOrder;
390
                $this->saveLog(array('message' => $message));
391
            } catch (\Exception $exception) {
392
                // Do nothing
393
            }
394
        } catch (\Exception $exception) {
395
            throw new UnknownException($exception->getMessage());
396
        }
397
    }
398
399
    /**
400
     * Leave the merchant order as it was previously
401
     *
402
     * @throws Exception
403
     */
404
    public function rollbackMerchantOrder()
405
    {
406
        // Do nothing because the order is created only when the purchase was successfully
407
        try {
408
            $message = 'Roolback method: ' .
409
                '. Pagantis OrderId=' . $this->pagantisOrderId .
410
                '. Prestashop CartId=' . $this->merchantOrderId;
411
            $this->saveLog(array('message' => $message));
412
            }
413
        } catch (\Exception $exception) {
0 ignored issues
show
Bug introduced by
A parse error occurred: Cannot use try without catch or finally
Loading history...
414
            // Do nothing
415
        }
416
    }
417
418
    /**
419
     * Lock the concurrency to prevent duplicated inputs
420
     *
421
     * @param $orderId
422
     * @return bool|void
423
     * @throws ConcurrencyException
424
     */
425
    protected function blockConcurrency($orderId)
426
    {
427
        try {
428
            $table = 'pagantis_cart_process';
429
            if (Db::getInstance()->insert($table, array('id' => $orderId, 'timestamp' => (time()))) === false) {
430
                if ($_SERVER['REQUEST_METHOD'] == 'POST') {
431
                    throw new ConcurrencyException();
432
                }
433
434
                $query = sprintf(
435
                    "SELECT TIMESTAMPDIFF(SECOND,NOW()-INTERVAL %s SECOND, FROM_UNIXTIME(timestamp)) as rest FROM %s WHERE %s",
436
                    self::CONCURRENCY_TIMEOUT,
437
                    _DB_PREFIX_.$table,
438
                    "id=$orderId"
439
                );
440
                $resultSeconds = Db::getInstance()->getValue($query);
441
                $restSeconds = isset($resultSeconds) ? ($resultSeconds) : 0;
442
                $secondsToExpire = ($restSeconds>self::CONCURRENCY_TIMEOUT) ? self::CONCURRENCY_TIMEOUT : $restSeconds;
443
444
                $logMessage = sprintf(
445
                    "Redirect concurrency, User have to wait %s seconds, default seconds %s, bd time to expire %s seconds. CartId=" . $orderId,
446
                    $secondsToExpire,
447
                    self::CONCURRENCY_TIMEOUT,
448
                    $restSeconds
449
                );
450
451
                $this->saveLog(array(
452
                    'message' => $logMessage
453
                ));
454
                sleep($secondsToExpire+1);
455
                // After waiting...user continue the confirmation, hoping that previous call have finished.
456
                return true;
457
            }
458
        } catch (\Exception $exception) {
459
            throw new ConcurrencyException();
460
        }
461
    }
462
463
    /**
464
     * @param null $orderId
465
     *
466
     * @throws ConcurrencyException
467
     */
468
    private function unblockConcurrency($orderId = null)
469
    {
470
        try {
471
            if (is_null($orderId)) {
472
                Db::getInstance()->delete(
473
                    'pagantis_cart_process',
474
                    'timestamp < ' . (time() - self::CONCURRENCY_TIMEOUT)
475
                );
476
                return;
477
            }
478
            Db::getInstance()->delete('pagantis_cart_process', 'id = \'' . $orderId . '\'');
479
        } catch (\Exception $exception) {
480
            throw new ConcurrencyException();
481
        }
482
    }
483
484
    /**
485
     * Do all the necessary actions to cancel the confirmation process in case of error
486
     * 1. Unblock concurrency
487
     * 2. Save log
488
     *
489
     * @param \Exception $exception
490
     *
491
     */
492
    public function cancelProcess($exception = null)
493
    {
494
        $debug = debug_backtrace();
495
        $method = $debug[1]['function'];
496
        $line = $debug[1]['line'];
497
        $data = array(
498
            'merchantOrderId' => $this->merchantOrderId,
499
            'pagantisOrderId' => $this->pagantisOrderId,
500
            'message' => ($exception)? $exception->getMessage() : 'Unable to get Exception message',
501
            'statusCode' => ($exception)? $exception->getCode() : 'Unable to get Exception statusCode',
502
            'method' => $method,
503
            'file' => __FILE__,
504
            'line' => $line,
505
        );
506
        $this->saveLog($data);
507
        return $this->finishProcess(true);
508
    }
509
510
    /**
511
     * Redirect the request to the e-commerce or show the output in json
512
     *
513
     * @param bool $error
514
     */
515
    public function finishProcess($error = true)
516
    {
517
        if ($_SERVER['REQUEST_METHOD'] == 'POST') {
518
            $this->jsonResponse->printResponse();
519
        }
520
521
        $parameters = array(
522
            'id_cart' => $this->merchantOrderId,
523
            'key' => $this->config['secureKey'],
524
            'id_module' => $this->module->id,
525
            'id_order' => ($this->pagantisOrder)?$this->pagantisOrder->getId(): null,
526
        );
527
        $url = ($error)? $this->config['urlKO'] : $this->config['urlOK'];
528
        return $this->redirect($url, $parameters);
529
    }
530
}