Passed
Pull Request — master (#83)
by
unknown
02:56
created

WcPagantisNotify::checkDbTable()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 17
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 8
c 1
b 0
f 0
nc 2
nop 0
dl 0
loc 17
rs 10
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
    /** @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...
55
    protected $product;
56
57
    /** @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...
58
    protected $urlToken = null;
59
60
    /**
61
     * Validation vs PagantisClient
62
     *
63
     * @return JsonExceptionResponse|JsonSuccessResponse
64
     * @throws ConcurrencyException
65
     */
66
    public function processInformation()
67
    {
68
        try {
69
            require_once(__ROOT__.'/vendor/autoload.php');
70
            try {
71
                if ($_SERVER['REQUEST_METHOD'] == 'GET' && $_GET['origin'] == 'notification') {
72
                    return $this->buildResponse();
73
                }
74
75
76
                $this->checkConcurrency();
77
                $this->getProductType();
78
                $this->getMerchantOrder();
79
                $this->getPagantisOrderId();
80
                $this->getPagantisOrder();
81
                $checkAlreadyProcessed = $this->checkOrderStatus();
82
                if ($checkAlreadyProcessed) {
83
                    return $this->buildResponse();
84
                }
85
                $this->validateAmount();
86
                if ($this->checkMerchantOrderStatus()) {
87
                    $this->processMerchantOrder();
88
                }
89
            } catch (\Exception $exception) {
90
                $this->insertLog($exception);
91
92
                return $this->buildResponse($exception);
93
            }
94
95
            try {
96
                $this->confirmPagantisOrder();
97
98
                return $this->buildResponse();
99
            } catch (\Exception $exception) {
100
                $this->rollbackMerchantOrder();
101
                $this->insertLog($exception);
102
103
                return $this->buildResponse($exception);
104
            }
105
        } catch (\Exception $exception) {
106
            $this->insertLog($exception);
107
            return $this->buildResponse($exception);
108
        }
109
    }
110
111
    /**
112
     * COMMON FUNCTIONS
113
     */
114
115
    /**
116
     * @throws ConcurrencyException
117
     * @throws QuoteNotFoundException
118
     */
119
    private function checkConcurrency()
120
    {
121
        $this->woocommerceOrderId = $_GET['order-received'];
122
        if ($this->woocommerceOrderId == '') {
123
            throw new QuoteNotFoundException();
124
        }
125
126
        $this->unblockConcurrency();
127
        $this->blockConcurrency($this->woocommerceOrderId);
128
    }
129
130
    /**
131
     * getProductType
132
     */
133
    private function getProductType()
134
    {
135
        if ($_GET['product'] == '') {
136
            $this->setProduct(WcPagantisGateway::METHOD_ID);
137
        } else {
138
            $this->setProduct($_GET['product']);
139
        }
140
    }
141
142
    /**
143
     * @throws MerchantOrderNotFoundException
144
     */
145
    private function getMerchantOrder()
146
    {
147
        try {
148
            $this->woocommerceOrder = new WC_Order($this->woocommerceOrderId);
149
            $this->woocommerceOrder->set_payment_method_title($this->getProduct());
150
        } catch (\Exception $e) {
151
            throw new MerchantOrderNotFoundException();
152
        }
153
    }
154
155
    /**
156
     * @throws NoIdentificationException|MerchantOrderNotFoundException
157
     */
158
    private function getPagantisOrderId()
159
    {
160
        global $wpdb;
161
162
        $this->setUrlToken();
163
164
        $this->checkDbTable();
165
        $tableName = $wpdb->prefix.PG_CART_PROCESS_TABLE;
166
        $order_id = $wpdb->get_var("SELECT order_id FROM $tableName WHERE token='{$this->getUrlToken()}' ");
167
        $this->pagantisOrderId = $order_id;
168
169
        $this->insertLog(null, '$order_id '. json_encode($order_id, JSON_PRETTY_PRINT) . " LINE:".__LINE__);
170
171
        if ($this->pagantisOrderId == '') {
172
            throw new NoIdentificationException();
173
        }
174
    }
175
176
    /**
177
     * @throws OrderNotFoundException
178
     */
179
    private function getPagantisOrder()
180
    {
181
        try {
182
            $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

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

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

316
            /** @scrutinizer ignore-call */ 
317
            dbDelta($sql);
Loading history...
317
        }
318
    }
319
320
    /**
321
     * Check if logs table exists
322
     */
323
    private function checkDbLogTable()
324
    {
325
        global $wpdb;
326
        $tableName = $wpdb->prefix.PG_LOGS_TABLE_NAME;
327
328
        if ($wpdb->get_var("SHOW TABLES LIKE '$tableName'") != $tableName) {
329
            $charset_collate = $wpdb->get_charset_collate();
330
            $sql = "CREATE TABLE $tableName ( id int NOT NULL AUTO_INCREMENT, log text NOT NULL, 
331
                    createdAt timestamp DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY id (id)) $charset_collate";
332
333
            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...
334
            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

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

427
        $this->woocommerceOrder->update_status('pending', /** @scrutinizer ignore-call */ __('Pending payment', 'woocommerce'));
Loading history...
428
    }
429
430
    /**
431
     * @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...
432
     * @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...
433
     */
434
    private function insertLog($exception = null, $message = null)
435
    {
436
        global $wpdb;
437
438
        $this->checkDbLogTable();
439
        $logEntry     = new LogEntry();
440
        if ($exception instanceof \Exception) {
441
            $logEntry = $logEntry->error($exception);
442
        } else {
443
            $logEntry = $logEntry->info($message);
444
        }
445
446
        $tableName = $wpdb->prefix.PG_LOGS_TABLE_NAME;
447
        $wpdb->insert($tableName, array('log' => $logEntry->toJson()));
448
    }
449
450
    /**
451
     * @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...
452
     *
453
     * @throws ConcurrencyException
454
     */
455
    private function unblockConcurrency($orderId = null)
456
    {
457
        global $wpdb;
458
        $tableName = $wpdb->prefix.PG_CONCURRENCY_TABLE_NAME;
459
        if ($orderId == null) {
0 ignored issues
show
introduced by
The condition $orderId == null is always true.
Loading history...
460
            $query = "DELETE FROM $tableName WHERE createdAt<(NOW()- INTERVAL ".self::CONCURRENCY_TIMEOUT." SECOND)";
461
        } else {
462
            $query = "DELETE FROM $tableName WHERE order_id = $orderId";
463
        }
464
        $resultDelete = $wpdb->query($query);
465
        if ($resultDelete === false) {
466
            throw new ConcurrencyException();
467
        }
468
    }
469
470
    /**
471
     * @param $orderId
472
     *
473
     * @throws ConcurrencyException
474
     */
475
    private function blockConcurrency($orderId)
476
    {
477
        global $wpdb;
478
        $tableName = $wpdb->prefix.PG_CONCURRENCY_TABLE_NAME;
479
        $insertResult = $wpdb->insert($tableName, array('order_id' => $orderId));
480
        if ($insertResult === false) {
481
            if ($this->getOrigin() == 'Notify') {
482
                throw new ConcurrencyException();
483
            } else {
484
                $query = sprintf(
485
                    "SELECT TIMESTAMPDIFF(SECOND,NOW()-INTERVAL %s SECOND, createdAt) as rest FROM %s WHERE %s",
486
                    self::CONCURRENCY_TIMEOUT,
487
                    $tableName,
488
                    "order_id=$orderId"
489
                );
490
                $resultSeconds = $wpdb->get_row($query);
491
                $restSeconds = isset($resultSeconds) ? ($resultSeconds->rest) : 0;
492
                $secondsToExpire = ($restSeconds>self::CONCURRENCY_TIMEOUT) ? self::CONCURRENCY_TIMEOUT : $restSeconds;
493
                sleep($secondsToExpire+1);
494
495
                $logMessage = sprintf(
496
                    "User waiting %s seconds, default seconds %s, bd time to expire %s seconds",
497
                    $secondsToExpire,
498
                    self::CONCURRENCY_TIMEOUT,
499
                    $restSeconds
500
                );
501
                $this->insertLog(null, $logMessage);
502
            }
503
        }
504
    }
505
506
    /**
507
     * @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...
508
     *
509
     *
510
     * @return JsonExceptionResponse|JsonSuccessResponse
511
     * @throws ConcurrencyException
512
     */
513
    private function buildResponse($exception = null)
514
    {
515
        $this->unblockConcurrency($this->woocommerceOrderId);
516
517
        if ($exception == null) {
0 ignored issues
show
introduced by
The condition $exception == null is always true.
Loading history...
518
            $jsonResponse = new JsonSuccessResponse();
519
        } else {
520
            $jsonResponse = new JsonExceptionResponse();
521
            $jsonResponse->setException($exception);
522
        }
523
524
        $jsonResponse->setMerchantOrderId($this->woocommerceOrderId);
525
        $jsonResponse->setPagantisOrderId($this->pagantisOrderId);
526
527
        if ($_SERVER['REQUEST_METHOD'] == 'POST') {
528
            $jsonResponse->printResponse();
529
        } else {
530
            return $jsonResponse;
531
        }
532
    }
533
534
    /**
535
     * GETTERS & SETTERS
536
     */
537
538
    /**
539
     * @return mixed
540
     */
541
    public function getOrigin()
542
    {
543
        return $this->origin;
544
    }
545
546
    /**
547
     * @param mixed $origin
548
     */
549
    public function setOrigin($origin)
550
    {
551
        $this->origin = $origin;
552
    }
553
554
    /**
555
     * @return bool
556
     */
557
    private function isProduct4x()
558
    {
559
        return ($this->product === Ucfirst(WcPagantis4xGateway::METHOD_ID));
560
    }
561
562
    /**
563
     * @return mixed
564
     */
565
    public function getProduct()
566
    {
567
        return $this->product;
568
    }
569
570
    /**
571
     * @param mixed $product
572
     */
573
    public function setProduct($product)
574
    {
575
        $this->product = Ucfirst($product);
576
    }
577
578
    /**
579
     * @return mixed
580
     */
581
    public function getWoocommerceOrderId()
582
    {
583
        return $this->woocommerceOrderId;
584
    }
585
586
    /**
587
     * @return mixed
588
     */
589
    private function getUrlToken()
590
    {
591
        return $this->urlToken;
592
    }
593
594
    /**
595
     * @throws MerchantOrderNotFoundException
596
     */
597
    private function setUrlToken()
598
    {
599
        $this->urlToken = $_GET['token'];
600
601
        if (is_null($this->urlToken)) {
602
            throw new MerchantOrderNotFoundException();
603
        }
604
    }
605
606
}
607