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
![]() |
|||||
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
$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
![]() |
|||||
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
|
|||||
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
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
![]() |
|||||
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
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
![]() |
|||||
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
$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
![]() |
|||||
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 |