Completed
Branch feature/coupons (8ab0b9)
by Adam
13:26
created

PaymentController   C

Complexity

Total Complexity 21

Size/Duplication

Total Lines 236
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 26

Importance

Changes 3
Bugs 0 Features 0
Metric Value
dl 0
loc 236
rs 5
c 3
b 0
f 0
wmc 21
lcom 1
cbo 26

7 Methods

Rating   Name   Duplication   Size   Complexity  
B __construct() 0 26 1
B index() 0 26 1
B process() 0 41 3
A getForm() 0 4 1
B makeTransaction() 0 31 5
A handlePaymentException() 0 18 2
C handlePayment() 0 23 8
1
<?php
2
3
namespace Coyote\Http\Controllers\Job;
4
5
use Coyote\Events\PaymentPaid;
6
use Coyote\Http\Controllers\Controller;
7
use Coyote\Http\Forms\Job\PaymentForm;
8
use Coyote\Payment;
9
use Coyote\Repositories\Contracts\CountryRepositoryInterface as CountryRepository;
10
use Coyote\Repositories\Contracts\CouponRepositoryInterface as CouponRepository;
11
use Coyote\Services\Invoice\Calculator;
12
use Coyote\Services\Invoice\CalculatorFactory;
13
use Coyote\Services\UrlBuilder\UrlBuilder;
14
use Coyote\Services\Invoice\Generator as InvoiceGenerator;
15
use Illuminate\Database\Connection as Db;
16
use Illuminate\Http\Request;
17
use Braintree\Configuration;
18
use Braintree\ClientToken;
19
use Braintree\Transaction;
20
use Braintree\Exception;
21
22
class PaymentController extends Controller
23
{
24
    /**
25
     * @var InvoiceGenerator
26
     */
27
    private $invoice;
28
29
    /**
30
     * @var CountryRepository
31
     */
32
    private $country;
33
34
    /**
35
     * @var Db
36
     */
37
    private $db;
38
39
    /**
40
     * @var CouponRepository
41
     */
42
    private $coupon;
43
44
    /**
45
     * @var array
46
     */
47
    private $vatRates;
48
49
    /**
50
     * @param InvoiceGenerator $invoice
51
     * @param Db $db
52
     * @param CountryRepository $country
53
     * @param CouponRepository $coupon
54
     */
55
    public function __construct(InvoiceGenerator $invoice, Db $db, CountryRepository $country, CouponRepository $coupon)
56
    {
57
        parent::__construct();
58
59
        $this->invoice = $invoice;
60
        $this->db = $db;
61
        $this->country = $country;
62
        $this->coupon = $coupon;
63
64
        $this->middleware(function (Request $request, $next) {
65
            /** @var \Coyote\Payment $payment */
66
            $payment = $request->route('payment');
67
68
            abort_if($payment->status_id == Payment::PAID, 404);
69
70
            return $next($request);
71
        });
72
73
        $this->breadcrumb->push('Praca', route('job.home'));
74
        $this->vatRates = $this->country->vatRatesList();
75
76
        Configuration::environment(config('services.braintree.env'));
77
        Configuration::merchantId(config('services.braintree.merchant_id'));
78
        Configuration::publicKey(config('services.braintree.public_key'));
79
        Configuration::privateKey(config('services.braintree.private_key'));
80
    }
81
82
    /**
83
     * @param \Coyote\Payment $payment
84
     * @return \Illuminate\View\View
85
     */
86
    public function index($payment)
87
    {
88
        $this->breadcrumb->push($payment->job->title, UrlBuilder::job($payment->job));
89
        $this->breadcrumb->push('Promowanie ogłoszenia');
90
91
        /** @var PaymentForm $form */
92
        $form = $this->getForm($payment);
93
94
        // calculate price based on payment details
95
        $calculator = CalculatorFactory::payment($payment);
96
        $calculator->vatRate = $this->vatRates[$form->get('invoice')->get('country_id')->getValue()] ?? $calculator->vatRate;
97
98
        $coupon = $this->coupon->firstOrNew(['code' => $form->get('coupon')->getValue()]);
99
100
        $this->request->attributes->set('validate_coupon_url', route('job.coupon'));
101
102
        return $this->view('job.payment', [
103
            'form'              => $form,
104
            'payment'           => $payment,
105
            'vat_rates'         => $this->vatRates,
106
            'calculator'        => $calculator->toArray(),
107
            'default_vat_rate'  => $payment->plan->vat_rate,
108
            'client_token'      => ClientToken::generate(),
109
            'coupon'            => $coupon->toArray()
110
        ]);
111
    }
112
113
    /**
114
     * @param \Coyote\Payment $payment
115
     * @return \Illuminate\Http\RedirectResponse
116
     * @throws \Exception
117
     */
118
    public function process($payment)
119
    {
120
        /** @var PaymentForm $form */
121
        $form = $this->getForm($payment);
122
        $form->validate();
123
124
        $calculator = CalculatorFactory::payment($payment);
125
        $calculator->vatRate = $this->vatRates[$form->get('invoice')->getValue()['country_id']] ?? $calculator->vatRate;
126
127
        $coupon = $this->coupon->findBy('code', $form->get('coupon')->getValue());
128
        $calculator->setCoupon($coupon);
129
130
        return $this->handlePayment(function () use ($payment, $form, $calculator, $coupon) {
131
            $this->makeTransaction($payment, $calculator);
132
133
            // save invoice data. keep in mind that we do not setup invoice number until payment is done.
134
            /** @var \Coyote\Invoice $invoice */
135
            $invoice = $this->invoice->create(
136
                array_merge($form->get('enable_invoice')->isChecked() ? $form->all()['invoice'] : [], ['user_id' => $this->auth->id]),
137
                $payment,
138
                $calculator
139
            );
140
141
            if ($coupon) {
142
                $payment->coupon_id = $coupon->id;
143
144
                $coupon->delete();
145
            }
146
147
            // associate invoice with payment
148
            $payment->invoice()->associate($invoice);
149
            $payment->save();
150
151
            // boost job offer, send invoice and reindex
152
            event(new PaymentPaid($payment, $this->auth));
153
154
            return redirect()
155
                ->to(UrlBuilder::job($payment->job))
156
                ->with('success', trans('payment.success'));
157
        });
158
    }
159
160
    /**
161
     * @param \Coyote\Payment $payment
162
     * @return \Coyote\Services\FormBuilder\Form
163
     */
164
    private function getForm($payment)
165
    {
166
        return $this->createForm(PaymentForm::class, $payment);
167
    }
168
169
    /**
170
     * @param Payment $payment
171
     * @param Calculator $calculator
172
     * @throws Exception\ValidationsFailed
173
     */
174
    private function makeTransaction(Payment $payment, Calculator $calculator)
175
    {
176
        if (!$calculator->grossPrice()) {
177
            return;
178
        }
179
180
        /** @var mixed $result */
181
        $result = Transaction::sale([
182
            'amount'                => number_format($calculator->grossPrice(), 2, '.', ''),
183
            'orderId'               => $payment->id,
184
            'paymentMethodNonce'    => $this->request->input("payment_method_nonce"),
185
            'options' => [
186
                'submitForSettlement' => true
187
            ]
188
        ]);
189
190
        /** @var $result \Braintree\Result\Error */
191
        if (!$result->success || is_null($result->transaction)) {
192
            /** @var \Braintree\Error\Validation $error */
193
            $error = array_first($result->errors->deepAll());
194
            logger()->error(var_export($result, true));
195
196
            if (is_null($error)) {
197
                throw new Exception\ValidationsFailed();
198
            }
199
200
            throw new Exception\ValidationsFailed($error->message, $error->code);
201
        }
202
203
        logger()->debug('Successfully payment', ['result' => $result]);
204
    }
205
206
    /**
207
     * @param \Exception $exception
208
     * @param string $message
209
     * @return \Illuminate\Http\RedirectResponse
210
     */
211
    private function handlePaymentException($exception, $message)
212
    {
213
        $back = back()->withInput()->with('error', $message);
214
215
        // remove sensitive data
216
        $this->request->merge(['number' => '***', 'cvc' => '***']);
217
        $_POST['number'] = $_POST['cvc'] = '***';
218
219
        $this->db->rollBack();
220
        // log error. sensitive data won't be saved (we removed them)
221
        logger()->error($exception);
222
223
        if (app()->environment('production')) {
224
            app('sentry')->captureException($exception);
225
        }
226
227
        return $back;
228
    }
229
230
    /**
231
     * @param \Closure $callback
232
     * @return \Illuminate\Http\RedirectResponse|mixed
233
     */
234
    private function handlePayment(\Closure $callback)
235
    {
236
        $this->db->beginTransaction();
237
238
        try {
239
            $result = $callback();
240
            $this->db->commit();
241
242
            return $result;
243
        } catch (Exception\Authentication $e) {
244
            return $this->handlePaymentException($e, trans('payment.forbidden'));
0 ignored issues
show
Bug introduced by
It seems like trans('payment.forbidden') targeting trans() can also be of type object<Illuminate\Contra...Translation\Translator>; however, Coyote\Http\Controllers\...andlePaymentException() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
245
        } catch (Exception\Authorization $e) {
246
            return $this->handlePaymentException($e, trans('payment.unauthorized'));
0 ignored issues
show
Bug introduced by
It seems like trans('payment.unauthorized') targeting trans() can also be of type object<Illuminate\Contra...Translation\Translator>; however, Coyote\Http\Controllers\...andlePaymentException() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
247
        } catch (Exception\Timeout $e) {
248
            return $this->handlePaymentException($e, trans('payment.timeout'));
0 ignored issues
show
Bug introduced by
It seems like trans('payment.timeout') targeting trans() can also be of type object<Illuminate\Contra...Translation\Translator>; however, Coyote\Http\Controllers\...andlePaymentException() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
249
        } catch (Exception\ServerError $e) {
250
            return $this->handlePaymentException($e, trans('payment.unauthorized'));
0 ignored issues
show
Bug introduced by
It seems like trans('payment.unauthorized') targeting trans() can also be of type object<Illuminate\Contra...Translation\Translator>; however, Coyote\Http\Controllers\...andlePaymentException() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
251
        } catch (Exception\ValidationsFailed $e) {
252
            return $this->handlePaymentException($e, $e->getMessage() ?: trans('payment.validation'));
253
        } catch (\Exception $e) {
254
            return $this->handlePaymentException($e, trans('payment.unhandled'));
0 ignored issues
show
Bug introduced by
It seems like trans('payment.unhandled') targeting trans() can also be of type object<Illuminate\Contra...Translation\Translator>; however, Coyote\Http\Controllers\...andlePaymentException() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
255
        }
256
    }
257
}
258