Completed
Branch master (8e0976)
by Adam
04:13
created

PaymentController   A

Complexity

Total Complexity 28

Size/Duplication

Total Lines 337
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 27

Importance

Changes 0
Metric Value
dl 0
loc 337
rs 10
c 0
b 0
f 0
wmc 28
lcom 1
cbo 27

12 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 26 3
A establishVatRate() 0 6 3
A index() 0 25 1
B process() 0 45 5
A success() 0 6 1
B paymentStatus() 0 43 5
A successfulTransaction() 0 9 1
A getForm() 0 4 1
A makeCardTransaction() 0 35 2
A makeTransferTransaction() 0 9 1
A handlePayment() 0 15 3
A handlePaymentException() 0 18 2
1
<?php
2
3
namespace Coyote\Http\Controllers\Job;
4
5
use Coyote\Events\PaymentPaid;
6
use Coyote\Exceptions\PaymentFailedException;
7
use Coyote\Http\Controllers\Controller;
8
use Coyote\Http\Forms\Job\PaymentForm;
9
use Coyote\Payment;
10
use Coyote\Repositories\Contracts\CountryRepositoryInterface as CountryRepository;
11
use Coyote\Repositories\Contracts\CouponRepositoryInterface as CouponRepository;
12
use Coyote\Repositories\Contracts\PaymentRepositoryInterface as PaymentRepository;
13
use Coyote\Services\FormBuilder\FormInterface;
14
use Coyote\Services\Invoice\CalculatorFactory;
15
use Coyote\Services\UrlBuilder\UrlBuilder;
16
use Coyote\Services\Invoice\Generator as InvoiceGenerator;
17
use Illuminate\Database\Connection as Db;
18
use Illuminate\Http\Request;
19
use GuzzleHttp\Client as HttpClient;
20
21
class PaymentController extends Controller
22
{
23
    /**
24
     * @var InvoiceGenerator
25
     */
26
    private $invoice;
27
28
    /**
29
     * @var CountryRepository
30
     */
31
    private $country;
32
33
    /**
34
     * @var Db
35
     */
36
    private $db;
37
38
    /**
39
     * @var CouponRepository
40
     */
41
    private $coupon;
42
43
    /**
44
     * @var array
45
     */
46
    private $vatRates;
47
48
    /**
49
     * @param InvoiceGenerator $invoice
50
     * @param Db $db
51
     * @param CountryRepository $country
52
     * @param CouponRepository $coupon
53
     */
54
    public function __construct(InvoiceGenerator $invoice, Db $db, CountryRepository $country, CouponRepository $coupon)
55
    {
56
        parent::__construct();
57
58
        $this->invoice = $invoice;
59
        $this->db = $db;
60
        $this->country = $country;
61
        $this->coupon = $coupon;
62
63
        $this->middleware(
64
            function (Request $request, $next) {
65
                /** @var \Coyote\Payment $payment */
66
                $payment = $request->route('payment');
67
68
                if ($payment !== null && $payment instanceof Payment) {
69
                    abort_if($payment->status_id == Payment::PAID, 404);
70
                }
71
72
                return $next($request);
73
            },
74
            ['except' => 'success']
75
        );
76
77
        $this->breadcrumb->push('Praca', route('job.home'));
78
        $this->vatRates = $this->country->vatRatesList();
79
    }
80
81
    private function establishVatRate(FormInterface $form)
82
    {
83
        $invoice = $form->get('invoice')->getValue();
84
85
        return isset($this->vatRates[$invoice['country_id']]) && $invoice['vat_id'] ? $this->vatRates[$invoice['country_id']] : config('vendor.default_vat_rate');
86
    }
87
88
    /**
89
     * @param \Coyote\Payment $payment
90
     * @return \Illuminate\View\View
91
     */
92
    public function index($payment)
93
    {
94
        $this->breadcrumb->push($payment->job->title, UrlBuilder::job($payment->job));
95
        $this->breadcrumb->push('Płatność');
96
97
        /** @var PaymentForm $form */
98
        $form = $this->getForm($payment);
99
100
        // calculate price based on payment details
101
        $calculator = CalculatorFactory::payment($payment);
102
        $calculator->vatRate = $this->establishVatRate($form);
103
104
        $coupon = $this->coupon->firstOrNew(['code' => $form->get('coupon')->getValue()]);
105
106
        $this->request->attributes->set('validate_coupon_url', route('job.coupon'));
107
108
        return $this->view('job.payment', [
109
            'form'              => $form,
110
            'payment'           => $payment,
111
            'vat_rates'         => $this->vatRates,
112
            'calculator'        => $calculator->toArray(),
113
            'default_vat_rate'  => $payment->plan->vat_rate,
114
            'coupon'            => $coupon->toArray()
115
        ]);
116
    }
117
118
    /**
119
     * @param \Coyote\Payment $payment
120
     * @return \Illuminate\Http\RedirectResponse
121
     * @throws \Exception
122
     */
123
    public function process($payment)
124
    {
125
        /** @var PaymentForm $form */
126
        $form = $this->getForm($payment);
127
        $form->validate();
128
129
        $calculator = CalculatorFactory::payment($payment);
130
        $calculator->vatRate = $this->establishVatRate($form);
131
132
        $coupon = $this->coupon->findBy('code', $form->get('coupon')->getValue());
133
        $calculator->setCoupon($coupon);
134
135
        // begin db transaction
136
        return $this->handlePayment(function () use ($payment, $form, $calculator, $coupon) {
137
            // save invoice data. keep in mind that we do not setup invoice number until payment is done.
138
            /** @var \Coyote\Invoice $invoice */
139
            $invoice = $this->invoice->create(
140
                array_merge($form->get('enable_invoice')->isChecked() ? $form->all()['invoice'] : [], ['user_id' => $this->auth->id]),
141
                $payment,
142
                $calculator
143
            );
144
145
            if ($coupon) {
146
                $payment->coupon_id = $coupon->id;
147
            }
148
149
            // associate invoice with payment
150
            $payment->invoice()->associate($invoice);
151
152
            if ($payment->job->firm_id) {
153
                // update firm's VAT ID
154
                $payment->job->firm->vat_id = $form->getRequest()->input('invoice.vat_id');
155
                $payment->job->firm->save();
156
            }
157
158
            $payment->save();
159
160
            if (!$calculator->grossPrice()) {
161
                return $this->successfulTransaction($payment);
162
            }
163
164
            // make a payment
165
            return $this->{'make' . ucfirst($form->get('payment_method')->getValue()) . 'Transaction'}($payment);
166
        });
167
    }
168
169
    /**
170
     * Successful bank transfer transaction. Redirect to the offer.
171
     *
172
     * @param Payment $payment
173
     * @return \Illuminate\Http\RedirectResponse
174
     */
175
    public function success(Payment $payment)
176
    {
177
        return redirect()
178
            ->to(UrlBuilder::job($payment->job))
179
            ->with('success', trans('payment.pending'));
180
    }
181
182
    /**
183
     * @param Request $request
184
     * @param HttpClient $client
185
     * @param PaymentRepository $payment
186
     * @throws \Exception
187
     */
188
    public function paymentStatus(Request $request, HttpClient $client, PaymentRepository $payment)
189
    {
190
        /** @var \Coyote\Payment $payment */
191
        $payment = $payment->findBy('session_id', $request->get('p24_session_id'));
0 ignored issues
show
Documentation Bug introduced by
The method findBy does not exist on object<Coyote\Payment>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
192
        abort_if($payment === null, 404);
193
194
        $crc = md5(
195
            join(
196
                '|',
197
                array_merge(
198
                    $request->only(['p24_session_id', 'p24_order_id', 'p24_amount', 'p24_currency']),
199
                    [config('services.p24.salt')]
200
                )
201
            )
202
        );
203
204
        try {
205
            if ($request->get('p24_sign') !== $crc) {
206
                throw new \InvalidArgumentException(
207
                    sprintf('Crc does not match in payment: %s.', $payment->session_id)
208
                );
209
            }
210
211
            $response = $client->post(config('services.p24.verify_url'), [
212
                'form_params' => $request->except(['p24_method', 'p24_statement', 'p24_amount'])
213
                    + ['p24_amount' => round($payment->invoice->grossPrice() * 100)]
214
            ]);
215
216
            $body = \GuzzleHttp\Psr7\parse_query($response->getBody());
217
218
            if (!isset($body['error']) || $body['error'] != 0) {
219
                throw new \InvalidArgumentException(
220
                    sprintf('[%s]: %s', $payment->session_id, $response->getBody())
221
                );
222
            }
223
224
            event(new PaymentPaid($payment));
225
        } catch (\Exception $e) {
226
            logger()->debug($request->all());
0 ignored issues
show
Documentation introduced by
$request->all() is of type array, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
227
228
            throw $e;
229
        }
230
    }
231
232
    /**
233
     * Card transaction was successful. Reindex and return to the offer
234
     *
235
     * @param Payment $payment
236
     * @return \Illuminate\Http\RedirectResponse
237
     */
238
    private function successfulTransaction(Payment $payment)
239
    {
240
        // boost job offer, send invoice and reindex
241
        event(new PaymentPaid($payment));
242
243
        return redirect()
244
            ->to(UrlBuilder::job($payment->job))
245
            ->with('success', trans('payment.success'));
246
    }
247
248
    /**
249
     * @param \Coyote\Payment $payment
250
     * @return \Coyote\Services\FormBuilder\Form
251
     */
252
    private function getForm($payment)
253
    {
254
        return $this->createForm(PaymentForm::class, $payment);
255
    }
256
257
    /**
258
     * @param Payment $payment
259
     * @throws PaymentFailedException
260
     * @return \Illuminate\Http\RedirectResponse
261
     */
262
    private function makeCardTransaction(Payment $payment)
263
    {
264
        $client = new \PayLaneRestClient(config('services.paylane.username'), config('services.paylane.password'));
265
266
        /** @var array $result */
267
        $result = $client->cardSale([
268
            'sale' => [
269
                'amount'            => number_format($payment->invoice->grossPrice(), 2, '.', ''),
270
                'currency'          => 'PLN',
271
                'description'       => sprintf('%s - %s', $payment->plan->name, $payment->id)
272
            ],
273
            'customer' => [
274
                'name'              => $this->request->input('name'),
275
                'email'             => $this->auth->email,
276
                'ip'                => $this->request->ip()
277
            ],
278
            'card' => [
279
                'card_number'       => str_replace('-', '', $this->request->input('number')),
280
                'name_on_card'      => $this->request->input('name'),
281
                'expiration_month'  => sprintf('%02d', $this->request->input('exp_month')),
282
                'expiration_year'   => $this->request->input('exp_year'),
283
                'card_code'         => $this->request->input('cvc'),
284
            ]
285
        ]);
286
287
        if (!$result['success']) {
288
            $error = $result['error'];
289
            logger()->error(var_export($result, true));
290
291
            throw new PaymentFailedException($error['error_description'], $error['error_number']);
292
        }
293
294
        logger()->debug('Successfully payment', ['result' => $result]);
295
        return $this->successfulTransaction($payment);
296
    }
297
298
    /**
299
     * @param Payment $payment
300
     * @return \Illuminate\View\View
301
     */
302
    private function makeTransferTransaction(Payment $payment)
303
    {
304
        $payment->session_id = str_random(90);
305
        $payment->save();
306
307
        return $this->view('job.gateway', [
308
            'payment' => $payment
309
        ]);
310
    }
311
312
    /**
313
     * @param \Closure $callback
314
     * @return \Illuminate\Http\RedirectResponse|mixed
315
     */
316
    private function handlePayment(\Closure $callback)
317
    {
318
        $this->db->beginTransaction();
319
320
        try {
321
            $result = $callback();
322
            $this->db->commit();
323
324
            return $result;
325
        } catch (PaymentFailedException $e) {
326
            return $this->handlePaymentException($e, trans('payment.validation', ['message' => $e->getMessage()]));
327
        } catch (\Exception $e) {
328
            return $this->handlePaymentException($e, trans('payment.unhandled'));
329
        }
330
    }
331
332
    /**
333
     * Handle payment exception. Remove sensitive data before saving to logs and sending to sentry.
334
     *
335
     * @param \Exception $exception
336
     * @param string $message
337
     * @return \Illuminate\Http\RedirectResponse
338
     */
339
    private function handlePaymentException($exception, $message)
340
    {
341
        $back = back()->withInput()->with('error', $message);
342
343
        // remove sensitive data
344
        $this->request->merge(['number' => '***', 'cvc' => '***']);
345
        $_POST['number'] = $_POST['cvc'] = '***';
346
347
        $this->db->rollBack();
348
        // log error. sensitive data won't be saved (we removed them)
349
        logger()->error($exception);
350
351
        if (app()->environment('production')) {
352
            app('sentry')->captureException($exception);
353
        }
354
355
        return $back;
356
    }
357
}
358