Passed
Push — master ( 51cefb...78ea54 )
by Action
02:48
created

UnitPay::validateSearchOrderRequiredAttributes()   B

Complexity

Conditions 6
Paths 8

Size

Total Lines 31
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 31
rs 8.439
c 0
b 0
f 0
cc 6
eloc 15
nc 8
nop 2
1
<?php
2
3
namespace ActionM\UnitPay;
4
5
use Illuminate\Http\Request;
6
use ActionM\UnitPay\Events\UnitPayEvent;
7
use Illuminate\Support\Facades\Validator;
8
use ActionM\UnitPay\Exceptions\InvalidConfiguration;
9
10
class UnitPay
11
{
12
13
    public function __construct()
14
    {
15
    }
16
17
    /**
18
     * Allow access, if the ip address is in the whitelist.
19
     * @param $ip
20
     * @return bool
21
     */
22
    public function allowIP($ip)
23
    {
24
        // Allow local ip
25
        if ($ip == '127.0.0.1') {
26
            return true;
27
        }
28
29
        return in_array($ip, config('unitpay.allowed_ips'));
30
    }
31
32
    /**
33
     * Return JSON error message
34
     * @param $message
35
     * @return mixed
36
     */
37
    public function responseError($message)
38
    {
39
        $result['error']['message'] = $message;
0 ignored issues
show
Coding Style Comprehensibility introduced by
$result was never initialized. Although not strictly required by PHP, it is generally a good practice to add $result = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
40
41
        return $result;
42
    }
43
44
    /**
45
     * Return JSON success message
46
     * @param $message
47
     * @return mixed
48
     */
49
    public function responseOK($message)
50
    {
51
        $result['result']['message'] = $message;
0 ignored issues
show
Coding Style Comprehensibility introduced by
$result was never initialized. Although not strictly required by PHP, it is generally a good practice to add $result = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
52
53
        return $result;
54
    }
55
56
    /**
57
     * Fill event details to pass the title and request params as array.
58
     * @param $event_type
59
     * @param $event_title
60
     * @param Request $request
61
     */
62
    public function eventFillAndSend($event_type, $event_title, Request $request)
63
    {
64
        $event_details = [
65
            'title' => 'UnitPay: '.$event_title,
66
            'ip' => $request->ip(),
67
            'request' => $request->all(),
68
        ];
69
70
        event(
71
            new UnitPayEvent($event_type, $event_details)
72
        );
73
    }
74
75
    /**
76
     * Return hash for the order form params.
77
     * @param $account
78
     * @param $currency
79
     * @param $desc
80
     * @param $sum
81
     * @param $secretKey
82
     * @return string
83
     */
84
    public function getFormSignature($account, $currency, $desc, $sum, $secretKey)
85
    {
86
        $hashStr = $account.'{up}'.$currency.'{up}'.$desc.'{up}'.$sum.'{up}'.$secretKey;
87
88
        return hash('sha256', $hashStr);
89
    }
90
91
    /**
92
     * Return hash for params from UnitPay gate.
93
     * @param $method
94
     * @param array $params
95
     * @param $secretKey
96
     * @return string
97
     */
98
    public function getSignature($method, array $params, $secretKey)
99
    {
100
        ksort($params);
101
        unset($params['sign'], $params['signature']);
102
        array_push($params, $secretKey);
103
        array_unshift($params, $method);
104
        return hash('sha256', implode('{up}', $params));
105
    }
106
107
    /**
108
     * Generate UnitPay order array with required array for order form.
109
     * @param $payment_amount
110
     * @param $payment_no
111
     * @param $user_email
112
     * @param $item_name
113
     * @param $currency
114
     * @return array
115
     */
116
    public function generateUnitPayOrderWithRequiredFields($payment_amount, $payment_no, $user_email, $item_name, $currency)
117
    {
118
        $order = [
119
            'PAYMENT_AMOUNT' => $payment_amount,
120
            'PAYMENT_NO' => $payment_no,
121
            'USER_EMAIL' => $user_email,
122
            'ITEM_NAME' => $item_name,
123
            'CURRENCY' => $currency,
124
        ];
125
126
        $this->requiredOrderParamsCheck($order);
127
128
        return $order;
129
    }
130
131
    /**
132
     * Check required order params for order form and raise an exception if fails.
133
     * @param $order
134
     * @throws InvalidConfiguration
135
     */
136
    public function requiredOrderParamsCheck($order)
137
    {
138
        $required_fields = [
139
            'PAYMENT_AMOUNT',
140
            'PAYMENT_NO',
141
            'USER_EMAIL',
142
            'ITEM_NAME',
143
            'CURRENCY',
144
        ];
145
146
        foreach ($required_fields as $key => $value) {
147
            if (! array_key_exists($value, $order) || empty($order[$value])) {
148
                throw InvalidConfiguration::generatePaymentFormOrderParamsNotSet($value);
149
            }
150
        }
151
152
        $currency_arr = [
153
            'RUB',
154
            'UAH',
155
            'BYR',
156
            'EUR',
157
            'USD',
158
        ];
159
160
        if (! in_array($order['CURRENCY'], $currency_arr)) {
161
            throw InvalidConfiguration::generatePaymentFormOrderInvalidCurrency($order['CURRENCY']);
162
        }
163
    }
164
165
    /**
166
     * Generate html forms from view with payment buttons
167
     * Note: you can customise the view via artisan:publish.
168
     * @param $order
169
     * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
170
     */
171
    public function generatePaymentForm($payment_amount, $payment_no, $user_email, $item_name, $currency)
172
    {
173
        $order = $this->generateUnitPayOrderWithRequiredFields($payment_amount, $payment_no, $user_email, $item_name, $currency);
174
175
        $this->requiredOrderParamsCheck($order);
176
177
        $payment_fields['LOCALE'] = config('unitpay.locale', 'ru');
0 ignored issues
show
Coding Style Comprehensibility introduced by
$payment_fields was never initialized. Although not strictly required by PHP, it is generally a good practice to add $payment_fields = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
178
        $payment_fields['PUB_KEY'] = config('unitpay.UNITPAY_PUBLIC_KEY');
179
        $payment_fields['PAYMENT_AMOUNT'] = $order['PAYMENT_AMOUNT'];
180
        $payment_fields['PAYMENT_NO'] = $order['PAYMENT_NO'];
181
        $payment_fields['USER_EMAIL'] = $order['USER_EMAIL'];
182
        $payment_fields['ITEM_NAME'] = $order['ITEM_NAME'];
183
        $payment_fields['CURRENCY'] = $order['CURRENCY'];
184
185
        $payment_fields['SIGN'] = $this->getFormSignature(
186
            $payment_fields['PAYMENT_NO'],
187
            $payment_fields['CURRENCY'],
188
            $payment_fields['ITEM_NAME'],
189
            $payment_fields['PAYMENT_AMOUNT'],
190
            config('unitpay.UNITPAY_SECRET_KEY')
191
        );
192
193
        return view('unitpay::payment_form', compact('payment_fields'));
194
    }
195
196
    /**
197
     * Validate request params from UnitPay gate.
198
     * @param Request $request
199
     * @return bool
200
     */
201
    public function validate(Request $request)
202
    {
203
        $validator = Validator::make($request->all(), [
204
            'method' => 'required|in:check,pay,error',
205
            'params.account' => 'required',
206
            'params.date' => 'required',
207
            'params.payerSum' => 'required',
208
            'params.payerCurrency' => 'required',
209
            'params.signature' => 'required',
210
            'params.orderSum' => 'required',
211
            'params.orderCurrency' => 'required',
212
            'params.unitpayId' => 'required',
213
        ]);
214
215
        if ($validator->fails()) {
216
            return false;
217
        }
218
219
        return true;
220
    }
221
222
    /**
223
     * Validate request signature from UnitPay gate.
224
     * @param Request $request
225
     * @return bool
226
     */
227
    public function validateSignature(Request $request)
228
    {
229
        $sign = $this->getSignature($request->get('method'), $request->get('params'), config('unitpay.UNITPAY_SECRET_KEY'));
230
231
        if ($request->input('params.signature') != $sign) {
232
            return false;
233
        }
234
235
        return true;
236
    }
237
238
    /**
239
     * Validate ip, request params and signature from UnitPay gate.
240
     * @param Request $request
241
     * @return bool
242
     */
243
    public function validateOrderRequestFromGate(Request $request)
244
    {
245
        if (! $this->AllowIP($request->ip()) || ! $this->validate($request) || ! $this->validateSignature($request)) {
246
            $this->eventFillAndSend('unitpay.error', 'validateOrderRequestFromGate', $request);
247
248
            return false;
249
        }
250
251
        return true;
252
    }
253
254
    /**
255
     * Validate the required attributes of the found order
256
     * @param Request $request
257
     * @param $order
258
     * @return bool
259
     */
260
    public function validateSearchOrderRequiredAttributes(Request $request, $order)
261
    {
262
        if (! $order) {
263
            $this->eventFillAndSend('unitpay.error', 'orderNotFound', $request);
264
265
            return false;
266
        }
267
268
        // check required found order attributes
269
        $attr = ['orderStatus', 'orderSum', 'orderCurrency'];
270
271
        foreach ($attr as $k => $value) {
272
            if (! $order->getAttribute($value)) {
273
                $this->eventFillAndSend('unitpay.error', $value.'Invalid', $request);
274
275
                return false;
276
            }
277
        }
278
279
        // compare order attributes vs request params
280
        $attr = ['orderSum', 'orderCurrency'];
281
        foreach ($attr as $k => $value) {
282
            if ( $order->getAttribute($value) != $request->input('params.'.$value) ) {
283
                $this->eventFillAndSend('unitpay.error', $value.'Invalid', $request);
284
285
                return false;
286
            }
287
        }
288
289
        return true;
290
    }
291
292
    /**
293
     * Call SearchOrderFilter and check return order params.
294
     * @param Request $request
295
     * @return bool
296
     * @throws InvalidConfiguration
297
     */
298
    public function callFilterSearchOrder(Request $request)
299
    {
300
        $callable = config('unitpay.searchOrderFilter');
301
302
        if (! is_callable($callable)) {
303
            throw InvalidConfiguration::searchOrderFilterInvalid();
304
        }
305
306
        /*
307
         *  SearchOrderFilter
308
         *  Search order in the database and return order details
309
         *  Must return array with:
310
         *
311
         *  orderStatus
312
         *  orderCurrency
313
         *  orderSum
314
         */
315
316
        $order = $callable($request, $request->input('params.account'));
317
318
        if (! $this->validateSearchOrderRequiredAttributes($request, $order)) {
319
            return false;
320
        }
321
322
        return $order;
323
    }
324
325
    /**
326
     * Call PaidOrderFilter if order not paid.
327
     * @param Request $request
328
     * @param $order
329
     * @return mixed
330
     * @throws InvalidConfiguration
331
     */
332
    public function callFilterPaidOrder(Request $request, $order)
333
    {
334
        $callable = config('unitpay.paidOrderFilter');
335
336
        if (! is_callable($callable)) {
337
            throw InvalidConfiguration::orderPaidFilterInvalid();
338
        }
339
340
        // Run PaidOrderFilter callback
341
        return $callable($request, $order);
342
    }
343
344
    /**
345
     * Run UnitPay::payOrderFromGate($request) when receive request from UnitPay gate.
346
     * @param Request $request
347
     * @return bool
348
     */
349
    public function payOrderFromGate(Request $request)
350
    {
351
        // Validate request params from UnitPay server.
352
        if (! $this->validateOrderRequestFromGate($request)) {
353
            return $this->responseError('validateOrderRequestFromGate');
354
        }
355
356
        // Search and return order
357
        $order = $this->callFilterSearchOrder($request);
358
359
        if (! $order) {
360
            return $this->responseError('searchOrderFilter');
361
        }
362
363
        // Return success response for check and error methods
364
        if (in_array($request->get('method'), ['check', 'error'])) {
365
            $this->eventFillAndSend('unitpay.info', 'payOrderFromGate method = '.$request->get('method'), $request);
366
367
            return $this->responseOK('OK');
368
        }
369
370
        // If method unknown then return error
371
        if ($request->get('method') != 'pay') {
372
            return $this->responseError('Invalid request');
373
        }
374
375
        // If method pay and current order status is paid
376
        // return success response and notify info
377
        if (mb_strtolower($order->orderStatus) === 'paid') {
378
            $this->eventFillAndSend('unitpay.info', 'order already paid', $request);
379
380
            return $this->responseOK('OK');
381
        }
382
383
        // Current order is paid in UnitPay and not paid in database
384
385
        $this->eventFillAndSend('unitpay.success', 'paid order', $request);
386
387
        // PaidOrderFilter - update order into DB as paid & other actions
388
        // if return false then error
389
        if (! $this->callFilterPaidOrder($request, $order)) {
390
            $this->eventFillAndSend('unitpay.error', 'callFilterPaidOrder', $request);
391
392
            return $this->responseError('callFilterPaidOrder');
393
        }
394
395
        // Order is paid in UnitPay and updated in database
396
        return $this->responseOK('OK');
397
    }
398
}
399