Passed
Pull Request — master (#246)
by
unknown
04:43 queued 01:42
created

SnappPay::pay()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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

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

97
            $data['discountAmount'] = $this->normalizerAmount(/** @scrutinizer ignore-type */ $discountAmount);
Loading history...
98
        }
99
100
        if (!is_null($externalSourceAmount = $this->invoice->getDetail('externalSourceAmount'))) {
101
            $data['externalSourceAmount'] = $externalSourceAmount;
102
        }
103
104
        if (!is_null($cartList = $this->invoice->getDetail('cartList'))) {
105
            $data['cartList'] = $cartList;
106
        }
107
108
        $this->normalizerCartList($data);
109
110
        $response = $this
111
            ->client
112
            ->post(
113
                $this->settings->apiPaymentUrl.self::TOKEN_URL,
114
                [
115
                    RequestOptions::BODY => json_encode($data),
116
                    RequestOptions::HEADERS => [
117
                        'Content-Type' => 'application/json',
118
                        'Authorization' => 'Bearer '.$this->oauthToken,
119
                    ],
120
                    RequestOptions::HTTP_ERRORS => false,
121
                ]
122
            );
123
124
        $body = json_decode($response->getBody()->getContents(), true);
125
126
        if ($response->getStatusCode() != 200 || $body['successful'] === false) {
127
            // error has happened
128
            $message = $body['errorData']['message'] ?? 'خطا در هنگام درخواست برای پرداخت رخ داده است.';
129
            throw new PurchaseFailedException($message);
130
        }
131
132
        $this->invoice->transactionId($body['response']['paymentToken']);
133
        $this->setPaymentUrl($body['response']['paymentPageUrl']);
134
135
        // return the transaction's id
136
        return $this->invoice->getTransactionId();
137
    }
138
139
    public function pay(): RedirectionForm
140
    {
141
        return $this->redirectWithForm($this->getPaymentUrl(), [], 'GET');
142
    }
143
144
    /**
145
     * @throws InvalidPaymentException
146
     */
147
    public function verify(): ReceiptInterface
148
    {
149
        $paymentToken = $this->invoice->getTransactionId();
150
151
        $response = $this
0 ignored issues
show
Unused Code introduced by
The assignment to $response is dead and can be removed.
Loading history...
152
            ->client
153
            ->post(
154
                $this->settings->apiPaymentUrl.self::TOKEN_URL,
155
                [
156
                    RequestOptions::BODY => [
157
                        'paymentToken' => $paymentToken,
158
                    ],
159
                    RequestOptions::HEADERS => [
160
                        'Authorization' => 'Bearer '.$this->oauthToken,
161
                    ],
162
                    RequestOptions::HTTP_ERRORS => false,
163
                ]
164
            );
0 ignored issues
show
Bug Best Practice introduced by
In this branch, the function will implicitly return null which is incompatible with the type-hinted return Shetabit\Multipay\Contracts\ReceiptInterface. Consider adding a return statement or allowing null as return value.

For hinted functions/methods where all return statements with the correct type are only reachable via conditions, ?null? gets implicitly returned which may be incompatible with the hinted type. Let?s take a look at an example:

interface ReturnsInt {
    public function returnsIntHinted(): int;
}

class MyClass implements ReturnsInt {
    public function returnsIntHinted(): int
    {
        if (foo()) {
            return 123;
        }
        // here: null is implicitly returned
    }
}
Loading history...
165
    }
166
167
    /**
168
     * @throws PurchaseFailedException
169
     */
170
    protected function oauth()
171
    {
172
        $response = $this
173
            ->client
174
            ->post(
175
                $this->settings->apiPaymentUrl.self::OAUTH_URL,
176
                [
177
                    RequestOptions::HEADERS => [
178
                        'Authorization' => 'Basic '.base64_encode("{$this->settings->client_id}:{$this->settings->client_secret}"),
179
                    ],
180
                    RequestOptions::FORM_PARAMS => [
181
                        'grant_type' => 'password',
182
                        'scope' => 'online-merchant',
183
                        'username' => $this->settings->username,
184
                        'password' => $this->settings->password,
185
                    ],
186
                    RequestOptions::HTTP_ERRORS => false,
187
                ]
188
            );
189
190
        if ($response->getStatusCode() != 200) {
191
            throw new PurchaseFailedException('خطا در هنگام احراز هویت.');
192
        }
193
194
        $body = json_decode($response->getBody()->getContents(), true);
195
196
        return $body['access_token'];
197
    }
198
199
    /**
200
     * @throws PurchaseFailedException
201
     */
202
    public function eligible()
203
    {
204
        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...
205
            throw new PurchaseFailedException('"amount" is required for this method.');
206
        }
207
208
        $response = $this->client->get($this->settings->apiPaymentUrl.self::ELIGIBLE_URL, [
209
            RequestOptions::HEADERS => [
210
                'Authorization' => 'Bearer '.$this->oauthToken,
211
            ],
212
            RequestOptions::QUERY => [
213
                '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

213
                'amount' => $this->normalizerAmount(/** @scrutinizer ignore-type */ $amount),
Loading history...
214
            ],
215
        ]);
216
217
        $body = json_decode($response->getBody()->getContents(), true);
218
219
        if ($response->getStatusCode() != 200) {
220
            throw new InvalidPaymentException('', (int) $response->getStatusCode());
221
        }
222
223
        return $body;
224
    }
225
226
    private function normalizerAmount(int $amount): int
227
    {
228
        return $amount * ($this->settings->currency == 'T' ? 10 : 1);
229
    }
230
231
    private function normalizerCartList(array &$data): void
232
    {
233
        if (isset($data['cartList']['shippingAmount'])) {
234
            $data['cartList'] = [$data['cartList']];
235
        }
236
237
        foreach ($data['cartList'] as &$item) {
238
            if (isset($item['shippingAmount'])) {
239
                $item['shippingAmount'] = $this->normalizerAmount($item['shippingAmount']);
240
            }
241
242
            if (isset($item['taxAmount'])) {
243
                $item['taxAmount'] = $this->normalizerAmount($item['taxAmount']);
244
            }
245
246
            if (isset($item['totalAmount'])) {
247
                $item['totalAmount'] = $this->normalizerAmount($item['totalAmount']);
248
            }
249
250
            foreach ($item['cartItems'] as &$cartItem) {
251
                $cartItem['amount'] = $this->normalizerAmount($cartItem['amount']);
252
            }
253
        }
254
    }
255
256
    public function settle()
257
    {
258
    }
259
260
    public function revert()
261
    {
262
    }
263
264
    public function status()
265
    {
266
    }
267
268
    public function cancel()
269
    {
270
    }
271
272
    public function update()
273
    {
274
    }
275
276
    public function getPaymentUrl(): string
277
    {
278
        return $this->paymentUrl;
279
    }
280
281
    public function setPaymentUrl(string $paymentUrl): void
282
    {
283
        $this->paymentUrl = $paymentUrl;
284
    }
285
}
286