Completed
Push — master ( 026455...55c688 )
by
unknown
13s
created

WcPagantisNotify   C

Complexity

Total Complexity 54

Size/Duplication

Total Lines 476
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 207
c 0
b 0
f 0
dl 0
loc 476
rs 6.4799
wmc 54

22 Methods

Rating   Name   Duplication   Size   Complexity  
A checkPagantisStatus() 0 19 5
A getPagantisOrderId() 0 10 2
A insertLog() 0 14 2
A validateAmount() 0 6 2
A checkDbLogTable() 0 14 2
A getOrigin() 0 3 1
A getPagantisOrder() 0 8 2
A setOrigin() 0 3 1
A unblockConcurrency() 0 12 3
A checkConcurrency() 0 9 2
A saveOrder() 0 13 2
A blockConcurrency() 0 27 5
A buildResponse() 0 18 3
A rollbackMerchantOrder() 0 3 1
A updateBdInfo() 0 13 1
A confirmPagantisOrder() 0 16 3
B processInformation() 0 36 6
A checkDbTable() 0 12 2
A processMerchantOrder() 0 4 1
A getMerchantOrder() 0 7 2
A checkMerchantOrderStatus() 0 24 2
A checkOrderStatus() 0 15 4

How to fix   Complexity   

Complex Class

Complex classes like WcPagantisNotify 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 WcPagantisNotify, and based on these observations, apply Extract Interface, too.

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
     *
57
     * @return JsonExceptionResponse|JsonSuccessResponse
58
     * @throws ConcurrencyException
59
     */
60
    public function processInformation()
61
    {
62
        try {
63
            require_once(__ROOT__.'/vendor/autoload.php');
64
            try {
65
                $this->checkConcurrency();
66
                $this->getMerchantOrder();
67
                $this->getPagantisOrderId();
68
                $this->getPagantisOrder();
69
                $checkAlreadyProcessed = $this->checkOrderStatus();
70
                if ($checkAlreadyProcessed) {
71
                    return $this->buildResponse();
72
                }
73
                $this->validateAmount();
74
                if ($this->checkMerchantOrderStatus()) {
75
                    $this->processMerchantOrder();
76
                }
77
            } catch (\Exception $exception) {
78
                $this->insertLog($exception);
79
80
                return $this->buildResponse($exception);
81
            }
82
83
            try {
84
                $this->confirmPagantisOrder();
85
86
                return $this->buildResponse();
87
            } catch (\Exception $exception) {
88
                $this->rollbackMerchantOrder();
89
                $this->insertLog($exception);
90
91
                return $this->buildResponse($exception);
92
            }
93
        } catch (\Exception $exception) {
94
            $this->insertLog($exception);
95
            return $this->buildResponse($exception);
96
        }
97
    }
98
99
    /**
100
     * COMMON FUNCTIONS
101
     */
102
103
    /**
104
     * @throws ConcurrencyException
105
     * @throws QuoteNotFoundException
106
     */
107
    private function checkConcurrency()
108
    {
109
        $this->woocommerceOrderId = $_GET['order-received'];
110
        if ($this->woocommerceOrderId == '') {
111
            throw new QuoteNotFoundException();
112
        }
113
114
        $this->unblockConcurrency();
115
        $this->blockConcurrency($this->woocommerceOrderId);
116
    }
117
118
    /**
119
     * @throws MerchantOrderNotFoundException
120
     */
121
    private function getMerchantOrder()
122
    {
123
        try {
124
            $this->woocommerceOrder = new WC_Order($this->woocommerceOrderId);
125
            $this->woocommerceOrder->set_payment_method_title(Ucfirst(WcPagantisGateway::METHOD_ID));
126
        } catch (\Exception $e) {
127
            throw new MerchantOrderNotFoundException();
128
        }
129
    }
130
131
    /**
132
     * @throws NoIdentificationException
133
     */
134
    private function getPagantisOrderId()
135
    {
136
        global $wpdb;
137
        $this->checkDbTable();
138
        $tableName = $wpdb->prefix.self::ORDERS_TABLE;
139
        $queryResult = $wpdb->get_row("select order_id from $tableName where id='".$this->woocommerceOrderId."'");
140
        $this->pagantisOrderId = $queryResult->order_id;
141
142
        if ($this->pagantisOrderId == '') {
143
            throw new NoIdentificationException();
144
        }
145
    }
146
147
    /**
148
     * @throws OrderNotFoundException
149
     */
150
    private function getPagantisOrder()
151
    {
152
        try {
153
            $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

153
            $this->cfg = /** @scrutinizer ignore-call */ get_option('woocommerce_pagantis_settings');
Loading history...
154
            $this->orderClient = new Client($this->cfg['pagantis_public_key'], $this->cfg['pagantis_private_key']);
155
            $this->pagantisOrder = $this->orderClient->getOrder($this->pagantisOrderId);
156
        } catch (\Exception $e) {
157
            throw new OrderNotFoundException();
158
        }
159
    }
160
161
    /**
162
     * @return bool
163
     * @throws WrongStatusException
164
     */
165
    private function checkOrderStatus()
166
    {
167
        try {
168
            $this->checkPagantisStatus(array('AUTHORIZED'));
169
        } catch (\Exception $e) {
170
            if ($this->pagantisOrder instanceof Order) {
171
                $status = $this->pagantisOrder->getStatus();
172
            } else {
173
                $status = '-';
174
            }
175
176
            if ($status === Order::STATUS_CONFIRMED) {
177
                return true;
178
            }
179
            throw new WrongStatusException($status);
180
        }
181
    }
182
183
    /**
184
     * @return bool
185
     */
186
    private function checkMerchantOrderStatus()
187
    {
188
        //Order status reference => https://docs.woocommerce.com/document/managing-orders/
189
        $validStatus   = array('on-hold', 'pending', 'failed', 'processing', 'completed');
190
        $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

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

273
            /** @scrutinizer ignore-call */ dbDelta($sql);
Loading history...
274
        }
275
    }
276
277
    /**
278
     * Check if logs table exists
279
     */
280
    private function checkDbLogTable()
281
    {
282
        global $wpdb;
283
        $tableName = $wpdb->prefix.self::LOGS_TABLE;
284
285
        if ($wpdb->get_var("SHOW TABLES LIKE '$tableName'") != $tableName) {
286
            $charset_collate = $wpdb->get_charset_collate();
287
            $sql = "CREATE TABLE $tableName ( id int NOT NULL AUTO_INCREMENT, log text NOT NULL, 
288
                    createdAt timestamp DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY id (id)) $charset_collate";
289
290
            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...
291
            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

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

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