Passed
Pull Request — master (#47)
by pablo
04:23
created

WcPagantisNotify::checkPagantisDeepStatus()   A

Complexity

Conditions 5
Paths 8

Size

Total Lines 19
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 13
c 0
b 0
f 0
nc 8
nop 1
dl 0
loc 19
rs 9.5222
1
<?php
2
3
use Pagantis\ModuleUtils\Exception\OrderNotCreatedException;
4
use Pagantis\OrdersApiClient\Client;
5
use Pagantis\ModuleUtils\Exception\ConcurrencyException;
6
use Pagantis\ModuleUtils\Exception\AlreadyProcessedException;
7
use Pagantis\ModuleUtils\Exception\AmountMismatchException;
8
use Pagantis\ModuleUtils\Exception\MerchantOrderNotFoundException;
9
use Pagantis\ModuleUtils\Exception\NoIdentificationException;
10
use Pagantis\ModuleUtils\Exception\OrderNotFoundException;
11
use Pagantis\ModuleUtils\Exception\QuoteNotFoundException;
12
use Pagantis\ModuleUtils\Exception\UnknownException;
13
use Pagantis\ModuleUtils\Exception\WrongStatusException;
14
use Pagantis\ModuleUtils\Model\Response\JsonSuccessResponse;
15
use Pagantis\ModuleUtils\Model\Response\JsonExceptionResponse;
16
use Pagantis\ModuleUtils\Model\Log\LogEntry;
17
use Pagantis\OrdersApiClient\Model\Order;
18
19
if ( ! defined('ABSPATH')) {
20
    exit;
21
}
22
23
class WcPagantisNotify extends WcPagantisGateway
24
{
25
    /** Concurrency table name  */
26
    const CONCURRENCY_TABLE = 'pagantis_concurrency';
27
28
    /** Seconds to expire a locked request */
29
    const CONCURRENCY_TIMEOUT = 5;
30
31
    /** @var mixed $pagantisOrder */
32
    protected $pagantisOrder;
33
34
    /** @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...
35
    public $origin;
36
37
    /** @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...
38
    public $order;
39
40
    /** @var mixed $woocommerceOrderId */
41
    protected $woocommerceOrderId = '';
42
43
    /** @var mixed $cfg */
44
    protected $cfg;
45
46
    /** @var Client $orderClient */
47
    protected $orderClient;
48
49
    /** @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...
50
    protected $woocommerceOrder;
51
52
    /** @var mixed $pagantisOrderId */
53
    protected $pagantisOrderId = '';
54
55
56
    /**
57
     * Validation vs PagantisClient
58
     *
59
     * @return JsonExceptionResponse|JsonSuccessResponse
60
     * @throws ConcurrencyException
61
     */
62
    public function processInformation()
63
    {
64
        try {
65
            require_once(__ROOT__ . '/vendor/autoload.php');
66
            try {
67
                if ($_SERVER['REQUEST_METHOD'] == 'GET' && $_GET['origin'] == 'notification') {
68
                    return $this->buildResponse();
69
                }
70
                $this->checkConcurrency();
71
                $this->getMerchantOrder();
72
                $this->getPgOrderId();
73
                $this->getPgOrder();
74
                $isPagantisOrderProcessed = $this->checkPgOrderStatus();
75
                if ($isPagantisOrderProcessed) {
76
                    return $this->buildResponse();
77
                }
78
                $this->validateAmount();
79
                if ($this->checkMerchantOrderStatus()) {
80
                    $this->processMerchantOrder();
81
                }
82
            } catch (\Exception $exception) {
83
                $this->insertLog($exception);
84
85
                return $this->buildResponse($exception);
86
            }
87
88
            try {
89
                $this->confirmPgOrder();
90
91
                return $this->buildResponse();
92
            } catch (\Exception $exception) {
93
                $this->rollbackMerchantOrder();
94
                $this->insertLog($exception);
95
96
                return $this->buildResponse($exception);
97
            }
98
        } catch (\Exception $exception) {
99
            $this->insertLog($exception);
100
101
            return $this->buildResponse($exception);
102
        }
103
    }
104
105
    /**
106
     * COMMON FUNCTIONS
107
     */
108
109
    /**
110
     * @throws ConcurrencyException
111
     * @throws QuoteNotFoundException
112
     */
113
    private function checkConcurrency()
114
    {
115
        $this->woocommerceOrderId = $_GET['order-received'];
116
        if ($this->woocommerceOrderId == '') {
117
            throw new QuoteNotFoundException();
118
        }
119
120
        $this->unblockConcurrency();
121
        $this->blockConcurrency($this->woocommerceOrderId);
122
    }
123
124
    /**
125
     * @throws MerchantOrderNotFoundException
126
     */
127
    private function getMerchantOrder()
128
    {
129
        try {
130
            $this->woocommerceOrder = new WC_Order($this->woocommerceOrderId);
131
            $this->woocommerceOrder->set_payment_method_title(Ucfirst(WcPagantisGateway::METHOD_ID));
132
        } catch (\Exception $e) {
133
            throw new MerchantOrderNotFoundException();
134
        }
135
    }
136
137
    /**
138
     * @throws NoIdentificationException
139
     */
140
    private function getPgOrderId()
141
    {
142
        global $wpdb;
143
        $this->checkDbTable();
144
        $tableName             = $wpdb->prefix . self::ORDERS_TABLE;
145
        $queryResult           = $wpdb->get_row("select order_id from $tableName where id='" . $this->woocommerceOrderId . "'");
146
        $this->pagantisOrderId = $queryResult->order_id;
147
148
        if ($this->pagantisOrderId == '') {
149
            throw new NoIdentificationException();
150
        }
151
    }
152
153
    /**
154
     * @throws OrderNotFoundException
155
     */
156
    private function getPgOrder()
157
    {
158
        try {
159
            $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

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

198
        $isOrderStatusValid = /** @scrutinizer ignore-call */ apply_filters('woocommerce_valid_order_statuses_for_payment_complete', $validOrderStatuses, $this);
Loading history...
199
        $this->woocommerceOrder->save();
200
        if ( ! $this->woocommerceOrder->has_status($isOrderStatusValid)) { // TO CONFIRM
201
            $logMessage = "WARNING checkMerchantOrderStatus." . " Merchant order id:" . $this->woocommerceOrder->get_id() . " Merchant order status:"
202
                          . $this->woocommerceOrder->get_status() . " Pagantis order id:" . $this->pagantisOrder->getStatus()
203
                          . " Pagantis order status:" . $this->pagantisOrder->getId();
204
205
            $this->insertLog(null, $logMessage);
206
            $this->woocommerceOrder->add_order_note($logMessage);
207
208
            return false;
209
        } else {
210
            return true; //TO SAVE
211
        }
212
    }
213
214
    /**
215
     * @throws AmountMismatchException
216
     */
217
    private function validateAmount()
218
    {
219
        $pagantisAmount = $this->pagantisOrder->getShoppingCart()->getTotalAmount();
220
        $wcAmount       = intval(strval(100 * $this->woocommerceOrder->get_total()));
221
        if ($pagantisAmount != $wcAmount) {
222
            throw new AmountMismatchException($pagantisAmount, $wcAmount);
223
        }
224
    }
225
226
    /**
227
     * @throws Exception
228
     */
229
    private function processMerchantOrder()
230
    {
231
        $this->saveMerchantOrder();
232
        $this->updateDbInfo();
233
    }
234
235
    /**
236
     * @return false|string
237
     * @throws UnknownException
238
     */
239
    private function confirmPgOrder()
240
    {
241
        try {
242
            $this->pagantisOrder = $this->orderClient->confirmOrder($this->pagantisOrderId);
243
        } catch (\Exception $e) {
244
            $this->pagantisOrder = $this->orderClient->getOrder($this->pagantisOrderId);
245
            if ($this->pagantisOrder->getStatus() !== Order::STATUS_CONFIRMED) {
246
                throw new UnknownException($e->getMessage());
247
            } else {
248
                $logMessage = 'Concurrency issue: Order_id ' . $this->pagantisOrderId . ' was confirmed by other process';
249
                $this->insertLog(null, $logMessage);
250
            }
251
        }
252
253
        $jsonResponse = new JsonSuccessResponse();
254
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
297
        return;
298
    }
299
300
    /** STEP 2 GMO - Get Merchant Order */
301
    /** STEP 3 GPOI - Get Pagantis OrderId */
302
    /** STEP 4 GPO - Get Pagantis Order */
303
    /** STEP 5 COS - Check Order Status */
304
305
    /**
306
     * @param $statusArray
307
     *
308
     * @throws \Exception
309
     */
310
    private function checkPagantisDeepStatus($statusArray)
311
    {
312
        $pagantisStatus = array();
313
        foreach ($statusArray as $status) {
314
            $pagantisStatus[] = constant("\Pagantis\OrdersApiClient\Model\Order::STATUS_$status");
315
        }
316
317
        if ($this->pagantisOrder instanceof Order) {
318
            $paid = in_array($this->pagantisOrder->getStatus(), $pagantisStatus);
319
            if ( ! $paid) {
320
                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...
321
                    $status = $this->pagantisOrder->getStatus();
322
                } else {
323
                    $status = '-';
324
                }
325
                throw new WrongStatusException($status);
326
            }
327
        } else {
328
            throw new OrderNotFoundException();
329
        }
330
    }
331
332
333
    /** STEP 6 CMOS - Check Merchant Order Status */
334
    /** STEP 7 VA - Validate Amount */
335
    /** STEP 8 PMO - Process Merchant Order */
336
    /**
337
     * @throws \Exception
338
     */
339
    private function saveMerchantOrder()
340
    {
341
        global $woocommerce;
342
        $paymentResult = $this->woocommerceOrder->payment_complete();
343
        if ($paymentResult) {
344
            $metadataOrder = $this->pagantisOrder->getMetadata();
345
            $metadataInfo  = null;
346
            foreach ($metadataOrder as $metadataKey => $metadataValue) {
347
                if ($metadataKey == 'promotedProduct') {
348
                    $metadataInfo .= "/Producto promocionado = $metadataValue";
349
                }
350
            }
351
352
            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...
353
                $this->woocommerceOrder->add_order_note($metadataInfo);
354
            }
355
356
            $this->woocommerceOrder->add_order_note("Notification received via $this->origin");
357
            $this->woocommerceOrder->add_order_note("Order Payment Completed via $this->origin");
358
359
            // https://docs.woocommerce.com/wc-apidocs/source-function-wc_reduce_stock_levels.html#147
360
            //$this->woocommerceOrder->reduce_order_stock();
361
            wc_reduce_stock_levels($this->woocommerceOrderId);
0 ignored issues
show
Bug introduced by
The function wc_reduce_stock_levels 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

361
            /** @scrutinizer ignore-call */ 
362
            wc_reduce_stock_levels($this->woocommerceOrderId);
Loading history...
362
            $this->woocommerceOrder->save();
363
364
365
            //$woocommerce->cart->empty_cart();
366
            sleep(3);
367
        } else {
368
            throw new UnknownException('Order can not be saved');
369
        }
370
    }
371
372
    /**
373
     * Save the merchant order_id with the related identification
374
     */
375
    private function updateDbInfo()
376
    {
377
        global $wpdb;
378
379
        $this->checkDbTable();
380
        $tableName = $wpdb->prefix . self::ORDERS_TABLE;
381
382
        $wpdb->update($tableName, array('wc_order_id' => $this->woocommerceOrderId), array('id' => $this->woocommerceOrderId), array('%s'),
383
            array('%d'));
384
    }
385
386
    /** STEP 9 CPO - Confirmation Pagantis Order */
387
    private function rollbackMerchantOrder()
388
    {
389
        $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

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