Passed
Pull Request — master (#82)
by
unknown
05:03
created

WcPagantisNotify::getPagantisOrderIdFromDB()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 5
c 0
b 0
f 0
nc 1
nop 0
dl 0
loc 8
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
25
26
    /** Seconds to expire a locked request */
27
    const CONCURRENCY_TIMEOUT = 5;
28
29
    /** @var mixed $pagantisOrder */
30
    protected $pagantisOrder;
31
32
    /** @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...
33
    public $origin;
34
35
    /** @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...
36
    public $order;
37
38
    /** @var mixed $woocommerceOrderId */
39
    protected $woocommerceOrderId = '';
40
41
    /** @var mixed $cfg */
42
    protected $cfg;
43
44
    /** @var Client $orderClient */
45
    protected $orderClient;
46
47
    /** @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...
48
    protected $woocommerceOrder;
49
50
    /** @var mixed $pagantisOrderId */
51
    protected $pagantisOrderId = '';
52
53
    /** @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...
54
    protected $product;
55
56
    /** @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...
57
    protected $verificationToken = null;
58
59
    /**
60
     * Validation vs PagantisClient
61
     *
62
     * @return JsonExceptionResponse|JsonSuccessResponse
63
     * @throws ConcurrencyException
64
     */
65
    public function processNotification()
66
    {
67
        try {
68
            require_once(__ROOT__ . '/vendor/autoload.php');
69
            try {
70
                if ($_SERVER['REQUEST_METHOD'] == 'GET' && $_GET['origin'] == 'notification') {
71
                    return $this->buildResponse();
72
                }
73
74
                $this->setWoocommerceOrderId();
75
                $this->setVerificationToken();
76
                $this->verifyOrderConformity();
77
                $this->checkConcurrency();
78
                $this->setPaymentProductType();
79
                $this->getMerchantOrder();
80
                $this->setPagantisOrderId();
81
                $this->getPagantisOrder();
82
                $checkAlreadyProcessed = $this->checkOrderStatus();
83
                if ($checkAlreadyProcessed) {
84
                    return $this->buildResponse();
85
                }
86
                $this->validateAmount();
87
                if ($this->checkMerchantOrderStatus()) {
88
                    $this->processMerchantOrder();
89
                }
90
            } catch (\Exception $exception) {
91
                $this->insertLog($exception);
92
93
                return $this->buildResponse($exception);
94
            }
95
96
            try {
97
                $this->confirmPagantisOrder();
98
99
                return $this->buildResponse();
100
            } catch (\Exception $exception) {
101
                $this->rollbackMerchantOrder();
102
                $this->insertLog($exception);
103
104
                return $this->buildResponse($exception);
105
            }
106
        } catch (\Exception $exception) {
107
            $this->insertLog($exception);
108
            return $this->buildResponse($exception);
109
        }
110
    }
111
112
    /**
113
     * COMMON FUNCTIONS
114
     */
115
116
    /**
117
     * @throws ConcurrencyException
118
     */
119
    private function checkConcurrency()
120
    {
121
        $this->unblockConcurrency();
122
        $this->blockConcurrency($this->woocommerceOrderId);
123
    }
124
125
    /**
126
     * getProductType
127
     */
128
    private function setPaymentProductType()
129
    {
130
        if ($_GET['product'] == '') {
131
            $this->setProduct(WcPagantisGateway::METHOD_ID);
132
        } else {
133
            $this->setProduct($_GET['product']);
134
        }
135
    }
136
137
    /**
138
     * @throws MerchantOrderNotFoundException
139
     */
140
    private function getMerchantOrder()
141
    {
142
        try {
143
            $this->woocommerceOrder = new WC_Order($this->woocommerceOrderId);
144
            $this->woocommerceOrder->set_payment_method_title($this->getProduct());
145
        } catch (\Exception $e) {
146
            throw new MerchantOrderNotFoundException();
147
        }
148
    }
149
150
    /**
151
     * @throws MerchantOrderNotFoundException
152
     */
153
    private function verifyOrderConformity()
154
    {
155
        global $wpdb;
156
        $this->checkDbTable();
157
        $tableName =$wpdb->prefix . PG_OLD_CART_PROCESS_TABLE;
158
        $tokenCount=$wpdb->get_var($wpdb->prepare("SELECT COUNT(wc_order_id) 
159
                                                                    FROM $tableName 
160
                                                                    WHERE token = %s",
161
                                                                $this->getVerificationToken()));
162
        $orderIDCount = $wpdb->get_var(
163
            $wpdb->prepare("SELECT COUNT(token) 
164
                                        FROM $tableName 
165
                                        WHERE wc_order_id = %s",
166
                                    $this->getWoocommerceOrderId()));
167
        if (!($tokenCount == 1 && $orderIDCount == 1)) {
168
            throw new MerchantOrderNotFoundException();
169
        }
170
    }
171
172
    private function getPagantisOrderIdFromDB()
173
    {
174
        global $wpdb;
175
        $this->checkDbTable();
176
        $tableName = $wpdb->prefix.PG_OLD_CART_PROCESS_TABLE;
177
        $queryResult = $wpdb->get_row("SELECT order_id FROM $tableName WHERE token='{$this->getVerificationToken()}'");
178
179
        return $queryResult->order_id;
180
    }
181
182
    /**
183
     * @throws NoIdentificationException
184
     */
185
    private function setPagantisOrderId()
186
    {
187
        $this->pagantisOrderId = $this->getPagantisOrderIdFromDB();
188
189
        if (empty($this->pagantisOrderId)) {
190
            throw new NoIdentificationException();
191
        }
192
    }
193
194
    /**
195
     * @throws OrderNotFoundException
196
     */
197
    private function getPagantisOrder()
198
    {
199
        try {
200
            $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

200
            $this->cfg = /** @scrutinizer ignore-call */ get_option('woocommerce_pagantis_settings');
Loading history...
201
            $this->cfg = get_option('woocommerce_pagantis_settings');
202
            if ($this->isProduct4x()) {
203
                $publicKey = $this->cfg['pagantis_public_key_4x'];
204
                $secretKey = $this->cfg['pagantis_private_key_4x'];
205
            } else {
206
                $publicKey = $this->cfg['pagantis_public_key'];
207
                $secretKey = $this->cfg['pagantis_private_key'];
208
            }
209
210
            $this->orderClient = new Client($publicKey, $secretKey);
211
            $this->pagantisOrder = $this->orderClient->getOrder($this->pagantisOrderId);
212
        } catch (\Exception $e) {
213
            throw new OrderNotFoundException();
214
        }
215
    }
216
217
    /**
218
     * @return bool
219
     * @throws WrongStatusException
220
     */
221
    private function checkOrderStatus()
222
    {
223
        try {
224
            $this->checkPagantisStatus(array('AUTHORIZED'));
225
        } catch (\Exception $e) {
226
            if ($this->pagantisOrder instanceof Order) {
227
                $status = $this->pagantisOrder->getStatus();
228
            } else {
229
                $status = '-';
230
            }
231
232
            if ($status === Order::STATUS_CONFIRMED) {
233
                return true;
234
            }
235
            throw new WrongStatusException($status);
236
        }
237
    }
238
239
    /**
240
     * @return bool
241
     */
242
    private function checkMerchantOrderStatus()
243
    {
244
        //Order status reference => https://docs.woocommerce.com/document/managing-orders/
245
        $validStatus=array('on-hold', 'pending', 'failed', 'processing', 'completed');
246
        $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

246
        $isValidStatus = /** @scrutinizer ignore-call */ apply_filters(
Loading history...
247
            'woocommerce_valid_order_statuses_for_payment_complete',
248
            $validStatus,
249
            $this
250
        );
251
252
        if (!$this->woocommerceOrder->has_status($isValidStatus)) { // TO CONFIRM
253
            $logMessage = "WARNING checkMerchantOrderStatus." .
254
                          " Merchant order id:".$this->woocommerceOrder->get_id().
255
                          " Merchant order status:".$this->woocommerceOrder->get_status().
256
                          " Pagantis order id:".$this->pagantisOrder->getStatus().
257
                          " Pagantis order status:".$this->pagantisOrder->getId().
258
                          " Pagantis urlToken: ".$this->getVerificationToken();
259
            
260
            $this->insertLog(null, $logMessage);
261
            $this->woocommerceOrder->add_order_note($logMessage);
262
            $this->woocommerceOrder->save();
263
            return false;
264
        }
265
266
        return true; //TO SAVE
267
    }
268
269
    /**
270
     * @throws AmountMismatchException
271
     */
272
    private function validateAmount()
273
    {
274
        $pagantisAmount = $this->pagantisOrder->getShoppingCart()->getTotalAmount();
275
        $wcAmount = intval(strval(100 * $this->woocommerceOrder->get_total()));
276
        if ($pagantisAmount != $wcAmount) {
277
            throw new AmountMismatchException($pagantisAmount, $wcAmount);
278
        }
279
    }
280
281
    /**
282
     * @throws Exception
283
     */
284
    private function processMerchantOrder()
285
    {
286
        $this->saveOrder();
287
        $this->updateBdInfo();
288
    }
289
290
    /**
291
     * @return false|string
292
     * @throws UnknownException
293
     */
294
    private function confirmPagantisOrder()
295
    {
296
        try {
297
            $this->pagantisOrder = $this->orderClient->confirmOrder($this->pagantisOrderId);
298
        } catch (\Exception $e) {
299
            $this->pagantisOrder = $this->orderClient->getOrder($this->pagantisOrderId);
300
            if ($this->pagantisOrder->getStatus() !== Order::STATUS_CONFIRMED) {
301
                throw new UnknownException($e->getMessage());
302
            } else {
303
                $logMessage = 'Concurrency issue: Order_id ' . $this->pagantisOrderId . ' was confirmed by other process';
304
                $this->insertLog(null, $logMessage);
305
            }
306
        }
307
308
        $jsonResponse = new JsonSuccessResponse();
309
310
        return $jsonResponse->toJson();
311
    }
312
313
    /**
314
     * UTILS FUNCTIONS
315
     */
316
    /** STEP 1 CC - Check concurrency */
317
318
    /**
319
     * Check if cart processing table exists
320
     */
321
    private function checkDbTable()
322
    {
323
        if (isPgTableCreated(PG_OLD_CART_PROCESS_TABLE)) {
324
            alterCartProcessingTable();
325
        } else {
326
            createOrderProcessingTable();
327
        }
328
    }
329
330
    /**
331
     * Check if logs table exists
332
     */
333
    private function checkDbLogTable()
334
    {
335
        global $wpdb;
336
        $tableName = $wpdb->prefix.PG_LOGS_TABLE_NAME;
337
338
        if ($wpdb->get_var("SHOW TABLES LIKE '$tableName'") != $tableName) {
339
            $charset_collate = $wpdb->get_charset_collate();
340
            $sql = "CREATE TABLE $tableName ( id int NOT NULL AUTO_INCREMENT, log text NOT NULL, 
341
                    createdAt timestamp DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY id (id)) $charset_collate";
342
343
            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...
344
            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

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

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