Completed
Push — master ( da3f71...7a2686 )
by pablo
19s queued 14s
created

WcPagantisNotify   D

Complexity

Total Complexity 59

Size/Duplication

Total Lines 491
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 216
dl 0
loc 491
rs 4.08
c 0
b 0
f 0
wmc 59

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

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

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

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

294
            /** @scrutinizer ignore-call */ dbDelta($sql);
Loading history...
295
        }
296
        return;
297
    }
298
299
    /** STEP 2 GMO - Get Merchant Order */
300
    /** STEP 3 GPOI - Get Pagantis OrderId */
301
    /** STEP 4 GPO - Get Pagantis Order */
302
    /** STEP 5 COS - Check Order Status */
303
304
    /**
305
     * @param $statusArray
306
     *
307
     * @throws \Exception
308
     */
309
    private function checkPagantisStatus($statusArray)
310
    {
311
        $pagantisStatus = array();
312
        foreach ($statusArray as $status) {
313
            $pagantisStatus[] = constant("\Pagantis\OrdersApiClient\Model\Order::STATUS_$status");
314
        }
315
316
        if ($this->pagantisOrder instanceof Order) {
317
            $payed = in_array($this->pagantisOrder->getStatus(), $pagantisStatus);
318
            if (!$payed) {
319
                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...
320
                    $status = $this->pagantisOrder->getStatus();
321
                } else {
322
                    $status = '-';
323
                }
324
                throw new WrongStatusException($status);
325
            }
326
        } else {
327
            throw new OrderNotFoundException();
328
        }
329
    }
330
331
    /** STEP 6 CMOS - Check Merchant Order Status */
332
    /** STEP 7 VA - Validate Amount */
333
    /** STEP 8 PMO - Process Merchant Order */
334
    /**
335
     * @throws \Exception
336
     */
337
    private function saveOrder()
338
    {
339
        global $woocommerce;
340
        $paymentResult = $this->woocommerceOrder->payment_complete();
341
        if ($paymentResult) {
342
            $metadataOrder = $this->pagantisOrder->getMetadata();
343
            $metadataInfo = null;
344
            foreach ($metadataOrder as $metadataKey => $metadataValue) {
345
                if ($metadataKey == 'promotedProduct') {
346
                    $metadataInfo.= "/Producto promocionado = $metadataValue";
347
                }
348
            }
349
350
            if ($metadataInfo != null) {
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $metadataInfo of type null|string against null; this is ambiguous if the string can be empty. Consider using a strict comparison !== instead.
Loading history...
351
                $this->woocommerceOrder->add_order_note($metadataInfo);
352
            }
353
354
            $this->woocommerceOrder->add_order_note("Notification received via $this->origin");
355
            $this->woocommerceOrder->reduce_order_stock();
356
            $this->woocommerceOrder->save();
357
358
            $woocommerce->cart->empty_cart();
359
            sleep(3);
360
        } else {
361
            throw new UnknownException('Order can not be saved');
362
        }
363
    }
364
365
    /**
366
     * Save the merchant order_id with the related identification
367
     */
368
    private function updateBdInfo()
369
    {
370
        global $wpdb;
371
372
        $this->checkDbTable();
373
        $tableName = $wpdb->prefix.self::ORDERS_TABLE;
374
375
        $wpdb->update(
376
            $tableName,
377
            array('wc_order_id'=>$this->woocommerceOrderId),
378
            array('id' => $this->woocommerceOrderId),
379
            array('%s'),
380
            array('%d')
381
        );
382
    }
383
384
    /** STEP 9 CPO - Confirmation Pagantis Order */
385
    private function rollbackMerchantOrder()
386
    {
387
        $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

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