1<?php
2
3// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0.
4// See the LICENCE file in the repository root for full licence text.
5
6namespace App\Http\Controllers\Payments;
7
8use App\Exceptions\InvalidSignatureException;
9use App\Exceptions\Store\OrderException;
10use App\Exceptions\Store\PaymentRejectedException;
11use App\Libraries\OrderCheckout;
12use App\Libraries\Payments\NotificationType;
13use App\Libraries\Payments\PaypalCreatePayment;
14use App\Libraries\Payments\PaypalExecutePayment;
15use App\Libraries\Payments\PaypalPaymentProcessor;
16use App\Libraries\Payments\PaypalSignature;
17use App\Models\Store\Order;
18use App\Traits\CheckoutErrorSettable;
19use Illuminate\Database\QueryException;
20use Illuminate\Http\Request as HttpRequest;
21use Lang;
22use PayPalHttp\HttpException;
23
24class PaypalController extends Controller
25{
26 use CheckoutErrorSettable;
27
28 public function __construct()
29 {
30 $this->middleware('auth', ['except' => ['ipn']]);
31 $this->middleware('check-user-restricted', ['except' => ['ipn']]);
32 $this->middleware('verify-user', ['except' => ['ipn']]);
33
34 parent::__construct();
35 }
36
37 // When user has approved a payment at Paypal and is redirected back here.
38 public function approved()
39 {
40 // new uses token
41 $params = get_params(request()->all(), null, [
42 'order_id:int',
43 'paymentId:string',
44 'token:string',
45 ], ['null_missing' => true]);
46
47 $order = auth()->user()
48 ->orders()
49 ->paymentRequested()
50 ->findOrFail($params['order_id']);
51
52 if (present($params['paymentId'])) {
53 return $this->setAndRedirectCheckoutError($order, osu_trans('paypal/errors.old_format'));
54 }
55
56 $token = $params['token'];
57 if (!present($token) || $token !== $order->reference) {
58 return $this->setAndRedirectCheckoutError($order, osu_trans('paypal/errors.invalid_token'));
59 }
60
61 try {
62 (new PaypalExecutePayment($order))->run();
63 } catch (HttpException $e) {
64 return $this->setAndRedirectCheckoutError($order, $this->userErrorMessage($e));
65 } catch (PaymentRejectedException) {
66 return $this->setAndRedirectCheckoutError($order, osu_trans('paypal/errors.unknown'));
67 }
68
69 return redirect(route('store.invoice.show', ['invoice' => $order->order_id, 'thanks' => 1]));
70 }
71
72 // Begin process of approving a payment.
73 public function create()
74 {
75 $orderId = get_int(request('order_id'));
76
77 $order = auth()->user()->orders()->paymentRequested()->findOrFail($orderId);
78
79 return (new PaypalCreatePayment($order))->run();
80 }
81
82 // Payment declined by user.
83 public function declined()
84 {
85 $orderId = get_int(request('order_id'));
86
87 $order = auth()->user()->orders()->paymentRequested()->find($orderId);
88
89 if ($order === null) {
90 return ujs_redirect(route('store.cart.show'));
91 }
92
93 (new OrderCheckout($order, Order::PROVIDER_PAYPAL))->failCheckout();
94
95 return $this->setAndRedirectCheckoutError($order, osu_trans('store.checkout.declined'));
96 }
97
98 // Called by Paypal.
99 public function ipn(HttpRequest $request)
100 {
101 $params = static::extractParams($request);
102 $signature = new PaypalSignature($request);
103 $processor = new PaypalPaymentProcessor($params, $signature);
104
105 try {
106 $processor->run();
107 } catch (OrderException $exception) {
108 log_error($exception);
109
110 return response(['message' => 'A validation error occured while running the transaction'], 406);
111 } catch (InvalidSignatureException $exception) {
112 log_error($exception);
113
114 return response(['message' => $exception->getMessage()], 406);
115 } catch (QueryException $exception) {
116 // can get multiple cancellations for the same order from paypal.
117 if (
118 is_sql_unique_exception($exception)
119 && $processor->getNotificationType() === NotificationType::REFUND
120 ) {
121 return 'ok';
122 }
123
124 throw $exception;
125 }
126
127 return 'ok';
128 }
129
130 private function userErrorMessage(HttpException $e)
131 {
132 $json = json_decode($e->getMessage());
133 $key = 'paypal/errors.'.strtolower($json->name ?? 'unknown');
134 if (!Lang::has($key)) {
135 $key = 'paypal/errors.unknown';
136 }
137
138 return osu_trans($key);
139 }
140}