Passed
Branch master (249862)
by Adam
07:51
created

PaymentController::handlePaymentException()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 17
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 8
nc 2
nop 2
dl 0
loc 17
rs 9.4285
c 0
b 0
f 0
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\Invoice\CalculatorFactory;
14
use Coyote\Services\UrlBuilder\UrlBuilder;
15
use Coyote\Services\Invoice\Generator as InvoiceGenerator;
16
use Illuminate\Database\Connection as Db;
17
use Illuminate\Http\Request;
18
use GuzzleHttp\Client as HttpClient;
19
20
class PaymentController extends Controller
21
{
22
    /**
23
     * @var InvoiceGenerator
24
     */
25
    private $invoice;
26
27
    /**
28
     * @var CountryRepository
29
     */
30
    private $country;
31
32
    /**
33
     * @var Db
34
     */
35
    private $db;
36
37
    /**
38
     * @var CouponRepository
39
     */
40
    private $coupon;
41
42
    /**
43
     * @var array
44
     */
45
    private $vatRates;
46
47
    /**
48
     * @param InvoiceGenerator $invoice
49
     * @param Db $db
50
     * @param CountryRepository $country
51
     * @param CouponRepository $coupon
52
     */
53
    public function __construct(InvoiceGenerator $invoice, Db $db, CountryRepository $country, CouponRepository $coupon)
54
    {
55
        parent::__construct();
56
57
        $this->invoice = $invoice;
58
        $this->db = $db;
59
        $this->country = $country;
60
        $this->coupon = $coupon;
61
62
        $this->middleware(
63
            function (Request $request, $next) {
64
                /** @var \Coyote\Payment $payment */
65
                $payment = $request->route('payment');
66
67
                if ($payment !== null && $payment instanceof Payment) {
68
                    abort_if($payment->status_id == Payment::PAID, 404);
69
                }
70
71
                return $next($request);
72
            },
73
            ['except' => 'success']
74
        );
75
76
        $this->breadcrumb->push('Praca', route('job.home'));
77
        $this->vatRates = $this->country->vatRatesList();
78
    }
79
80
    /**
81
     * @param \Coyote\Payment $payment
82
     * @return \Illuminate\View\View
83
     */
84
    public function index($payment)
85
    {
86
        $this->breadcrumb->push($payment->job->title, UrlBuilder::job($payment->job));
87
        $this->breadcrumb->push('Płatność');
88
89
        /** @var PaymentForm $form */
90
        $form = $this->getForm($payment);
91
92
        // calculate price based on payment details
93
        $calculator = CalculatorFactory::payment($payment);
94
        $calculator->vatRate = $this->vatRates[$form->get('invoice')->get('country_id')->getValue()] ?? $calculator->vatRate;
95
96
        $coupon = $this->coupon->firstOrNew(['code' => $form->get('coupon')->getValue()]);
0 ignored issues
show
Bug introduced by
The method firstOrNew() does not exist on Coyote\Repositories\Cont...uponRepositoryInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to Coyote\Repositories\Cont...uponRepositoryInterface. ( Ignorable by Annotation )

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

96
        /** @scrutinizer ignore-call */ 
97
        $coupon = $this->coupon->firstOrNew(['code' => $form->get('coupon')->getValue()]);
Loading history...
97
98
        $this->request->attributes->set('validate_coupon_url', route('job.coupon'));
99
100
        return $this->view('job.payment', [
101
            'form'              => $form,
102
            'payment'           => $payment,
103
            'vat_rates'         => $this->vatRates,
104
            'calculator'        => $calculator->toArray(),
105
            'default_vat_rate'  => $payment->plan->vat_rate,
106
            'coupon'            => $coupon->toArray()
107
        ]);
108
    }
109
110
    /**
111
     * @param \Coyote\Payment $payment
112
     * @return \Illuminate\Http\RedirectResponse
113
     * @throws \Exception
114
     */
115
    public function process($payment)
116
    {
117
        /** @var PaymentForm $form */
118
        $form = $this->getForm($payment);
119
        $form->validate();
120
121
        $calculator = CalculatorFactory::payment($payment);
122
        $calculator->vatRate = $this->vatRates[$form->get('invoice')->getValue()['country_id']] ?? $calculator->vatRate;
123
124
        $coupon = $this->coupon->findBy('code', $form->get('coupon')->getValue());
125
        $calculator->setCoupon($coupon);
126
127
        // begin db transaction
128
        return $this->handlePayment(function () use ($payment, $form, $calculator, $coupon) {
129
            // save invoice data. keep in mind that we do not setup invoice number until payment is done.
130
            /** @var \Coyote\Invoice $invoice */
131
            $invoice = $this->invoice->create(
132
                array_merge($form->get('enable_invoice')->isChecked() ? $form->all()['invoice'] : [], ['user_id' => $this->auth->id]),
133
                $payment,
134
                $calculator
135
            );
136
137
            if ($coupon) {
138
                $payment->coupon_id = $coupon->id;
139
            }
140
141
            // associate invoice with payment
142
            $payment->invoice()->associate($invoice);
143
144
            if ($payment->job->firm_id) {
145
                // update firm's VAT ID
146
                $payment->job->firm->vat_id = $form->getRequest()->input('invoice.vat_id');
0 ignored issues
show
Documentation Bug introduced by
It seems like $form->getRequest()->input('invoice.vat_id') of type array is incompatible with the declared type string of property $vat_id.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
147
                $payment->job->firm->save();
148
            }
149
150
            $payment->save();
151
152
            if (!$calculator->grossPrice()) {
153
                return $this->successfulTransaction($payment);
154
            }
155
156
            // make a payment
157
            return $this->{'make' . ucfirst($form->get('payment_method')->getValue()) . 'Transaction'}($payment);
158
        });
159
    }
160
161
    /**
162
     * Successful bank transfer transaction. Redirect to the offer.
163
     *
164
     * @param Payment $payment
165
     * @return \Illuminate\Http\RedirectResponse
166
     */
167
    public function success(Payment $payment)
168
    {
169
        return redirect()
170
            ->to(UrlBuilder::job($payment->job))
171
            ->with('success', trans('payment.pending'));
172
    }
173
174
    /**
175
     * @param Request $request
176
     * @param HttpClient $client
177
     * @param PaymentRepository $payment
178
     * @throws \Exception
179
     */
180
    public function paymentStatus(Request $request, HttpClient $client, PaymentRepository $payment)
181
    {
182
        /** @var \Coyote\Payment $payment */
183
        $payment = $payment->findBy('session_id', $request->get('p24_session_id'));
184
        abort_if($payment === null, 404);
185
186
        $crc = md5(
187
            join(
188
                '|',
189
                array_merge(
190
                    $request->only(['p24_session_id', 'p24_order_id', 'p24_amount', 'p24_currency']),
191
                    [config('services.p24.salt')]
192
                )
193
            )
194
        );
195
196
        try {
197
            if ($request->get('p24_sign') !== $crc) {
198
                throw new \InvalidArgumentException(
199
                    sprintf('Crc does not match in payment: %s.', $payment->session_id)
200
                );
201
            }
202
203
            $response = $client->post(config('services.p24.verify_url'), [
204
                'form_params' => $request->except(['p24_method', 'p24_statement', 'p24_amount'])
205
                    + ['p24_amount' => round($payment->invoice->grossPrice() * 100)]
206
            ]);
207
208
            $body = \GuzzleHttp\Psr7\parse_query($response->getBody());
209
210
            if (!isset($body['error']) || $body['error'] != 0) {
211
                throw new \InvalidArgumentException(
212
                    sprintf('[%s]: %s', $payment->session_id, $response->getBody())
213
                );
214
            }
215
216
            event(new PaymentPaid($payment));
217
        } catch (\Exception $e) {
218
            logger()->debug($request->all());
0 ignored issues
show
Bug introduced by
$request->all() of type array is incompatible with the type string expected by parameter $message of Illuminate\Log\Writer::debug(). ( Ignorable by Annotation )

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

218
            logger()->debug(/** @scrutinizer ignore-type */ $request->all());
Loading history...
219
220
            throw $e;
221
        }
222
    }
223
224
    /**
225
     * Card transaction was successful. Reindex and return to the offer
226
     *
227
     * @param Payment $payment
228
     * @return \Illuminate\Http\RedirectResponse
229
     */
230
    private function successfulTransaction(Payment $payment)
231
    {
232
        // boost job offer, send invoice and reindex
233
        event(new PaymentPaid($payment));
234
235
        return redirect()
236
            ->to(UrlBuilder::job($payment->job))
237
            ->with('success', trans('payment.success'));
238
    }
239
240
    /**
241
     * @param \Coyote\Payment $payment
242
     * @return \Coyote\Services\FormBuilder\Form
243
     */
244
    private function getForm($payment)
245
    {
246
        return $this->createForm(PaymentForm::class, $payment);
247
    }
248
249
    /**
250
     * @param Payment $payment
251
     * @throws PaymentFailedException
252
     * @return \Illuminate\Http\RedirectResponse
253
     */
254
    private function makeCardTransaction(Payment $payment)
255
    {
256
        $client = new \PayLaneRestClient(config('services.paylane.username'), config('services.paylane.password'));
257
258
        /** @var array $result */
259
        $result = $client->cardSale([
260
            'sale' => [
261
                'amount'            => number_format($payment->invoice->grossPrice(), 2, '.', ''),
262
                'currency'          => 'PLN',
263
                'description'       => sprintf('%s - %s', $payment->plan->name, $payment->id)
264
            ],
265
            'customer' => [
266
                'name'              => $this->request->input('name'),
267
                'email'             => $this->auth->email,
268
                'ip'                => $this->request->ip()
269
            ],
270
            'card' => [
271
                'card_number'       => str_replace('-', '', $this->request->input('number')),
272
                'name_on_card'      => $this->request->input('name'),
273
                'expiration_month'  => sprintf('%02d', $this->request->input('exp_month')),
0 ignored issues
show
Bug introduced by
$this->request->input('exp_month') of type array is incompatible with the type string expected by parameter $args of sprintf(). ( Ignorable by Annotation )

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

273
                'expiration_month'  => sprintf('%02d', /** @scrutinizer ignore-type */ $this->request->input('exp_month')),
Loading history...
274
                'expiration_year'   => $this->request->input('exp_year'),
275
                'card_code'         => $this->request->input('cvc'),
276
            ]
277
        ]);
278
279
        if (!$result['success']) {
280
            $error = $result['error'];
281
            logger()->error(var_export($result, true));
282
283
            throw new PaymentFailedException($error['error_description'], $error['error_number']);
284
        }
285
286
        logger()->debug('Successfully payment', ['result' => $result]);
287
        return $this->successfulTransaction($payment);
288
    }
289
290
    /**
291
     * @param Payment $payment
292
     * @return \Illuminate\View\View
293
     */
294
    private function makeTransferTransaction(Payment $payment)
295
    {
296
        $payment->session_id = str_random(90);
297
        $payment->save();
298
299
        return $this->view('job.gateway', [
300
            'payment' => $payment
301
        ]);
302
    }
303
304
    /**
305
     * @param \Closure $callback
306
     * @return \Illuminate\Http\RedirectResponse|mixed
307
     */
308
    private function handlePayment(\Closure $callback)
309
    {
310
        $this->db->beginTransaction();
311
312
        try {
313
            $result = $callback();
314
            $this->db->commit();
315
316
            return $result;
317
        } catch (PaymentFailedException $e) {
318
            return $this->handlePaymentException($e, trans('payment.validation', ['message' => $e->getMessage()]));
319
        } catch (\Exception $e) {
320
            return $this->handlePaymentException($e, trans('payment.unhandled'));
321
        }
322
    }
323
324
    /**
325
     * Handle payment exception. Remove sensitive data before saving to logs and sending to sentry.
326
     *
327
     * @param \Exception $exception
328
     * @param string $message
329
     * @return \Illuminate\Http\RedirectResponse
330
     */
331
    private function handlePaymentException($exception, $message)
332
    {
333
        $back = back()->withInput()->with('error', $message);
334
335
        // remove sensitive data
336
        $this->request->merge(['number' => '***', 'cvc' => '***']);
337
        $_POST['number'] = $_POST['cvc'] = '***';
338
339
        $this->db->rollBack();
340
        // log error. sensitive data won't be saved (we removed them)
341
        logger()->error($exception);
342
343
        if (app()->environment('production')) {
0 ignored issues
show
introduced by
The method environment() does not exist on Illuminate\Container\Container. Are you sure you never get this type here, but always one of the subclasses? ( Ignorable by Annotation )

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

343
        if (app()->/** @scrutinizer ignore-call */ environment('production')) {
Loading history...
344
            app('sentry')->captureException($exception);
345
        }
346
347
        return $back;
348
    }
349
}
350