Passed
Pull Request — master (#48)
by
unknown
02:40
created

WC_PG_Notification_Handler::checkOrderStatus()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 15
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 10
c 0
b 0
f 0
nc 5
nop 0
dl 0
loc 15
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 WC_PG_Notification_Handler extends WC_Pagantis_Gateway
23
{
24
    /**
25
     * Concurrency table name
26
     */
27
    const CONCURRENCY_TABLE = 'pagantis_concurrency';
28
29
    /**
30
     * Seconds to expire a locked request
31
     */
32
    const CONCURRENCY_TIMEOUT = 5;
33
34
    /**
35
     * Pagantis Order Number
36
     *
37
     * @var mixed $pagantisOrder
38
     */
39
    protected $pagantisOrder;
40
41
    /**
42
     *  Origin of Order
43
     *
44
     * @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...
45
     */
46
    public $origin;
47
48
    /**
49
     * WC_Order object
50
     *
51
     * @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...
52
     */
53
    public $order;
54
55
    /**
56
     * @var mixed $woocommerceOrderId
57
     */
58
    protected $woocommerceOrderId = '';
59
60
    /**
61
     * @var mixed $cfg
62
     */
63
    protected $cfg;
64
65
    /**
66
     * @var Client $orderClient
67
     */
68
    protected $orderClient;
69
70
    /**
71
     * @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...
72
     */
73
    protected $woocommerceOrder;
74
75
    /**
76
     * @var mixed $pagantisOrderId
77
     */
78
    protected $pagantisOrderId = '';
79
80
    /**
81
     * Validation vs PagantisClient
82
     *
83
     * @return JsonExceptionResponse|JsonSuccessResponse
84
     * @throws ConcurrencyException
85
     */
86
    public function processInformation()
87
    {
88
        require_once dirname(__FILE__) . '/../includes/class-wc-pagantis-logger.php';
89
        require_once dirname(__FILE__) . '/../includes/functions.php';
90
        try {
91
            require_once(__ROOT__ . '/vendor/autoload.php');
92
            try {
93
                if ($_SERVER['REQUEST_METHOD'] === 'GET' && $_GET['origin'] === 'notification') {
94
                    return $this->buildResponse();
95
                }
96
                $this->checkConcurrency();
97
                $this->getMerchantOrder();
98
                $this->getPagantisOrderId();
99
                $this->getPagantisOrder();
100
                $checkAlreadyProcessed = $this->checkOrderStatus();
101
                if ($checkAlreadyProcessed) {
102
                    return $this->buildResponse();
103
                }
104
                $this->validateAmount();
105
                if ($this->checkMerchantOrderStatus()) {
106
                    $this->processMerchantOrder();
107
                }
108
            } catch (\Exception $exception) {
109
                WC_Pagantis_Logger::insert_log_entry_in_wpdb($exception);
110
111
                return $this->buildResponse($exception);
112
            }
113
114
            try {
115
                $this->confirmPagantisOrder();
116
117
                return $this->buildResponse();
118
            } catch (\Exception $exception) {
119
                $this->rollbackMerchantOrder();
120
                WC_Pagantis_Logger::insert_log_entry_in_wpdb($exception);
121
122
                return $this->buildResponse($exception);
123
            }
124
        } catch (\Exception $exception) {
125
            WC_Pagantis_Logger::insert_log_entry_in_wpdb($exception);
126
127
            return $this->buildResponse($exception);
128
        }
129
    }
130
131
    /**
132
     * COMMON FUNCTIONS
133
     */
134
135
    /**
136
     * @throws ConcurrencyException
137
     * @throws QuoteNotFoundException
138
     */
139
    private function checkConcurrency()
140
    {
141
        $this->woocommerceOrderId = $_GET['order-received'];
142
        if ($this->woocommerceOrderId === '') {
143
            throw new QuoteNotFoundException();
144
        }
145
146
        $this->unblockConcurrency();
147
        $this->blockConcurrency($this->woocommerceOrderId);
148
    }
149
150
    /**
151
     * @throws MerchantOrderNotFoundException
152
     */
153
    private function getMerchantOrder()
154
    {
155
        try {
156
            $this->woocommerceOrder = new WC_Order($this->woocommerceOrderId);
157
            $this->woocommerceOrder->set_payment_method_title(Ucfirst(PAGANTIS_PLUGIN_ID));
158
        } catch (\Exception $e) {
159
            throw new MerchantOrderNotFoundException();
160
        }
161
    }
162
163
    /**
164
     * @throws NoIdentificationException
165
     */
166
    private function getPagantisOrderId()
167
    {
168
        global $wpdb;
169
        $this->checkDbTable();
170
        $tableName             = $wpdb->prefix . PAGANTIS_WC_ORDERS_TABLE;
171
        $queryResult           =
172
            $wpdb->get_row("select order_id from $tableName where id='" . $this->woocommerceOrderId . "'");
173
        $this->pagantisOrderId = $queryResult->order_id;
174
175
        if ($this->pagantisOrderId === '') {
176
            throw new NoIdentificationException();
177
        }
178
    }
179
180
    /**
181
     * @throws OrderNotFoundException
182
     */
183
    private function getPagantisOrder()
184
    {
185
        try {
186
            $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

186
            $this->cfg           = /** @scrutinizer ignore-call */ get_option('woocommerce_pagantis_settings');
Loading history...
187
            $this->orderClient   = new Client($this->cfg['pagantis_public_key'], $this->cfg['pagantis_private_key']);
188
            $this->pagantisOrder = $this->orderClient->getOrder($this->pagantisOrderId);
189
        } catch (\Exception $e) {
190
            throw new OrderNotFoundException();
191
        }
192
    }
193
194
    /**
195
     * @return bool
196
     * @throws WrongStatusException
197
     */
198
    private function checkOrderStatus()
199
    {
200
        try {
201
            $this->checkPagantisStatus(array('AUTHORIZED'));
202
        } catch (\Exception $e) {
203
            if ($this->pagantisOrder instanceof Order) {
204
                $status = $this->pagantisOrder->getStatus();
205
            } else {
206
                $status = '-';
207
            }
208
209
            if ($status === Order::STATUS_CONFIRMED) {
210
                return true;
211
            }
212
            throw new WrongStatusException($status);
213
        }
214
    }
215
216
    /**
217
     * @return bool
218
     */
219
    private function checkMerchantOrderStatus()
220
    {
221
        //Order status reference => https://docs.woocommerce.com/document/managing-orders/
222
        $validStatus   = array('on-hold', 'pending', 'failed', 'processing', 'completed');
223
        $isValidStatus = apply_filters('woocommerce_valid_order_statuses_for_payment_complete', $validStatus, $this);
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

223
        $isValidStatus = /** @scrutinizer ignore-call */ apply_filters('woocommerce_valid_order_statuses_for_payment_complete', $validStatus, $this);
Loading history...
224
225
        if (! $this->woocommerceOrder->has_status($isValidStatus)) { // TO CONFIRM
226
            $logMessage =
227
                'WARNING checkMerchantOrderStatus.' . ' Merchant order id:' . $this->woocommerceOrder->get_id()
228
                . ' Merchant order status:' . $this->woocommerceOrder->get_status() . ' Pagantis order id:'
229
                . $this->pagantisOrder->getStatus() . ' Pagantis order status:' . $this->pagantisOrder->getId();
230
231
            WC_Pagantis_Logger::insert_log_entry_in_wpdb(null, $logMessage);
232
            $this->woocommerceOrder->add_order_note($logMessage);
233
            $this->woocommerceOrder->save();
234
235
            return false;
236
        }
237
238
        return true; //TO SAVE
239
    }
240
241
    /**
242
     * @throws AmountMismatchException
243
     */
244
    private function validateAmount()
245
    {
246
        $pagantisAmount = $this->pagantisOrder->getShoppingCart()->getTotalAmount();
247
        $wcAmount       = intval(strval(100 * $this->woocommerceOrder->get_total()));
248
        if ($pagantisAmount != $wcAmount) {
249
            throw new AmountMismatchException($pagantisAmount, $wcAmount);
250
        }
251
    }
252
253
    /**
254
     * @throws Exception
255
     */
256
    private function processMerchantOrder()
257
    {
258
        $this->saveOrder();
259
        $this->updateBdInfo();
260
    }
261
262
    /**
263
     * @return false|string
264
     * @throws UnknownException
265
     */
266
    private function confirmPagantisOrder()
267
    {
268
        try {
269
            $this->pagantisOrder = $this->orderClient->confirmOrder($this->pagantisOrderId);
270
        } catch (\Exception $e) {
271
            $this->pagantisOrder = $this->orderClient->getOrder($this->pagantisOrderId);
272
            if ($this->pagantisOrder->getStatus() !== Order::STATUS_CONFIRMED) {
273
                throw new UnknownException($e->getMessage());
274
            } else {
275
                $logMessage =
276
                    'Concurrency issue: Order_id ' . $this->pagantisOrderId . ' was confirmed by other process';
277
                WC_Pagantis_Logger::insert_log_entry_in_wpdb(null, $logMessage);
278
            }
279
        }
280
281
        $jsonResponse = new JsonSuccessResponse();
282
283
        return $jsonResponse->toJson();
284
    }
285
286
    /**
287
     * UTILS FUNCTIONS
288
     */
289
    /**
290
     * STEP 1 CC - Check concurrency
291
     */
292
    /**
293
     * Check if orders table exists
294
     */
295
    private function checkDbTable()
296
    {
297
        global $wpdb;
298
        $tableName = $wpdb->prefix . PAGANTIS_WC_ORDERS_TABLE;
299
300
        if ($wpdb->get_var("SHOW TABLES LIKE '$tableName'") != $tableName) {
301
            $charset_collate = $wpdb->get_charset_collate();
302
            $sql             = "CREATE TABLE $tableName (id int, order_id varchar(50), wc_order_id varchar(50), 
303
                  UNIQUE KEY id (id)) $charset_collate";
304
305
            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...
306
            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

306
            /** @scrutinizer ignore-call */ 
307
            dbDelta($sql);
Loading history...
307
        }
308
    }
309
310
    /**
311
     * Check if logs table exists
312
     */
313
    private function checkDbLogTable()
0 ignored issues
show
Unused Code introduced by
The method checkDbLogTable() is not used, and could be removed.

This check looks for private methods that have been defined, but are not used inside the class.

Loading history...
314
    {
315
        global $wpdb;
316
        $tableName = $wpdb->prefix . PAGANTIS_LOGS_TABLE;
317
318
        if ($wpdb->get_var("SHOW TABLES LIKE '$tableName'") != $tableName) {
319
            $charset_collate = $wpdb->get_charset_collate();
320
            $sql             = "CREATE TABLE $tableName ( id int NOT NULL AUTO_INCREMENT, log text NOT NULL, 
321
                    createdAt timestamp DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY id (id)) $charset_collate";
322
323
            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...
324
            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

324
            /** @scrutinizer ignore-call */ 
325
            dbDelta($sql);
Loading history...
325
        }
326
327
        return;
328
    }
329
330
    /**
331
     * STEP 2 GMO - Get Merchant Order
332
     */
333
    /**
334
     * STEP 3 GPOI - Get Pagantis OrderId
335
     */
336
    /**
337
     * STEP 4 GPO - Get Pagantis Order
338
     */
339
    /**
340
     * STEP 5 COS - Check Order Status
341
     */
342
343
    /**
344
     * @param $statusArray
345
     *
346
     * @throws \Exception
347
     */
348
    private function checkPagantisStatus($statusArray)
349
    {
350
        $pagantisStatus = array();
351
        foreach ($statusArray as $status) {
352
            $pagantisStatus[] = constant("\Pagantis\OrdersApiClient\Model\Order::STATUS_$status");
353
        }
354
355
        if ($this->pagantisOrder instanceof Order) {
356
            $payed = in_array($this->pagantisOrder->getStatus(), $pagantisStatus);
357
            if (! $payed) {
358
                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...
359
                    $status = $this->pagantisOrder->getStatus();
360
                } else {
361
                    $status = '-';
362
                }
363
                throw new WrongStatusException($status);
364
            }
365
        } else {
366
            throw new OrderNotFoundException();
367
        }
368
    }
369
370
    /**
371
     * STEP 6 CMOS - Check Merchant Order Status
372
     */
373
    /**
374
     * STEP 7 VA - Validate Amount
375
     */
376
    /**
377
     * STEP 8 PMO - Process Merchant Order
378
     */
379
    /**
380
     * @throws \Exception
381
     */
382
    private function saveOrder()
383
    {
384
        global $woocommerce;
385
        $paymentResult = $this->woocommerceOrder->payment_complete();
386
        if ($paymentResult) {
387
            $metadataOrder = $this->pagantisOrder->getMetadata();
388
            $metadataInfo  = null;
389
            foreach ($metadataOrder as $metadataKey => $metadataValue) {
390
                if ($metadataKey === 'promotedProduct') {
391
                    $metadataInfo .= "/Producto promocionado = $metadataValue";
392
                }
393
            }
394
395
            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...
396
                $this->woocommerceOrder->add_order_note($metadataInfo);
397
            }
398
399
            $this->woocommerceOrder->add_order_note("Notification received via $this->origin");
400
            $this->woocommerceOrder->reduce_order_stock();
401
            $this->woocommerceOrder->save();
402
403
            $woocommerce->cart->empty_cart();
404
            sleep(3);
405
        } else {
406
            throw new UnknownException('Order can not be saved');
407
        }
408
    }
409
410
    /**
411
     * Save the merchant order_id with the related identification
412
     */
413
    private function updateBdInfo()
414
    {
415
        global $wpdb;
416
417
        $this->checkDbTable();
418
        $tableName = $wpdb->prefix . PAGANTIS_WC_ORDERS_TABLE;
419
420
        $wpdb->update(
421
            $tableName,
422
            array('wc_order_id' => $this->woocommerceOrderId),
423
            array('id' => $this->woocommerceOrderId),
424
            array('%s'),
425
            array('%d')
426
        );
427
    }
428
429
    /**
430
     * STEP 9 CPO - Confirmation Pagantis Order
431
     */
432
    private function rollbackMerchantOrder()
433
    {
434
        $this->woocommerceOrder->update_status(
435
            'pending',
436
            __('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

436
            /** @scrutinizer ignore-call */ 
437
            __('Pending payment', 'woocommerce')
Loading history...
437
        ); // phpcs:ignore WordPress.WP.I18n.TextDomainMismatch
438
    }
439
440
    /**
441
     * @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...
442
     * @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...
443
     */
444
    private function insertLog($exception = null, $message = null)
0 ignored issues
show
Unused Code introduced by
The method insertLog() is not used, and could be removed.

This check looks for private methods that have been defined, but are not used inside the class.

Loading history...
445
    {
446
        global $wpdb;
447
448
        pg_wc_check_db_log_table();
449
        $logEntry = new LogEntry();
450
        if ($exception instanceof \Exception) {
451
            $logEntry = $logEntry->error($exception);
452
        } else {
453
            $logEntry = $logEntry->info($message);
454
        }
455
456
        $tableName = $wpdb->prefix . PAGANTIS_LOGS_TABLE;
457
        $wpdb->insert($tableName, array('log' => $logEntry->toJson()));
458
    }
459
460
    /**
461
     * @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...
462
     *
463
     * @throws ConcurrencyException
464
     */
465
    private function unblockConcurrency($orderId = null)
466
    {
467
        global $wpdb;
468
        $tableName = $wpdb->prefix . PAGANTIS_CONCURRENCY_TABLE;
469
        if ($orderId === null) {
0 ignored issues
show
introduced by
The condition $orderId === null is always true.
Loading history...
470
            $query =
471
                "DELETE FROM $tableName WHERE createdAt<(NOW()- INTERVAL " . self::CONCURRENCY_TIMEOUT . ' SECOND)';
472
        } else {
473
            $query = "DELETE FROM $tableName WHERE order_id = $orderId";
474
        }
475
        $resultDelete = $wpdb->query($query);
476
        if ($resultDelete === false) {
477
            throw new ConcurrencyException();
478
        }
479
    }
480
481
    /**
482
     * @param $orderId
483
     *
484
     * @throws ConcurrencyException
485
     */
486
    private function blockConcurrency($orderId)
487
    {
488
        global $wpdb;
489
        $tableName    = $wpdb->prefix . PAGANTIS_CONCURRENCY_TABLE;
490
        $insertResult = $wpdb->insert($tableName, array('order_id' => $orderId));
491
        if ($insertResult === false) {
492
            if ($this->getOrigin() === 'Notify') {
493
                throw new ConcurrencyException();
494
            } else {
495
                $query           =
496
                    sprintf(
497
                        'SELECT TIMESTAMPDIFF(SECOND,NOW()-INTERVAL %s SECOND, createdAt) as rest FROM %s WHERE %s',
498
                        self::CONCURRENCY_TIMEOUT,
499
                        $tableName,
500
                        "order_id=$orderId"
501
                    );
502
                $resultSeconds   = $wpdb->get_row($query);
503
                $restSeconds     = isset($resultSeconds) ? ($resultSeconds->rest) : 0;
504
                $secondsToExpire =
505
                    ($restSeconds > self::CONCURRENCY_TIMEOUT) ? self::CONCURRENCY_TIMEOUT : $restSeconds;
506
                sleep($secondsToExpire + 1);
507
508
                $logMessage = sprintf(
509
                    'User waiting %s seconds, default seconds %s, bd time to expire %s seconds',
510
                    $secondsToExpire,
511
                    self::CONCURRENCY_TIMEOUT,
512
                    $restSeconds
513
                );
514
                WC_Pagantis_Logger::insert_log_entry_in_wpdb(null, $logMessage);
515
            }
516
        }
517
    }
518
519
    /**
520
     * @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...
521
     *
522
     * @return JsonExceptionResponse|JsonSuccessResponse
523
     * @throws ConcurrencyException
524
     */
525
    private function buildResponse($exception = null)
526
    {
527
        $this->unblockConcurrency($this->woocommerceOrderId);
528
529
        if ($exception === null) {
0 ignored issues
show
introduced by
The condition $exception === null is always true.
Loading history...
530
            $jsonResponse = new JsonSuccessResponse();
531
        } else {
532
            $jsonResponse = new JsonExceptionResponse();
533
            $jsonResponse->setException($exception);
534
        }
535
536
        $jsonResponse->setMerchantOrderId($this->woocommerceOrderId);
537
        $jsonResponse->setPagantisOrderId($this->pagantisOrderId);
538
539
        if ($_SERVER['REQUEST_METHOD'] === 'POST') {
540
            $jsonResponse->printResponse();
541
        } else {
542
            return $jsonResponse;
543
        }
544
    }
545
546
    /**
547
     * GETTERS & SETTERS
548
     */
549
550
    /**
551
     * @return mixed
552
     */
553
    public function getOrigin()
554
    {
555
        return $this->origin;
556
    }
557
558
    /**
559
     * @param mixed $origin
560
     */
561
    public function setOrigin($origin)
562
    {
563
        $this->origin = $origin;
564
    }
565
}
566