SnappPay   B
last analyzed

Complexity

Total Complexity 49

Size/Duplication

Total Lines 450
Duplicated Lines 0 %

Importance

Changes 3
Bugs 1 Features 0
Metric Value
eloc 212
c 3
b 1
f 0
dl 0
loc 450
rs 8.48
wmc 49

15 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
B purchase() 0 60 6
A pay() 0 5 1
A normalizerAmount() 0 3 2
A oauth() 0 27 2
A setPaymentUrl() 0 3 1
B update() 0 47 6
A status() 0 29 3
A eligible() 0 23 4
A revert() 0 29 3
A settle() 0 29 3
A getPaymentUrl() 0 3 1
A cancel() 0 29 3
B normalizerCartList() 0 21 7
B verify() 0 39 6

How to fix   Complexity   

Complex Class

Complex classes like SnappPay often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use SnappPay, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
4
namespace Shetabit\Multipay\Drivers\SnappPay;
5
6
use GuzzleHttp\Client;
7
use GuzzleHttp\Exception\ConnectException;
8
use GuzzleHttp\RequestOptions;
9
use Shetabit\Multipay\Abstracts\Driver;
10
use Shetabit\Multipay\Contracts\ReceiptInterface;
11
use Shetabit\Multipay\Exceptions\TimeoutException;
12
use Shetabit\Multipay\Exceptions\InvalidPaymentException;
13
use Shetabit\Multipay\Exceptions\PurchaseFailedException;
14
use Shetabit\Multipay\Invoice;
15
use Shetabit\Multipay\Receipt;
16
use Shetabit\Multipay\RedirectionForm;
17
18
class SnappPay extends Driver
19
{
20
    const VERSION = '1.8';
21
    const RELEASE_DATE = '2023-01-08';
22
23
    const OAUTH_URL = '/api/online/v1/oauth/token';
24
    const ELIGIBLE_URL = '/api/online/offer/v1/eligible';
25
    const TOKEN_URL = '/api/online/payment/v1/token';
26
    const VERIFY_URL = '/api/online/payment/v1/verify';
27
    const SETTLE_URL = '/api/online/payment/v1/settle';
28
    const REVERT_URL = '/api/online/payment/v1/revert';
29
    const STATUS_URL = '/api/online/payment/v1/status';
30
    const CANCEL_URL = '/api/online/payment/v1/cancel';
31
    const UPDATE_URL = '/api/online/payment/v1/update';
32
33
    /**
34
     * SnappPay Client.
35
     *
36
     * @var Client
37
     */
38
    protected $client;
39
40
    /**
41
     * Invoice
42
     *
43
     * @var Invoice
44
     */
45
    protected $invoice;
46
47
    /**
48
     * Driver settings
49
     *
50
     * @var object
51
     */
52
    protected $settings;
53
    /**
54
     * SnappPay Oauth Data
55
     *
56
     * @var string
57
     */
58
    protected $oauthToken;
59
60
    /**
61
     * SnappPay payment url
62
     *
63
     * @var string
64
     */
65
    protected $paymentUrl;
66
67
    /**
68
     * SnappPay constructor.
69
     * Construct the class with the relevant settings.
70
     */
71
    public function __construct(Invoice $invoice, $settings)
72
    {
73
        $this->invoice($invoice);
74
        $this->settings = (object) $settings;
75
        $this->client = new Client();
76
        $this->oauthToken = $this->oauth();
77
    }
78
79
    /**
80
     * @throws PurchaseFailedException
81
     */
82
    public function purchase(): string
83
    {
84
        $phone = $this->invoice->getDetail('phone')
85
            ?? $this->invoice->getDetail('cellphone')
86
            ?? $this->invoice->getDetail('mobile');
87
88
        // convert to format +98 901 XXX XXXX
89
        $phone = preg_replace('/^0/', '+98', $phone);
90
91
        $data = [
92
            'amount' => $this->normalizerAmount($this->invoice->getAmount()),
0 ignored issues
show
Bug introduced by
It seems like $this->invoice->getAmount() can also be of type double; however, parameter $amount of Shetabit\Multipay\Driver...Pay::normalizerAmount() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

92
            'amount' => $this->normalizerAmount(/** @scrutinizer ignore-type */ $this->invoice->getAmount()),
Loading history...
93
            'mobile' => $phone,
94
            'paymentMethodTypeDto' => 'INSTALLMENT',
95
            'transactionId' => $this->invoice->getUuid(),
96
            'returnURL' => $this->settings->callbackUrl,
97
        ];
98
99
        if (!is_null($discountAmount = $this->invoice->getDetail('discountAmount'))) {
100
            $data['discountAmount'] = $this->normalizerAmount($discountAmount);
0 ignored issues
show
Bug introduced by
$discountAmount of type string is incompatible with the type integer expected by parameter $amount of Shetabit\Multipay\Driver...Pay::normalizerAmount(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

100
            $data['discountAmount'] = $this->normalizerAmount(/** @scrutinizer ignore-type */ $discountAmount);
Loading history...
101
        }
102
103
        if (!is_null($externalSourceAmount = $this->invoice->getDetail('externalSourceAmount'))) {
104
            $data['externalSourceAmount'] = $this->normalizerAmount($externalSourceAmount) ;
105
        }
106
107
        if (is_null($this->invoice->getDetail('cartList'))) {
108
            throw new PurchaseFailedException('"cartList" is required for this driver');
109
        }
110
111
        $data['cartList'] = $this->invoice->getDetail('cartList');
112
113
        $this->normalizerCartList($data);
114
115
        $response = $this
116
            ->client
117
            ->post(
118
                $this->settings->apiPaymentUrl.self::TOKEN_URL,
119
                [
120
                    RequestOptions::BODY => json_encode($data),
121
                    RequestOptions::HEADERS => [
122
                        'Content-Type' => 'application/json',
123
                        'Authorization' => 'Bearer '.$this->oauthToken,
124
                    ],
125
                    RequestOptions::HTTP_ERRORS => false,
126
                ]
127
            );
128
129
        $body = json_decode($response->getBody()->getContents(), true);
130
131
        if ($response->getStatusCode() != 200 || $body['successful'] === false) {
132
            // error has happened
133
            $message = $body['errorData']['message'] ?? 'خطا در هنگام درخواست برای پرداخت رخ داده است.';
134
            throw new PurchaseFailedException($message);
135
        }
136
137
        $this->invoice->transactionId($body['response']['paymentToken']);
138
        $this->setPaymentUrl($body['response']['paymentPageUrl']);
139
140
        // return the transaction's id
141
        return $this->invoice->getTransactionId();
142
    }
143
144
    public function pay(): RedirectionForm
145
    {
146
        parse_str(parse_url($this->getPaymentUrl(), PHP_URL_QUERY), $formData);
147
148
        return $this->redirectWithForm($this->getPaymentUrl(), $formData, 'GET');
149
    }
150
151
    /**
152
     * @throws TimeoutException
153
     * @throws PurchaseFailedException
154
     */
155
    public function verify(): ReceiptInterface
156
    {
157
        $data = [
158
            'paymentToken' => $this->invoice->getTransactionId(),
159
        ];
160
161
        try {
162
            $response = $this
163
                ->client
164
                ->post(
165
                    $this->settings->apiPaymentUrl.self::VERIFY_URL,
166
                    [
167
                        RequestOptions::TIMEOUT => 60, // 1 minute
168
                        RequestOptions::BODY => json_encode($data),
169
                        RequestOptions::HEADERS => [
170
                            'Content-Type' => 'application/json',
171
                            'Authorization' => 'Bearer '.$this->oauthToken,
172
                        ],
173
                        RequestOptions::HTTP_ERRORS => false,
174
                    ]
175
                );
176
177
            $body = json_decode($response->getBody()->getContents(), true);
178
179
            if ($response->getStatusCode() != 200 || $body['successful'] === false) {
180
                // error has happened
181
                $message = $body['errorData']['message'] ?? 'خطا در هنگام تایید تراکنش';
182
                throw new PurchaseFailedException($message);
183
            }
184
185
            return (new Receipt('snapppay', $body['response']['transactionId']))->detail($body['response']);
186
        } catch (ConnectException $exception) {
187
            $status_response = $this->status();
188
189
            if (isset($status_response['status']) && $status_response['status'] == 'VERIFY') {
190
                return (new Receipt('snapppay', $status_response['transactionId']))->detail($status_response);
191
            }
192
193
            throw new TimeoutException('پاسخی از درگاه دریافت نشد.');
194
        }
195
    }
196
197
    /**
198
     * @throws PurchaseFailedException
199
     */
200
    protected function oauth()
201
    {
202
        $response = $this
203
            ->client
204
            ->post(
205
                $this->settings->apiPaymentUrl.self::OAUTH_URL,
206
                [
207
                    RequestOptions::HEADERS => [
208
                        'Authorization' => 'Basic '.base64_encode("{$this->settings->client_id}:{$this->settings->client_secret}"),
209
                    ],
210
                    RequestOptions::FORM_PARAMS => [
211
                        'grant_type' => 'password',
212
                        'scope' => 'online-merchant',
213
                        'username' => $this->settings->username,
214
                        'password' => $this->settings->password,
215
                    ],
216
                    RequestOptions::HTTP_ERRORS => false,
217
                ]
218
            );
219
220
        if ($response->getStatusCode() != 200) {
221
            throw new PurchaseFailedException('خطا در هنگام احراز هویت.');
222
        }
223
224
        $body = json_decode($response->getBody()->getContents(), true);
225
226
        return $body['access_token'];
227
    }
228
229
    /**
230
     * @throws PurchaseFailedException
231
     */
232
    public function eligible()
233
    {
234
        if (is_null($amount = $this->invoice->getAmount())) {
0 ignored issues
show
introduced by
The condition is_null($amount = $this->invoice->getAmount()) is always false.
Loading history...
235
            throw new PurchaseFailedException('"amount" is required for this method.');
236
        }
237
238
        $response = $this->client->get($this->settings->apiPaymentUrl.self::ELIGIBLE_URL, [
239
            RequestOptions::HEADERS => [
240
                'Authorization' => 'Bearer '.$this->oauthToken,
241
            ],
242
            RequestOptions::QUERY => [
243
                'amount' => $this->normalizerAmount($amount),
0 ignored issues
show
Bug introduced by
It seems like $amount can also be of type double; however, parameter $amount of Shetabit\Multipay\Driver...Pay::normalizerAmount() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

243
                'amount' => $this->normalizerAmount(/** @scrutinizer ignore-type */ $amount),
Loading history...
244
            ],
245
        ]);
246
247
        $body = json_decode($response->getBody()->getContents(), true);
248
249
        if ($response->getStatusCode() != 200 || $body['successful'] === false) {
250
            $message = $body['errorData']['message'] ?? 'خطا در هنگام درخواست برای پرداخت رخ داده است.';
251
            throw new InvalidPaymentException($message, (int) $response->getStatusCode());
252
        }
253
254
        return $body['response'];
255
    }
256
257
    private function normalizerAmount(int $amount): int
258
    {
259
        return $amount * ($this->settings->currency == 'T' ? 10 : 1);
260
    }
261
262
    private function normalizerCartList(array &$data): void
263
    {
264
        if (isset($data['cartList']['shippingAmount'])) {
265
            $data['cartList'] = [$data['cartList']];
266
        }
267
268
        foreach ($data['cartList'] as &$item) {
269
            if (isset($item['shippingAmount'])) {
270
                $item['shippingAmount'] = $this->normalizerAmount($item['shippingAmount']);
271
            }
272
273
            if (isset($item['taxAmount'])) {
274
                $item['taxAmount'] = $this->normalizerAmount($item['taxAmount']);
275
            }
276
277
            if (isset($item['totalAmount'])) {
278
                $item['totalAmount'] = $this->normalizerAmount($item['totalAmount']);
279
            }
280
281
            foreach ($item['cartItems'] as &$cartItem) {
282
                $cartItem['amount'] = $this->normalizerAmount($cartItem['amount']);
283
            }
284
        }
285
    }
286
287
    public function settle(): array
288
    {
289
        $data = [
290
            'paymentToken' => $this->invoice->getTransactionId(),
291
        ];
292
293
        $response = $this
294
            ->client
295
            ->post(
296
                $this->settings->apiPaymentUrl.self::SETTLE_URL,
297
                [
298
                    RequestOptions::BODY => json_encode($data),
299
                    RequestOptions::HEADERS => [
300
                        'Content-Type' => 'application/json',
301
                        'Authorization' => 'Bearer '.$this->oauthToken,
302
                    ],
303
                    RequestOptions::HTTP_ERRORS => false,
304
                ]
305
            );
306
307
        $body = json_decode($response->getBody()->getContents(), true);
308
309
        if ($response->getStatusCode() != 200 || $body['successful'] === false) {
310
            // error has happened
311
            $message = $body['errorData']['message'] ?? 'خطا در Settle تراکنش';
312
            throw new PurchaseFailedException($message);
313
        }
314
315
        return $body['response'];
316
    }
317
318
    public function revert()
319
    {
320
        $data = [
321
            'paymentToken' => $this->invoice->getTransactionId(),
322
        ];
323
324
        $response = $this
325
            ->client
326
            ->post(
327
                $this->settings->apiPaymentUrl.self::REVERT_URL,
328
                [
329
                    RequestOptions::BODY => json_encode($data),
330
                    RequestOptions::HEADERS => [
331
                        'Content-Type' => 'application/json',
332
                        'Authorization' => 'Bearer '.$this->oauthToken,
333
                    ],
334
                    RequestOptions::HTTP_ERRORS => false,
335
                ]
336
            );
337
338
        $body = json_decode($response->getBody()->getContents(), true);
339
340
        if ($response->getStatusCode() != 200 || $body['successful'] === false) {
341
            // error has happened
342
            $message = $body['errorData']['message'] ?? 'خطا در Revert تراکنش';
343
            throw new PurchaseFailedException($message);
344
        }
345
346
        return $body['response'];
347
    }
348
349
    public function status()
350
    {
351
        $data = [
352
            'paymentToken' => $this->invoice->getTransactionId(),
353
        ];
354
355
        $response = $this
356
            ->client
357
            ->get(
358
                $this->settings->apiPaymentUrl.self::STATUS_URL,
359
                [
360
                    RequestOptions::QUERY => $data,
361
                    RequestOptions::HEADERS => [
362
                        'Content-Type' => 'application/json',
363
                        'Authorization' => 'Bearer '.$this->oauthToken,
364
                    ],
365
                    RequestOptions::HTTP_ERRORS => false,
366
                ]
367
            );
368
369
        $body = json_decode($response->getBody()->getContents(), true);
370
371
        if ($response->getStatusCode() != 200 || $body['successful'] === false) {
372
            // error has happened
373
            $message = $body['errorData']['message'] ?? 'خطا در status تراکنش';
374
            throw new PurchaseFailedException($message);
375
        }
376
377
        return $body['response'];
378
    }
379
380
    public function cancel()
381
    {
382
        $data = [
383
            'paymentToken' => $this->invoice->getTransactionId(),
384
        ];
385
386
        $response = $this
387
            ->client
388
            ->post(
389
                $this->settings->apiPaymentUrl.self::CANCEL_URL,
390
                [
391
                    RequestOptions::BODY => json_encode($data),
392
                    RequestOptions::HEADERS => [
393
                        'Content-Type' => 'application/json',
394
                        'Authorization' => 'Bearer '.$this->oauthToken,
395
                    ],
396
                    RequestOptions::HTTP_ERRORS => false,
397
                ]
398
            );
399
400
        $body = json_decode($response->getBody()->getContents(), true);
401
402
        if ($response->getStatusCode() != 200 || $body['successful'] === false) {
403
            // error has happened
404
            $message = $body['errorData']['message'] ?? 'خطا در Cancel تراکنش';
405
            throw new PurchaseFailedException($message);
406
        }
407
408
        return $body['response'];
409
    }
410
411
    public function update()
412
    {
413
        $data = [
414
            'amount' => $this->normalizerAmount($this->invoice->getAmount()),
0 ignored issues
show
Bug introduced by
It seems like $this->invoice->getAmount() can also be of type double; however, parameter $amount of Shetabit\Multipay\Driver...Pay::normalizerAmount() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

414
            'amount' => $this->normalizerAmount(/** @scrutinizer ignore-type */ $this->invoice->getAmount()),
Loading history...
415
            'paymentMethodTypeDto' => 'INSTALLMENT',
416
            'paymentToken' => $this->invoice->getTransactionId(),
417
        ];
418
419
        if (!is_null($discountAmount = $this->invoice->getDetail('discountAmount'))) {
420
            $data['discountAmount'] = $this->normalizerAmount($discountAmount);
0 ignored issues
show
Bug introduced by
$discountAmount of type string is incompatible with the type integer expected by parameter $amount of Shetabit\Multipay\Driver...Pay::normalizerAmount(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

420
            $data['discountAmount'] = $this->normalizerAmount(/** @scrutinizer ignore-type */ $discountAmount);
Loading history...
421
        }
422
423
        if (!is_null($externalSourceAmount = $this->invoice->getDetail('externalSourceAmount'))) {
424
            $data['externalSourceAmount'] = $this->normalizerAmount($externalSourceAmount) ;
425
        }
426
427
        if (is_null($this->invoice->getDetail('cartList'))) {
428
            throw new PurchaseFailedException('"cartList" is required for this driver');
429
        }
430
431
        $data['cartList'] = $this->invoice->getDetail('cartList');
432
433
        $this->normalizerCartList($data);
434
435
        $response = $this
436
            ->client
437
            ->post(
438
                $this->settings->apiPaymentUrl.self::UPDATE_URL,
439
                [
440
                    RequestOptions::BODY => json_encode($data),
441
                    RequestOptions::HEADERS => [
442
                        'Content-Type' => 'application/json',
443
                        'Authorization' => 'Bearer '.$this->oauthToken,
444
                    ],
445
                    RequestOptions::HTTP_ERRORS => false,
446
                ]
447
            );
448
449
        $body = json_decode($response->getBody()->getContents(), true);
450
451
        if ($response->getStatusCode() != 200 || $body['successful'] === false) {
452
            // error has happened
453
            $message = $body['errorData']['message'] ?? 'خطا در بروزرسانی تراکنش رخ داده است.';
454
            throw new PurchaseFailedException($message);
455
        }
456
457
        return $body['response'];
458
    }
459
460
    private function getPaymentUrl(): string
461
    {
462
        return $this->paymentUrl;
463
    }
464
465
    private function setPaymentUrl(string $paymentUrl): void
466
    {
467
        $this->paymentUrl = $paymentUrl;
468
    }
469
}
470