Passed
Push — master ( 76d0c6...b8ef24 )
by mahdi
02:59
created

SnappPay::eligible()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 22
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 11
c 1
b 0
f 0
nc 3
nop 0
dl 0
loc 22
rs 9.9
1
<?php
2
3
4
namespace Shetabit\Multipay\Drivers\SnappPay;
5
6
use GuzzleHttp\Client;
7
use GuzzleHttp\RequestOptions;
8
use Shetabit\Multipay\Abstracts\Driver;
9
use Shetabit\Multipay\Contracts\ReceiptInterface;
10
use Shetabit\Multipay\Exceptions\InvalidPaymentException;
11
use Shetabit\Multipay\Exceptions\PurchaseFailedException;
12
use Shetabit\Multipay\Invoice;
13
use Shetabit\Multipay\Receipt;
14
use Shetabit\Multipay\RedirectionForm;
15
16
class SnappPay extends Driver
17
{
18
    const VERSION = '1.8';
19
    const RELEASE_DATE = '2023-01-08';
20
21
    const OAUTH_URL = '/api/online/v1/oauth/token';
22
    const ELIGIBLE_URL = '/api/online/offer/v1/eligible';
23
    const TOKEN_URL = '/api/online/payment/v1/token';
24
    const VERIFY_URL = '/api/online/payment/v1/verify';
25
    const SETTLE_URL = '/api/online/payment/v1/settle';
26
    const REVERT_URL = '/api/online/payment/v1/revert';
27
    const STATUS_URL = '/api/online/payment/v1/status';
28
    const CANCEL_URL = '/api/online/payment/v1/cancel';
29
    const UPDATE_URL = '/api/online/payment/v1/update';
30
31
    /**
32
     * SnappPay Client.
33
     *
34
     * @var Client
35
     */
36
    protected $client;
37
38
    /**
39
     * Invoice
40
     *
41
     * @var Invoice
42
     */
43
    protected $invoice;
44
45
    /**
46
     * Driver settings
47
     *
48
     * @var object
49
     */
50
    protected $settings;
51
    /**
52
     * SnappPay Oauth Data
53
     *
54
     * @var string
55
     */
56
    protected $oauthToken;
57
58
    /**
59
     * SnappPay payment url
60
     *
61
     * @var string
62
     */
63
    protected $paymentUrl;
64
65
    /**
66
     * SnappPay constructor.
67
     * Construct the class with the relevant settings.
68
     */
69
    public function __construct(Invoice $invoice, $settings)
70
    {
71
        $this->invoice($invoice);
72
        $this->settings = (object) $settings;
73
        $this->client = new Client();
74
        $this->oauthToken = $this->oauth();
75
    }
76
77
    /**
78
     * @throws PurchaseFailedException
79
     */
80
    public function purchase(): string
81
    {
82
        $phone = $this->invoice->getDetail('phone')
83
            ?? $this->invoice->getDetail('cellphone')
84
            ?? $this->invoice->getDetail('mobile');
85
86
        // convert to format +98 901 XXX XXXX
87
        $phone = preg_replace('/^0/', '+98', $phone);
88
89
        $data = [
90
            '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

90
            'amount' => $this->normalizerAmount(/** @scrutinizer ignore-type */ $this->invoice->getAmount()),
Loading history...
91
            'mobile' => $phone,
92
            'paymentMethodTypeDto' => 'INSTALLMENT',
93
            'transactionId' => $this->invoice->getUuid(),
94
            'returnURL' => $this->settings->callbackUrl,
95
        ];
96
97
        if (!is_null($discountAmount = $this->invoice->getDetail('discountAmount'))) {
98
            $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

98
            $data['discountAmount'] = $this->normalizerAmount(/** @scrutinizer ignore-type */ $discountAmount);
Loading history...
99
        }
100
101
        if (!is_null($externalSourceAmount = $this->invoice->getDetail('externalSourceAmount'))) {
102
            $data['externalSourceAmount'] = $externalSourceAmount;
103
        }
104
105
        if (is_null($this->invoice->getDetail('cartList'))) {
106
            throw new PurchaseFailedException('"cartList" is required for this driver');
107
        }
108
109
        $data['cartList'] = $this->invoice->getDetail('cartList');
110
111
        $this->normalizerCartList($data);
112
113
        $response = $this
114
            ->client
115
            ->post(
116
                $this->settings->apiPaymentUrl.self::TOKEN_URL,
117
                [
118
                    RequestOptions::BODY => json_encode($data),
119
                    RequestOptions::HEADERS => [
120
                        'Content-Type' => 'application/json',
121
                        'Authorization' => 'Bearer '.$this->oauthToken,
122
                    ],
123
                    RequestOptions::HTTP_ERRORS => false,
124
                ]
125
            );
126
127
        $body = json_decode($response->getBody()->getContents(), true);
128
129
        if ($response->getStatusCode() != 200 || $body['successful'] === false) {
130
            // error has happened
131
            $message = $body['errorData']['message'] ?? 'خطا در هنگام درخواست برای پرداخت رخ داده است.';
132
            throw new PurchaseFailedException($message);
133
        }
134
135
        $this->invoice->transactionId($body['response']['paymentToken']);
136
        $this->setPaymentUrl($body['response']['paymentPageUrl']);
137
138
        // return the transaction's id
139
        return $this->invoice->getTransactionId();
140
    }
141
142
    public function pay(): RedirectionForm
143
    {
144
        parse_str(parse_url($this->getPaymentUrl(), PHP_URL_QUERY), $formData);
145
146
        return $this->redirectWithForm($this->getPaymentUrl(), $formData, 'GET');
147
    }
148
149
    /**
150
     * @throws InvalidPaymentException
151
     */
152
    public function verify(): ReceiptInterface
153
    {
154
        $data = [
155
            'paymentToken' => $this->invoice->getTransactionId(),
156
        ];
157
158
        $response = $this
159
            ->client
160
            ->post(
161
                $this->settings->apiPaymentUrl.self::VERIFY_URL,
162
                [
163
                    RequestOptions::BODY => json_encode($data),
164
                    RequestOptions::HEADERS => [
165
                        'Content-Type' => 'application/json',
166
                        'Authorization' => 'Bearer '.$this->oauthToken,
167
                    ],
168
                    RequestOptions::HTTP_ERRORS => false,
169
                ]
170
            );
171
172
        $body = json_decode($response->getBody()->getContents(), true);
173
174
        if ($response->getStatusCode() != 200 || $body['successful'] === false) {
175
            // error has happened
176
            $message = $body['errorData']['message'] ?? 'خطا در هنگام تایید تراکنش';
177
            throw new PurchaseFailedException($message);
178
        }
179
180
        return (new Receipt('digipay', $body['response']['transactionId']))->detail($body['response']);
181
    }
182
183
    /**
184
     * @throws PurchaseFailedException
185
     */
186
    protected function oauth()
187
    {
188
        $response = $this
189
            ->client
190
            ->post(
191
                $this->settings->apiPaymentUrl.self::OAUTH_URL,
192
                [
193
                    RequestOptions::HEADERS => [
194
                        'Authorization' => 'Basic '.base64_encode("{$this->settings->client_id}:{$this->settings->client_secret}"),
195
                    ],
196
                    RequestOptions::FORM_PARAMS => [
197
                        'grant_type' => 'password',
198
                        'scope' => 'online-merchant',
199
                        'username' => $this->settings->username,
200
                        'password' => $this->settings->password,
201
                    ],
202
                    RequestOptions::HTTP_ERRORS => false,
203
                ]
204
            );
205
206
        if ($response->getStatusCode() != 200) {
207
            throw new PurchaseFailedException('خطا در هنگام احراز هویت.');
208
        }
209
210
        $body = json_decode($response->getBody()->getContents(), true);
211
212
        return $body['access_token'];
213
    }
214
215
    /**
216
     * @throws PurchaseFailedException
217
     */
218
    public function eligible()
219
    {
220
        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...
221
            throw new PurchaseFailedException('"amount" is required for this method.');
222
        }
223
224
        $response = $this->client->get($this->settings->apiPaymentUrl.self::ELIGIBLE_URL, [
225
            RequestOptions::HEADERS => [
226
                'Authorization' => 'Bearer '.$this->oauthToken,
227
            ],
228
            RequestOptions::QUERY => [
229
                '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

229
                'amount' => $this->normalizerAmount(/** @scrutinizer ignore-type */ $amount),
Loading history...
230
            ],
231
        ]);
232
233
        $body = json_decode($response->getBody()->getContents(), true);
234
235
        if ($response->getStatusCode() != 200) {
236
            throw new InvalidPaymentException('', (int) $response->getStatusCode());
237
        }
238
239
        return $body;
240
    }
241
242
    private function normalizerAmount(int $amount): int
243
    {
244
        return $amount * ($this->settings->currency == 'T' ? 10 : 1);
245
    }
246
247
    private function normalizerCartList(array &$data): void
248
    {
249
        if (isset($data['cartList']['shippingAmount'])) {
250
            $data['cartList'] = [$data['cartList']];
251
        }
252
253
        foreach ($data['cartList'] as &$item) {
254
            if (isset($item['shippingAmount'])) {
255
                $item['shippingAmount'] = $this->normalizerAmount($item['shippingAmount']);
256
            }
257
258
            if (isset($item['taxAmount'])) {
259
                $item['taxAmount'] = $this->normalizerAmount($item['taxAmount']);
260
            }
261
262
            if (isset($item['totalAmount'])) {
263
                $item['totalAmount'] = $this->normalizerAmount($item['totalAmount']);
264
            }
265
266
            foreach ($item['cartItems'] as &$cartItem) {
267
                $cartItem['amount'] = $this->normalizerAmount($cartItem['amount']);
268
            }
269
        }
270
    }
271
272
    public function settle(): array
273
    {
274
        $data = [
275
            'paymentToken' => $this->invoice->getTransactionId(),
276
        ];
277
278
        $response = $this
279
            ->client
280
            ->post(
281
                $this->settings->apiPaymentUrl.self::SETTLE_URL,
282
                [
283
                    RequestOptions::BODY => json_encode($data),
284
                    RequestOptions::HEADERS => [
285
                        'Content-Type' => 'application/json',
286
                        'Authorization' => 'Bearer '.$this->oauthToken,
287
                    ],
288
                    RequestOptions::HTTP_ERRORS => false,
289
                ]
290
            );
291
292
        $body = json_decode($response->getBody()->getContents(), true);
293
294
        if ($response->getStatusCode() != 200 || $body['successful'] === false) {
295
            // error has happened
296
            $message = $body['errorData']['message'] ?? 'خطا در Settle تراکنش';
297
            throw new PurchaseFailedException($message);
298
        }
299
300
        return $body['response'];
301
    }
302
303
    public function revert()
304
    {
305
        $data = [
306
            'paymentToken' => $this->invoice->getTransactionId(),
307
        ];
308
309
        $response = $this
310
            ->client
311
            ->post(
312
                $this->settings->apiPaymentUrl.self::REVERT_URL,
313
                [
314
                    RequestOptions::BODY => json_encode($data),
315
                    RequestOptions::HEADERS => [
316
                        'Content-Type' => 'application/json',
317
                        'Authorization' => 'Bearer '.$this->oauthToken,
318
                    ],
319
                    RequestOptions::HTTP_ERRORS => false,
320
                ]
321
            );
322
323
        $body = json_decode($response->getBody()->getContents(), true);
324
325
        if ($response->getStatusCode() != 200 || $body['successful'] === false) {
326
            // error has happened
327
            $message = $body['errorData']['message'] ?? 'خطا در Revert تراکنش';
328
            throw new PurchaseFailedException($message);
329
        }
330
331
        return $body['response'];
332
    }
333
334
    public function status()
335
    {
336
        $data = [
337
            'paymentToken' => $this->invoice->getTransactionId(),
338
        ];
339
340
        $response = $this
341
            ->client
342
            ->get(
343
                $this->settings->apiPaymentUrl.self::STATUS_URL,
344
                [
345
                    RequestOptions::QUERY => $data,
346
                    RequestOptions::HEADERS => [
347
                        'Content-Type' => 'application/json',
348
                        'Authorization' => 'Bearer '.$this->oauthToken,
349
                    ],
350
                    RequestOptions::HTTP_ERRORS => false,
351
                ]
352
            );
353
354
        $body = json_decode($response->getBody()->getContents(), true);
355
356
        if ($response->getStatusCode() != 200 || $body['successful'] === false) {
357
            // error has happened
358
            $message = $body['errorData']['message'] ?? 'خطا در status تراکنش';
359
            throw new PurchaseFailedException($message);
360
        }
361
362
        return $body['response'];
363
    }
364
365
    public function cancel()
366
    {
367
        $data = [
368
            'paymentToken' => $this->invoice->getTransactionId(),
369
        ];
370
371
        $response = $this
372
            ->client
373
            ->post(
374
                $this->settings->apiPaymentUrl.self::CANCEL_URL,
375
                [
376
                    RequestOptions::BODY => json_encode($data),
377
                    RequestOptions::HEADERS => [
378
                        'Content-Type' => 'application/json',
379
                        'Authorization' => 'Bearer '.$this->oauthToken,
380
                    ],
381
                    RequestOptions::HTTP_ERRORS => false,
382
                ]
383
            );
384
385
        $body = json_decode($response->getBody()->getContents(), true);
386
387
        if ($response->getStatusCode() != 200 || $body['successful'] === false) {
388
            // error has happened
389
            $message = $body['errorData']['message'] ?? 'خطا در Cancel تراکنش';
390
            throw new PurchaseFailedException($message);
391
        }
392
393
        return $body['response'];
394
    }
395
396
    public function update()
397
    {
398
        $data = [
399
            '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

399
            'amount' => $this->normalizerAmount(/** @scrutinizer ignore-type */ $this->invoice->getAmount()),
Loading history...
400
            'paymentMethodTypeDto' => 'INSTALLMENT',
401
            'paymentToken' => $this->invoice->getTransactionId(),
402
        ];
403
404
        if (!is_null($discountAmount = $this->invoice->getDetail('discountAmount'))) {
405
            $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

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