Passed
Pull Request — master (#162)
by
unknown
02:37
created

Azki   A

Complexity

Total Complexity 25

Size/Duplication

Total Lines 327
Duplicated Lines 0 %

Importance

Changes 2
Bugs 1 Features 0
Metric Value
eloc 143
c 2
b 1
f 0
dl 0
loc 327
rs 10
wmc 25

14 Methods

Rating   Name   Duplication   Size   Complexity  
A getPaymentUrl() 0 3 1
A setPaymentUrl() 0 3 1
A purchaseFailed() 0 28 2
A verify() 0 11 2
A ApiCall() 0 25 4
B purchase() 0 45 6
A makeSignature() 0 13 1
A __construct() 0 6 1
A verifyFailed() 0 18 2
A getPaymentStatus() 0 15 1
A convertAmountItems() 0 31 1
A createReceipt() 0 5 1
A pay() 0 9 1
A VerifyTransaction() 0 15 1
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 (count($this->getItems()) == 0) {
0 ignored issues
show
Bug introduced by
The method getItems() does not exist on Shetabit\Multipay\Drivers\Azki\Azki. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

80
        if (count($this->/** @scrutinizer ignore-call */ getItems()) == 0) {

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
81
            throw new PurchaseFailedException('Items is required');
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() * 10, // convert toman 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
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
Bug introduced by
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(function ($item) {
183
            $item['amount'] *= 10;  // convert toman to rial
184
            return $item;
185
        },
186
            $this->invoice->getDetails()['items'] ?? []
187
        );
188
189
        $this->invoice->detail('items', $new_items);
190
        return $new_items;
191
    }
192
193
    /**
194
     * @param array $data
195
     * @param       $signature
196
     * @param       $url
197
     * @return mixed
198
     */
199
    public function ApiCall(array $data, $signature, $url, $request_method = 'POST')
200
    {
201
        $response = $this
202
            ->client
203
            ->request(
204
                $request_method,
205
                $url,
206
                [
207
                    "json"        => $data,
208
                    "headers"     => [
209
                        'Content-Type' => 'application/json',
210
                        'Signature'    => $signature,
211
                        'MerchantId'   => $this->settings->merchantId,
212
                    ],
213
                    "http_errors" => false,
214
                ]
215
            );
216
217
        $response_array = json_decode($response->getBody()->getContents(), true);
218
219
220
        if (($response->getStatusCode() === null or $response->getStatusCode() != 200) || $response_array['rsCode'] != self::SUCCESSFUL) {
221
            $this->purchaseFailed($response_array['rsCode']);
222
        } else {
223
            return $response_array['result'];
224
        }
225
    }
226
227
    /**
228
     * Trigger an exception
229
     *
230
     * @param $status
231
     *
232
     * @throws PurchaseFailedException
233
     */
234
    protected function purchaseFailed($status)
235
    {
236
        $translations = [
237
            "1"  => "Internal Server Error",
238
            "2"  => "Resource Not Found",
239
            "4"  => "Malformed Data",
240
            "5"  => "Data Not Found",
241
            "15" => "Access Denied",
242
            "16" => "Transaction already reversed",
243
            "17" => "Ticket Expired",
244
            "18" => "Signature Invalid",
245
            "19" => "Ticket unpayable",
246
            "20" => "Ticket customer mismatch",
247
            "21" => "Insufficient Credit",
248
            "28" => "Unverifiable ticket due to status",
249
            "32" => "Invalid Invoice Data",
250
            "33" => "Contract is not started",
251
            "34" => "Contract is expired",
252
            "44" => "Validation exception",
253
            "51" => "Request data is not valid",
254
            "59" => "Transaction not reversible",
255
            "60" => "Transaction must be in verified state",
256
        ];
257
258
        if (array_key_exists($status, $translations)) {
259
            throw new PurchaseFailedException($translations[$status]);
260
        } else {
261
            throw new PurchaseFailedException('خطای ناشناخته ای رخ داده است.');
262
        }
263
    }
264
265
    private function getPaymentStatus()
266
    {
267
        $sub_url = self::subUrls['paymentStatus'];
268
        $url     = $this->settings->apiPaymentUrl . $sub_url;
269
270
        $signature = $this->makeSignature(
271
            $sub_url,
272
            'POST'
273
        );
274
275
        $data = [
276
            "ticket_id" => $this->invoice->getTransactionId(),
277
        ];
278
279
        return $this->ApiCall($data, $signature, $url)['status'];
280
    }
281
282
283
    /**
284
     * Trigger an exception
285
     *
286
     * @param $status
287
     *
288
     * @throws PurchaseFailedException
289
     */
290
    protected function verifyFailed($status)
291
    {
292
        $translations = [
293
            "1" => "Created",
294
            "2" => "Verified",
295
            "3" => "Reversed",
296
            "4" => "Failed",
297
            "5" => "Canceled",
298
            "6" => "Settled",
299
            "7" => "Expired",
300
            "8" => "Done",
301
            "9" => "Settle Queue",
302
        ];
303
304
        if (array_key_exists($status, $translations)) {
305
            throw new PurchaseFailedException("تراکنش در وضعیت " . $translations[$status] . " است.");
306
        } else {
307
            throw new PurchaseFailedException('خطای ناشناخته ای رخ داده است.');
308
        }
309
    }
310
311
    /**
312
     * Generate the payment's receipt
313
     *
314
     * @param $referenceId
315
     *
316
     * @return Receipt
317
     */
318
    private function createReceipt($referenceId): Receipt
319
    {
320
        $receipt = new Receipt('azki', $referenceId);
321
322
        return $receipt;
323
    }
324
325
    private function VerifyTransaction()
326
    {
327
        $sub_url = self::subUrls['verify'];
328
        $url     = $this->settings->apiPaymentUrl . $sub_url;
329
330
        $signature = $this->makeSignature(
331
            $sub_url,
332
            'POST'
333
        );
334
335
        $data = [
336
            "ticket_id" => $this->invoice->getTransactionId(),
337
        ];
338
339
        return $this->ApiCall($data, $signature, $url);
340
    }
341
}
342