Issues (52)

src/Drivers/Azki/Azki.php (2 issues)

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
The assignment to $secret_iv is dead and can be removed.
Loading history...
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 ignore-type  annotation

157
        return bin2hex(/** @scrutinizer ignore-type */ $digest);
Loading history...
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