Passed
Pull Request — master (#21)
by
unknown
03:27
created

WcPagantisNotify::saveOrder()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 13
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 10
c 0
b 0
f 0
nc 2
nop 0
dl 0
loc 13
rs 9.9332
1
<?php
2
3
use Pagantis\OrdersApiClient\Client;
4
use Pagantis\ModuleUtils\Exception\ConcurrencyException;
5
use Pagantis\ModuleUtils\Exception\AlreadyProcessedException;
6
use Pagantis\ModuleUtils\Exception\AmountMismatchException;
7
use Pagantis\ModuleUtils\Exception\MerchantOrderNotFoundException;
8
use Pagantis\ModuleUtils\Exception\NoIdentificationException;
9
use Pagantis\ModuleUtils\Exception\OrderNotFoundException;
10
use Pagantis\ModuleUtils\Exception\QuoteNotFoundException;
11
use Pagantis\ModuleUtils\Exception\UnknownException;
12
use Pagantis\ModuleUtils\Exception\WrongStatusException;
13
use Pagantis\ModuleUtils\Model\Response\JsonSuccessResponse;
14
use Pagantis\ModuleUtils\Model\Response\JsonExceptionResponse;
15
use Pagantis\ModuleUtils\Model\Log\LogEntry;
16
use Pagantis\OrdersApiClient\Model\Order;
17
18
if (!defined('ABSPATH')) {
19
    exit;
20
}
21
22
class WcPagantisNotify extends WcPagantisGateway
23
{
24
    /** Concurrency tablename  */
25
    const CONCURRENCY_TABLE = 'pagantis_concurrency';
26
27
    /** Seconds to expire a locked request */
28
    const CONCURRENCY_TIMEOUT = 5;
29
30
    /** @var mixed $pagantisOrder */
31
    protected $pagantisOrder;
32
33
    /** @var $string $origin */
0 ignored issues
show
Documentation Bug introduced by
The doc comment $string at position 0 could not be parsed: Unknown type name '$string' at position 0 in $string.
Loading history...
34
    public $origin;
35
36
    /** @var $string */
0 ignored issues
show
Documentation Bug introduced by
The doc comment $string at position 0 could not be parsed: Unknown type name '$string' at position 0 in $string.
Loading history...
37
    public $order;
38
39
    /** @var mixed $woocommerceOrderId */
40
    protected $woocommerceOrderId = '';
41
42
    /** @var mixed $cfg */
43
    protected $cfg;
44
45
    /** @var Client $orderClient */
46
    protected $orderClient;
47
48
    /** @var  WC_Order $woocommerceOrder */
0 ignored issues
show
Bug introduced by
The type WC_Order 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...
49
    protected $woocommerceOrder;
50
51
    /** @var mixed $pagantisOrderId */
52
    protected $pagantisOrderId = '';
53
54
    /**
55
     * Validation vs PagantisClient
56
     * @return JsonExceptionResponse|JsonSuccessResponse
57
     */
58
    public function processInformation()
59
    {
60
        try {
61
            require_once(__ROOT__.'/vendor/autoload.php');
62
            try {
63
                $this->checkConcurrency();
64
                $this->getMerchantOrder();
65
                $this->getPagantisOrderId();
66
                $this->getPagantisOrder();
67
                $checkAlreadyProcessed = $this->checkOrderStatus();
68
                if ($checkAlreadyProcessed) {
69
                    return $this->buildResponse();
70
                }
71
                $this->validateAmount();
72
                if ($this->checkMerchantOrderStatus()) {
73
                    $this->processMerchantOrder();
74
                }
75
            } catch (\Exception $exception) {
76
                $this->insertLog($exception);
77
78
                return $this->buildResponse($exception);
79
            }
80
81
            try {
82
                $this->confirmPagantisOrder();
83
84
                return $this->buildResponse();
85
            } catch (\Exception $exception) {
86
                $this->rollbackMerchantOrder();
87
                $this->insertLog($exception);
88
89
                return $this->buildResponse($exception);
90
            }
91
        } catch (\Exception $exception) {
92
            $this->insertLog($exception);
93
            return $this->buildResponse($exception);
94
        }
95
    }
96
97
    /**
98
     * COMMON FUNCTIONS
99
     */
100
101
    /**
102
     * @throws QuoteNotFoundException
103
     */
104
    private function checkConcurrency()
105
    {
106
        $this->woocommerceOrderId = $_GET['order-received'];
107
        if ($this->woocommerceOrderId == '') {
108
            throw new QuoteNotFoundException();
109
        }
110
111
        $this->unblockConcurrency();
112
        $this->blockConcurrency($this->woocommerceOrderId);
113
    }
114
115
    /**
116
     * @throws MerchantOrderNotFoundException
117
     */
118
    private function getMerchantOrder()
119
    {
120
        try {
121
            $this->woocommerceOrder = new WC_Order($this->woocommerceOrderId);
122
            $this->woocommerceOrder->set_payment_method_title(Ucfirst(WcPagantisGateway::METHOD_ID));
123
        } catch (\Exception $e) {
124
            throw new MerchantOrderNotFoundException();
125
        }
126
    }
127
128
    /**
129
     * @throws NoIdentificationException
130
     */
131
    private function getPagantisOrderId()
132
    {
133
        global $wpdb;
134
        $this->checkDbTable();
135
        $tableName = $wpdb->prefix.self::ORDERS_TABLE;
136
        $queryResult = $wpdb->get_row("select order_id from $tableName where id='".$this->woocommerceOrderId."'");
137
        $this->pagantisOrderId = $queryResult->order_id;
138
139
        if ($this->pagantisOrderId == '') {
140
            throw new NoIdentificationException();
141
        }
142
    }
143
144
    /**
145
     * @throws OrderNotFoundException
146
     */
147
    private function getPagantisOrder()
148
    {
149
        try {
150
            $this->cfg = get_option('woocommerce_pagantis_settings');
0 ignored issues
show
Bug introduced by
The function get_option was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

150
            $this->cfg = /** @scrutinizer ignore-call */ get_option('woocommerce_pagantis_settings');
Loading history...
151
            $this->orderClient = new Client($this->cfg['pagantis_public_key'], $this->cfg['pagantis_private_key']);
152
            $this->pagantisOrder = $this->orderClient->getOrder($this->pagantisOrderId);
153
        } catch (\Exception $e) {
154
            throw new OrderNotFoundException();
155
        }
156
    }
157
158
    /**
159
     * @throws AlreadyProcessedException
160
     * @throws WrongStatusException
161
     */
162
    private function checkOrderStatus()
163
    {
164
        try {
165
            $this->checkPagantisStatus(array('AUTHORIZED'));
166
        } catch (\Exception $e) {
167
            if ($this->pagantisOrder instanceof Order) {
168
                $status = $this->pagantisOrder->getStatus();
169
            } else {
170
                $status = '-';
171
            }
172
173
            if ($status === Order::STATUS_CONFIRMED) {
174
                return true;
175
            }
176
            throw new WrongStatusException($status);
177
        }
178
    }
179
180
    /**
181
     * @return bool
182
     */
183
    private function checkMerchantOrderStatus()
184
    {
185
        //Order status reference => https://docs.woocommerce.com/document/managing-orders/
186
        $validStatus   = array('on-hold', 'pending', 'failed', 'processing', 'completed');
187
        $isValidStatus = apply_filters(
0 ignored issues
show
Bug introduced by
The function apply_filters was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

187
        $isValidStatus = /** @scrutinizer ignore-call */ apply_filters(
Loading history...
188
            'woocommerce_valid_order_statuses_for_payment_complete',
189
            $validStatus,
190
            $this
191
        );
192
193
        if (!$this->woocommerceOrder->has_status($isValidStatus)) { // TO CONFIRM
194
            $logMessage = "WARNING checkMerchantOrderStatus." .
195
                          " Merchant order id:".$this->woocommerceOrder->get_id().
196
                          " Merchant order status:".$this->woocommerceOrder->get_status().
197
                          " Pagantis order id:".$this->pagantisOrder->getStatus().
198
                          " Pagantis order status:".$this->pagantisOrder->getId();
199
200
            $this->insertLog(null, $logMessage);
201
            $this->woocommerceOrder->add_order_note($logMessage);
202
            $this->woocommerceOrder->save();
203
            return false;
204
        }
205
206
        return true; //TO SAVE
207
    }
208
209
    /**
210
     * @throws AmountMismatchException
211
     */
212
    private function validateAmount()
213
    {
214
        $pagantisAmount = $this->pagantisOrder->getShoppingCart()->getTotalAmount();
215
        $wcAmount = (string) floor(100 * $this->woocommerceOrder->get_total());
216
        if ($pagantisAmount != $wcAmount) {
217
            throw new AmountMismatchException($pagantisAmount, $wcAmount);
218
        }
219
    }
220
221
    /**
222
     * @throws Exception
223
     */
224
    private function processMerchantOrder()
225
    {
226
        $this->saveOrder();
227
        $this->updateBdInfo();
228
    }
229
230
    /**
231
     * @return false|string
232
     * @throws UnknownException
233
     */
234
    private function confirmPagantisOrder()
235
    {
236
        try {
237
            $this->pagantisOrder = $this->orderClient->confirmOrder($this->pagantisOrderId);
238
        } catch (\Exception $e) {
239
            $this->pagantisOrder = $this->orderClient->getOrder($this->pagantisOrderId);
240
            if ($this->pagantisOrder->getStatus() !== Order::STATUS_CONFIRMED) {
241
                throw new UnknownException($e->getMessage());
242
            } else {
243
                $logMessage = 'Concurrency issue: Order_id '.$this->pagantisOrderId.' was confirmed by other process';
244
                $this->insertLog(null, $logMessage);
245
            }
246
        }
247
248
        $jsonResponse = new JsonSuccessResponse();
249
        return $jsonResponse->toJson();
250
    }
251
252
    /**
253
     * UTILS FUNCTIONS
254
     */
255
    /** STEP 1 CC - Check concurrency */
256
    /**
257
     * Check if orders table exists
258
     */
259
    private function checkDbTable()
260
    {
261
        global $wpdb;
262
        $tableName = $wpdb->prefix.self::ORDERS_TABLE;
263
264
        if ($wpdb->get_var("SHOW TABLES LIKE '$tableName'") != $tableName) {
265
            $charset_collate = $wpdb->get_charset_collate();
266
            $sql             = "CREATE TABLE $tableName (id int, order_id varchar(50), wc_order_id varchar(50), 
267
                  UNIQUE KEY id (id)) $charset_collate";
268
269
            require_once(ABSPATH.'wp-admin/includes/upgrade.php');
0 ignored issues
show
Bug introduced by
The constant ABSPATH was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
270
            dbDelta($sql);
0 ignored issues
show
Bug introduced by
The function dbDelta was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

270
            /** @scrutinizer ignore-call */ dbDelta($sql);
Loading history...
271
        }
272
    }
273
274
    /**
275
     * Check if logs table exists
276
     */
277
    private function checkDbLogTable()
278
    {
279
        global $wpdb;
280
        $tableName = $wpdb->prefix.self::LOGS_TABLE;
281
282
        if ($wpdb->get_var("SHOW TABLES LIKE '$tableName'") != $tableName) {
283
            $charset_collate = $wpdb->get_charset_collate();
284
            $sql = "CREATE TABLE $tableName ( id int NOT NULL AUTO_INCREMENT, log text NOT NULL, 
285
                    createdAt timestamp DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY id (id)) $charset_collate";
286
287
            require_once(ABSPATH.'wp-admin/includes/upgrade.php');
0 ignored issues
show
Bug introduced by
The constant ABSPATH was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
288
            dbDelta($sql);
0 ignored issues
show
Bug introduced by
The function dbDelta was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

288
            /** @scrutinizer ignore-call */ dbDelta($sql);
Loading history...
289
        }
290
        return;
291
    }
292
293
    /** STEP 2 GMO - Get Merchant Order */
294
    /** STEP 3 GPOI - Get Pagantis OrderId */
295
    /** STEP 4 GPO - Get Pagantis Order */
296
    /** STEP 5 COS - Check Order Status */
297
298
    /**
299
     * @param $statusArray
300
     *
301
     * @throws \Exception
302
     */
303
    private function checkPagantisStatus($statusArray)
304
    {
305
        $pagantisStatus = array();
306
        foreach ($statusArray as $status) {
307
            $pagantisStatus[] = constant("\Pagantis\OrdersApiClient\Model\Order::STATUS_$status");
308
        }
309
310
        if ($this->pagantisOrder instanceof Order) {
311
            $payed = in_array($this->pagantisOrder->getStatus(), $pagantisStatus);
312
            if (!$payed) {
313
                if ($this->pagantisOrder instanceof Order) {
0 ignored issues
show
introduced by
$this->pagantisOrder is always a sub-type of Pagantis\OrdersApiClient\Model\Order.
Loading history...
314
                    $status = $this->pagantisOrder->getStatus();
315
                } else {
316
                    $status = '-';
317
                }
318
                throw new WrongStatusException($status);
319
            }
320
        } else {
321
            throw new OrderNotFoundException();
322
        }
323
    }
324
325
    /** STEP 6 CMOS - Check Merchant Order Status */
326
    /** STEP 7 VA - Validate Amount */
327
    /** STEP 8 PMO - Process Merchant Order */
328
    /**
329
     * @throws \Exception
330
     */
331
    private function saveOrder()
332
    {
333
        global $woocommerce;
334
        $paymentResult = $this->woocommerceOrder->payment_complete();
335
        if ($paymentResult) {
336
            $this->woocommerceOrder->add_order_note("Notification received via $this->origin");
337
            $this->woocommerceOrder->reduce_order_stock();
338
            $this->woocommerceOrder->save();
339
340
            $woocommerce->cart->empty_cart();
341
            sleep(3);
342
        } else {
343
            throw new UnknownException('Order can not be saved');
344
        }
345
    }
346
347
    /**
348
     * Save the merchant order_id with the related identification
349
     */
350
    private function updateBdInfo()
351
    {
352
        global $wpdb;
353
354
        $this->checkDbTable();
355
        $tableName = $wpdb->prefix.self::ORDERS_TABLE;
356
357
        $wpdb->update(
358
            $tableName,
359
            array('wc_order_id'=>$this->woocommerceOrderId),
360
            array('id' => $this->woocommerceOrderId),
361
            array('%s'),
362
            array('%d')
363
        );
364
    }
365
366
    /** STEP 9 CPO - Confirmation Pagantis Order */
367
    private function rollbackMerchantOrder()
368
    {
369
        $this->woocommerceOrder->update_status('pending', __('Pending payment', 'woocommerce'));
0 ignored issues
show
Bug introduced by
The function __ was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

369
        $this->woocommerceOrder->update_status('pending', /** @scrutinizer ignore-call */ __('Pending payment', 'woocommerce'));
Loading history...
370
    }
371
372
    /**
373
     * @param null $exception
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $exception is correct as it would always require null to be passed?
Loading history...
374
     * @param null $message
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $message is correct as it would always require null to be passed?
Loading history...
375
     */
376
    private function insertLog($exception = null, $message = null)
377
    {
378
        global $wpdb;
379
380
        $this->checkDbLogTable();
381
        $logEntry     = new LogEntry();
382
        if ($exception instanceof \Exception) {
383
            $logEntry = $logEntry->error($exception);
384
        } else {
385
            $logEntry = $logEntry->info($message);
386
        }
387
388
        $tableName = $wpdb->prefix.self::LOGS_TABLE;
389
        $wpdb->insert($tableName, array('log' => $logEntry->toJson()));
390
    }
391
392
    /**
393
     * @param null $orderId
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $orderId is correct as it would always require null to be passed?
Loading history...
394
     *
395
     * @return bool
396
     * @throws ConcurrencyException
397
     */
398
    private function unblockConcurrency($orderId = null)
399
    {
400
        global $wpdb;
401
        $tableName = $wpdb->prefix.self::CONCURRENCY_TABLE;
402
        if ($orderId == null) {
0 ignored issues
show
introduced by
The condition $orderId == null is always true.
Loading history...
403
            $query = "DELETE FROM $tableName WHERE createdAt<(NOW()- INTERVAL ".self::CONCURRENCY_TIMEOUT." SECOND)";
404
        } else {
405
            $query = "DELETE FROM $tableName WHERE order_id = $orderId";
406
        }
407
        $resultDelete = $wpdb->query($query);
408
        if ($resultDelete === false) {
409
            throw new ConcurrencyException();
410
        }
411
    }
412
413
    /**
414
     * @throws ConcurrencyException
415
     */
416
    private function blockConcurrency($orderId)
417
    {
418
        global $wpdb;
419
        $tableName = $wpdb->prefix.self::CONCURRENCY_TABLE;
420
        $insertResult = $wpdb->insert($tableName, array('order_id' => $orderId));
421
        if ($insertResult === false) {
422
            if ($this->getOrigin() == 'Notify') {
423
                throw new ConcurrencyException();
424
            } else {
425
                $query = sprintf(
426
                    "SELECT TIMESTAMPDIFF(SECOND,NOW()-INTERVAL %s SECOND, createdAt) as rest FROM %s WHERE %s",
427
                    self::CONCURRENCY_TIMEOUT,
428
                    $tableName,
429
                    "order_id=$orderId"
430
                );
431
                $resultSeconds = $wpdb->get_row($query);
432
                $restSeconds = isset($resultSeconds) ? ($resultSeconds->rest) : 0;
433
                $secondsToExpire = ($restSeconds>self::CONCURRENCY_TIMEOUT) ? self::CONCURRENCY_TIMEOUT : $restSeconds;
434
                sleep($secondsToExpire+1);
435
436
                $logMessage = sprintf(
437
                    "User waiting %s seconds, default seconds %s, bd time to expire %s seconds",
438
                    $secondsToExpire,
439
                    self::CONCURRENCY_TIMEOUT,
440
                    $restSeconds
441
                );
442
                $this->insertLog(null, $logMessage);
443
444
            }
445
        }
446
    }
447
448
    /**
449
     * @param null $exception
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $exception is correct as it would always require null to be passed?
Loading history...
450
     *
451
     *
452
     * @return JsonExceptionResponse|JsonSuccessResponse
453
     * @throws ConcurrencyException
454
     */
455
    private function buildResponse($exception = null)
456
    {
457
        $this->unblockConcurrency($this->woocommerceOrderId);
458
459
        if ($exception == null) {
0 ignored issues
show
introduced by
The condition $exception == null is always true.
Loading history...
460
            $jsonResponse = new JsonSuccessResponse();
461
        } else {
462
            $jsonResponse = new JsonExceptionResponse();
463
            $jsonResponse->setException($exception);
464
        }
465
466
        $jsonResponse->setMerchantOrderId($this->woocommerceOrderId);
467
        $jsonResponse->setPagantisOrderId($this->pagantisOrderId);
468
469
        if ($_SERVER['REQUEST_METHOD'] == 'POST') {
470
            $jsonResponse->printResponse();
471
        } else {
472
            return $jsonResponse;
473
        }
474
    }
475
476
    /**
477
     * GETTERS & SETTERS
478
     */
479
480
    /**
481
     * @return mixed
482
     */
483
    public function getOrigin()
484
    {
485
        return $this->origin;
486
    }
487
488
    /**
489
     * @param mixed $origin
490
     */
491
    public function setOrigin($origin)
492
    {
493
        $this->origin = $origin;
494
    }
495
}
496