1 | <?php |
||||
2 | |||||
3 | namespace Shetabit\Multipay\Drivers\Azki; |
||||
4 | |||||
5 | use GuzzleHttp\Client; |
||||
6 | use Shetabit\Multipay\Abstracts\Driver; |
||||
7 | use Shetabit\Multipay\Contracts\ReceiptInterface; |
||||
8 | use Shetabit\Multipay\Exceptions\PurchaseFailedException; |
||||
9 | use Shetabit\Multipay\Invoice; |
||||
10 | use Shetabit\Multipay\Receipt; |
||||
11 | use Shetabit\Multipay\RedirectionForm; |
||||
12 | |||||
13 | class Azki extends Driver |
||||
14 | { |
||||
15 | const STATUS_DONE = 8; |
||||
16 | |||||
17 | const SUCCESSFUL = 0; |
||||
18 | |||||
19 | const subUrls = [ |
||||
20 | 'purchase' => '/payment/purchase', |
||||
21 | 'paymentStatus' => '/payment/status', |
||||
22 | 'verify' => '/payment/verify', |
||||
23 | ]; |
||||
24 | /** |
||||
25 | * Azki Client. |
||||
26 | * |
||||
27 | * @var object |
||||
28 | */ |
||||
29 | protected $client; |
||||
30 | |||||
31 | /** |
||||
32 | * Invoice |
||||
33 | * |
||||
34 | * @var Invoice |
||||
35 | */ |
||||
36 | protected $invoice; |
||||
37 | |||||
38 | /** |
||||
39 | * Driver settings |
||||
40 | * |
||||
41 | * @var object |
||||
42 | */ |
||||
43 | protected $settings; |
||||
44 | |||||
45 | protected $paymentUrl; |
||||
46 | |||||
47 | /** |
||||
48 | * @return string |
||||
49 | */ |
||||
50 | public function getPaymentUrl(): string |
||||
51 | { |
||||
52 | return $this->paymentUrl; |
||||
53 | } |
||||
54 | |||||
55 | /** |
||||
56 | * @param mixed $paymentUrl |
||||
57 | */ |
||||
58 | public function setPaymentUrl($paymentUrl): void |
||||
59 | { |
||||
60 | $this->paymentUrl = $paymentUrl; |
||||
61 | } |
||||
62 | |||||
63 | |||||
64 | public function __construct(Invoice $invoice, $settings) |
||||
65 | { |
||||
66 | $this->invoice($invoice); |
||||
67 | $this->settings = (object)$settings; |
||||
68 | $this->client = new Client(); |
||||
69 | $this->convertAmountItems(); |
||||
70 | } |
||||
71 | |||||
72 | public function purchase() |
||||
73 | { |
||||
74 | $details = $this->invoice->getDetails(); |
||||
75 | $order_id = $this->invoice->getUuid(); |
||||
76 | |||||
77 | if (empty($details['phone']) && empty($details['mobile'])) { |
||||
78 | throw new PurchaseFailedException('Phone number is required'); |
||||
79 | } |
||||
80 | if (!isset($details['items']) || count($details['items']) == 0) { |
||||
81 | throw new PurchaseFailedException('Items is required for this driver'); |
||||
82 | } |
||||
83 | |||||
84 | $merchant_id = $this->settings->merchantId; |
||||
85 | $callback = $this->settings->callbackUrl; |
||||
86 | $fallback = |
||||
87 | $this->settings->fallbackUrl != 'http://yoursite.com/path/to' && $this->settings->fallbackUrl == '' |
||||
88 | ? $this->settings->fallbackUrl |
||||
89 | : $callback; |
||||
90 | $sub_url = self::subUrls['purchase']; |
||||
91 | $url = $this->settings->apiPaymentUrl . $sub_url; |
||||
92 | |||||
93 | $signature = $this->makeSignature( |
||||
94 | $sub_url, |
||||
95 | 'POST' |
||||
96 | ); |
||||
97 | |||||
98 | $data = [ |
||||
99 | "amount" => $this->invoice->getAmount() * ($this->settings->currency == 'T' ? 10 : 1), // convert to rial |
||||
100 | "redirect_uri" => $callback, |
||||
101 | "fallback_uri" => $fallback, |
||||
102 | "provider_id" => $order_id, |
||||
103 | "mobile_number" => $details['mobile'] ?? $details['phone'] ?? null, |
||||
104 | "merchant_id" => $merchant_id, |
||||
105 | "description" => $details['description'] ?? $this->settings->description, |
||||
106 | "items" => $details['items'], |
||||
107 | ]; |
||||
108 | |||||
109 | $response = $this->ApiCall($data, $signature, $url); |
||||
110 | |||||
111 | // set transaction's id |
||||
112 | $this->invoice->transactionId($response['ticket_id']); |
||||
113 | $this->setPaymentUrl($response['payment_uri']); |
||||
114 | |||||
115 | // return the transaction's id |
||||
116 | return $this->invoice->getTransactionId(); |
||||
117 | } |
||||
118 | |||||
119 | public function pay(): RedirectionForm |
||||
120 | { |
||||
121 | $url = $this->getPaymentUrl(); |
||||
122 | return $this->redirectWithForm( |
||||
123 | $url, |
||||
124 | [ |
||||
125 | 'ticketId' => $this->invoice->getTransactionId(), |
||||
126 | ], |
||||
127 | 'GET' |
||||
128 | ); |
||||
129 | } |
||||
130 | |||||
131 | public function verify(): ReceiptInterface |
||||
132 | { |
||||
133 | $paymentStatus = $this->getPaymentStatus(); |
||||
134 | |||||
135 | if ($paymentStatus != self::STATUS_DONE) { |
||||
136 | $this->verifyFailed($paymentStatus); |
||||
137 | } |
||||
138 | |||||
139 | $this->VerifyTransaction(); |
||||
140 | |||||
141 | return $this->createReceipt($this->invoice->getTransactionId()); |
||||
142 | } |
||||
143 | |||||
144 | |||||
145 | private function makeSignature($sub_url, $request_method = 'POST') |
||||
146 | { |
||||
147 | $time = time(); |
||||
148 | $key = $this->settings->key; |
||||
149 | |||||
150 | $plain_signature = "{$sub_url}#{$time}#{$request_method}#{$key}"; |
||||
151 | $encrypt_method = "AES-256-CBC"; |
||||
152 | $secret_key = hex2bin($key); |
||||
153 | $secret_iv = str_repeat(0, 16); |
||||
0 ignored issues
–
show
Unused Code
introduced
by
![]() |
|||||
154 | |||||
155 | $digest = @openssl_encrypt($plain_signature, $encrypt_method, $secret_key, OPENSSL_RAW_DATA); |
||||
156 | |||||
157 | return bin2hex($digest); |
||||
0 ignored issues
–
show
It seems like
$digest can also be of type false ; however, parameter $string of bin2hex() does only seem to accept string , 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
![]() |
|||||
158 | } |
||||
159 | |||||
160 | private function convertAmountItems() |
||||
161 | { |
||||
162 | /** |
||||
163 | * example data |
||||
164 | * |
||||
165 | * $items = [ |
||||
166 | * [ |
||||
167 | * "name" => "Item 1", |
||||
168 | * "count" => "string", |
||||
169 | * "amount" => 0, |
||||
170 | * "url" => "http://shop.com/items/1", |
||||
171 | * ], |
||||
172 | * [ |
||||
173 | * "name" => "Item 2", |
||||
174 | * "count" => 5, |
||||
175 | * "amount" => 20000, |
||||
176 | * "url" => "http://google.com/items/2", |
||||
177 | * ], |
||||
178 | * ]; |
||||
179 | * |
||||
180 | */ |
||||
181 | |||||
182 | $new_items = array_map( |
||||
183 | function ($item) { |
||||
184 | $item['amount'] *= ($this->settings->currency == 'T' ? 10 : 1); // convert to rial |
||||
185 | return $item; |
||||
186 | }, |
||||
187 | $this->invoice->getDetails()['items'] ?? [] |
||||
188 | ); |
||||
189 | |||||
190 | $this->invoice->detail('items', $new_items); |
||||
191 | return $new_items; |
||||
192 | } |
||||
193 | |||||
194 | /** |
||||
195 | * @param array $data |
||||
196 | * @param $signature |
||||
197 | * @param $url |
||||
198 | * @return mixed |
||||
199 | */ |
||||
200 | public function ApiCall(array $data, $signature, $url, $request_method = 'POST') |
||||
201 | { |
||||
202 | $response = $this |
||||
203 | ->client |
||||
204 | ->request( |
||||
205 | $request_method, |
||||
206 | $url, |
||||
207 | [ |
||||
208 | "json" => $data, |
||||
209 | "headers" => [ |
||||
210 | 'Content-Type' => 'application/json', |
||||
211 | 'Signature' => $signature, |
||||
212 | 'MerchantId' => $this->settings->merchantId, |
||||
213 | ], |
||||
214 | "http_errors" => false, |
||||
215 | ] |
||||
216 | ); |
||||
217 | |||||
218 | $response_array = json_decode($response->getBody()->getContents(), true); |
||||
219 | |||||
220 | |||||
221 | if (($response->getStatusCode() === null or $response->getStatusCode() != 200) || $response_array['rsCode'] != self::SUCCESSFUL) { |
||||
222 | $this->purchaseFailed($response_array['rsCode']); |
||||
223 | } else { |
||||
224 | return $response_array['result']; |
||||
225 | } |
||||
226 | } |
||||
227 | |||||
228 | /** |
||||
229 | * Trigger an exception |
||||
230 | * |
||||
231 | * @param $status |
||||
232 | * |
||||
233 | * @throws PurchaseFailedException |
||||
234 | */ |
||||
235 | protected function purchaseFailed($status) |
||||
236 | { |
||||
237 | $translations = [ |
||||
238 | "1" => "Internal Server Error", |
||||
239 | "2" => "Resource Not Found", |
||||
240 | "4" => "Malformed Data", |
||||
241 | "5" => "Data Not Found", |
||||
242 | "15" => "Access Denied", |
||||
243 | "16" => "Transaction already reversed", |
||||
244 | "17" => "Ticket Expired", |
||||
245 | "18" => "Signature Invalid", |
||||
246 | "19" => "Ticket unpayable", |
||||
247 | "20" => "Ticket customer mismatch", |
||||
248 | "21" => "Insufficient Credit", |
||||
249 | "28" => "Unverifiable ticket due to status", |
||||
250 | "32" => "Invalid Invoice Data", |
||||
251 | "33" => "Contract is not started", |
||||
252 | "34" => "Contract is expired", |
||||
253 | "44" => "Validation exception", |
||||
254 | "51" => "Request data is not valid", |
||||
255 | "59" => "Transaction not reversible", |
||||
256 | "60" => "Transaction must be in verified state", |
||||
257 | ]; |
||||
258 | |||||
259 | if (array_key_exists($status, $translations)) { |
||||
260 | throw new PurchaseFailedException($translations[$status]); |
||||
261 | } else { |
||||
262 | throw new PurchaseFailedException('خطای ناشناخته ای رخ داده است.'); |
||||
263 | } |
||||
264 | } |
||||
265 | |||||
266 | private function getPaymentStatus() |
||||
267 | { |
||||
268 | $sub_url = self::subUrls['paymentStatus']; |
||||
269 | $url = $this->settings->apiPaymentUrl . $sub_url; |
||||
270 | |||||
271 | $signature = $this->makeSignature( |
||||
272 | $sub_url, |
||||
273 | 'POST' |
||||
274 | ); |
||||
275 | |||||
276 | $data = [ |
||||
277 | "ticket_id" => $this->invoice->getTransactionId(), |
||||
278 | ]; |
||||
279 | |||||
280 | return $this->ApiCall($data, $signature, $url)['status']; |
||||
281 | } |
||||
282 | |||||
283 | |||||
284 | /** |
||||
285 | * Trigger an exception |
||||
286 | * |
||||
287 | * @param $status |
||||
288 | * |
||||
289 | * @throws PurchaseFailedException |
||||
290 | */ |
||||
291 | protected function verifyFailed($status) |
||||
292 | { |
||||
293 | $translations = [ |
||||
294 | "1" => "Created", |
||||
295 | "2" => "Verified", |
||||
296 | "3" => "Reversed", |
||||
297 | "4" => "Failed", |
||||
298 | "5" => "Canceled", |
||||
299 | "6" => "Settled", |
||||
300 | "7" => "Expired", |
||||
301 | "8" => "Done", |
||||
302 | "9" => "Settle Queue", |
||||
303 | ]; |
||||
304 | |||||
305 | if (array_key_exists($status, $translations)) { |
||||
306 | throw new PurchaseFailedException("تراکنش در وضعیت " . $translations[$status] . " است."); |
||||
307 | } else { |
||||
308 | throw new PurchaseFailedException('خطای ناشناخته ای رخ داده است.'); |
||||
309 | } |
||||
310 | } |
||||
311 | |||||
312 | /** |
||||
313 | * Generate the payment's receipt |
||||
314 | * |
||||
315 | * @param $referenceId |
||||
316 | * |
||||
317 | * @return Receipt |
||||
318 | */ |
||||
319 | private function createReceipt($referenceId): Receipt |
||||
320 | { |
||||
321 | $receipt = new Receipt('azki', $referenceId); |
||||
322 | |||||
323 | return $receipt; |
||||
324 | } |
||||
325 | |||||
326 | private function VerifyTransaction() |
||||
327 | { |
||||
328 | $sub_url = self::subUrls['verify']; |
||||
329 | $url = $this->settings->apiPaymentUrl . $sub_url; |
||||
330 | |||||
331 | $signature = $this->makeSignature( |
||||
332 | $sub_url, |
||||
333 | 'POST' |
||||
334 | ); |
||||
335 | |||||
336 | $data = [ |
||||
337 | "ticket_id" => $this->invoice->getTransactionId(), |
||||
338 | ]; |
||||
339 | |||||
340 | return $this->ApiCall($data, $signature, $url); |
||||
341 | } |
||||
342 | } |
||||
343 |