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

WcPagantisNotify::setUrlToken()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
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 $urlTokenVerification = 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
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
        if ($this->pagantisOrderId == '') {
170
            throw new NoIdentificationException();
171
        }
172
    }
173
174
    /**
175
     * @throws OrderNotFoundException
176
     */
177
    private function getPagantisOrder()
178
    {
179
        try {
180
            $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

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

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

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

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

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