From 8bcf106ffea6c83d66567e2f1e7dda8ec0646b4f Mon Sep 17 00:00:00 2001 From: Jeremy Quinton Date: Wed, 4 Sep 2019 15:31:40 +0200 Subject: [PATCH] Refactored Event Checkout 1. Removed dependencies on $gateway->id in the database which made the codebase fairly brittle in places. Use the name field of payment_gatway instead. 2. Refactored Event Checkout so that in theory it should be a lot easier to add other Omnipay gateways. 3. Removed Paypal Express as a gateway supported by default. 4. Payment Gateways are now configurable by specifying an admin and checkout template in the database directly. 5. Broke each payment option out into its own template instead of trying to handle all payment gateways in one form with funny if conditions. 6. Account settings for Payment Gateways is now dynamic based on what is in the database. 7. Added migration and updated Seeder to make sure the payment_gateways is correct on in stall and when upgrading. --- .../Controllers/EventCheckoutController.php | 181 ++++-------------- .../Controllers/ManageAccountController.php | 21 +- app/Services/PaymentGateway/Dummy.php | 52 +++++ app/Services/PaymentGateway/Factory.php | 67 +++++++ app/Services/PaymentGateway/Stripe.php | 60 ++++++ app/Services/PaymentGateway/StripeSCA.php | 84 ++++++++ composer.json | 3 +- ...2019_09_04_075835_add_default_gateways.php | 74 +++++++ database/seeds/PaymentGatewaySeeder.php | 69 +++++-- .../Partials/PaymentGatewayOptions.blade.php | 95 ++------- .../ManageAccount/Partials/Stripe.blade.php | 17 ++ .../Partials/StripeSCA.blade.php | 17 ++ .../Public/ViewEvent/Partials/Dummy.blade.php | 46 +++++ .../Partials/EventPaymentSection.blade.php | 10 +- .../Partials/PaymentStripe.blade.php | 2 +- .../Partials/PaymentStripeSCA.blade.php | 7 +- 16 files changed, 541 insertions(+), 264 deletions(-) create mode 100644 app/Services/PaymentGateway/Dummy.php create mode 100644 app/Services/PaymentGateway/Factory.php create mode 100644 app/Services/PaymentGateway/Stripe.php create mode 100644 app/Services/PaymentGateway/StripeSCA.php create mode 100644 database/migrations/2019_09_04_075835_add_default_gateways.php create mode 100644 resources/views/ManageAccount/Partials/Stripe.blade.php create mode 100644 resources/views/ManageAccount/Partials/StripeSCA.blade.php create mode 100644 resources/views/Public/ViewEvent/Partials/Dummy.blade.php diff --git a/app/Http/Controllers/EventCheckoutController.php b/app/Http/Controllers/EventCheckoutController.php index 1a2006d2..7e08f05a 100644 --- a/app/Http/Controllers/EventCheckoutController.php +++ b/app/Http/Controllers/EventCheckoutController.php @@ -16,6 +16,7 @@ use App\Models\QuestionAnswer; use App\Models\ReservedTickets; use App\Models\Ticket; use App\Services\Order as OrderService; +use App\Services\PaymentGateway\Factory as PaymentGatewayFactory; use Carbon\Carbon; use Cookie; use DB; @@ -353,17 +354,6 @@ class EventCheckoutController extends Controller $orderService = new OrderService($order_session['order_total'], $order_session['total_booking_fee'], $event); $orderService->calculateFinalCosts(); - switch ($payment_gateway->id) { - case config('attendize.payment_gateway_stripe_sca'): - \Stripe\Stripe::setApiKey($account_payment_gateway->config['apiKey']); - - $intent = \Stripe\PaymentIntent::create([ - 'amount' => $order_total, - 'currency' => $event->currency->code, - ]); - break; - } - $secondsToExpire = Carbon::now()->diffInSeconds($order_session['expires']); $viewData = ['event' => $event, @@ -376,19 +366,17 @@ class EventCheckoutController extends Controller 'secondsToExpire' => $secondsToExpire ]; - if (!empty($intent)) { - $viewData['client_secret'] = $intent->client_secret; - } - return view('Public.ViewEvent.EventPagePayment', $viewData); } /** - * Create the order, update stats, fire off email jobs then redirect user + * Create the order and start the payment for the order via Omnipay + * * * @param Request $request * @param $event_id - * @return \Illuminate\Http\JsonResponse + * @return \Illuminate\Http\JsonResponse|\Illuminate\Http\RedirectResponse + * @throws \Exception */ public function postCreateOrder(Request $request, $event_id) { @@ -398,124 +386,36 @@ class EventCheckoutController extends Controller $ticket_order = session()->get('ticket_order_' . $event_id); $event = Event::findOrFail($event_id); - $orderRequiresPayment = $ticket_order['order_requires_payment']; + $order_requires_payment = $ticket_order['order_requires_payment']; - if ($orderRequiresPayment && $request->get('pay_offline') && $event->enable_offline_payments) { + if ($order_requires_payment && $request->get('pay_offline') && $event->enable_offline_payments) { return $this->completeOrder($event_id); } - if (!$orderRequiresPayment) { + if (!$order_requires_payment) { return $this->completeOrder($event_id); } try { - //more transation data being put in here. - $transaction_data = []; - if (config('attendize.enable_dummy_payment_gateway') == TRUE) { - $formData = config('attendize.fake_card_data'); - $transaction_data = [ - 'card' => $formData - ]; - $gateway = Omnipay::create('Dummy'); - $gateway->initialize(); + $order_service = new OrderService($ticket_order['order_total'], $ticket_order['total_booking_fee'], $event); + $order_service->calculateFinalCosts(); - } else { + $payment_gateway_config = $ticket_order['account_payment_gateway']->config + [ + 'testMode' => config('attendize.enable_test_payments')]; - $gateway = Omnipay::create($ticket_order['payment_gateway']->name); - $gateway->initialize($ticket_order['account_payment_gateway']->config + [ - 'testMode' => config('attendize.enable_test_payments'), - ]); - } + $payment_gateway_factory = new PaymentGatewayFactory(); + $gateway = $payment_gateway_factory->create($ticket_order['payment_gateway']->name, $payment_gateway_config); + //certain payment gateways require an extra parameter here and there so this method takes care of that + //and sets certain options for the gateway that can be used when the transaction is started + $gateway->extractRequestParameters($request); - $orderService = new OrderService($ticket_order['order_total'], $ticket_order['total_booking_fee'], $event); - $orderService->calculateFinalCosts(); + //generic data that is needed for most orders + $order_total = $order_service->getGrandTotal(); + $order_email = $ticket_order['request_data'][0]['order_email']; - $transaction_data += [ - 'amount' => $orderService->getGrandTotal(), - 'currency' => $event->currency->code, - 'description' => 'Order for customer: ' . $request->get('order_email'), - ]; + $response = $gateway->startTransaction($order_total, $order_email, $event); - //TODO: class with an interface that builds the transaction data. - switch ($ticket_order['payment_gateway']->id) { - case config('attendize.payment_gateway_dummy'): - $token = uniqid(); - $transaction_data += [ - 'token' => $token, - 'receipt_email' => $request->get('order_email'), - 'card' => $formData - ]; - break; - case config('attendize.payment_gateway_paypal'): - - $transaction_data += [ - 'cancelUrl' => route('showEventCheckoutPaymentReturn', [ - 'event_id' => $event_id, - 'is_payment_cancelled' => 1 - ]), - 'returnUrl' => route('showEventCheckoutPaymentReturn', [ - 'event_id' => $event_id, - 'is_payment_successful' => 1 - ]), - 'brandName' => isset($ticket_order['account_payment_gateway']->config['brandingName']) - ? $ticket_order['account_payment_gateway']->config['brandingName'] - : $event->organiser->name - ]; - - $transaction = $gateway->purchase($transaction_data); - $response = $transaction->send(); - - break; - case config('attendize.payment_gateway_stripe'): - $token = $request->get('stripeToken'); - $transaction_data += [ - 'token' => $token, - 'receipt_email' => $request->get('order_email'), - 'returnUrl' => route('showEventCheckoutPaymentReturn', [ - 'event_id' => $event_id, - 'is_payment_successful' => 1 - ]), - ]; - - $transaction = $gateway->purchase($transaction_data); - $response = $transaction->send(); - - break; - - case config('attendize.payment_gateway_stripe_sca'): - - Log::error($ticket_order['payment_gateway']->name); - - $paymentMethod = $request->get('paymentMethod'); - - $returnUrl = route('showEventCheckoutPaymentReturn', [ - 'event_id' => $event_id, - 'is_payment_successful' => 1, - ]); - - $transaction_data += [ - 'paymentMethod' => $paymentMethod, - 'receipt_email' => 'jeremyquinton@gmail.com', //$request->get('order_email'), - 'returnUrl' => $returnUrl, - 'confirm' => true, - ]; - - Log::error(print_r($transaction_data, true)); - - $response = $gateway->authorize($transaction_data)->send(); - - break; - - default: - Log::error('No payment gateway configured.'); - return response()->json([ - 'status' => 'error', - 'message' => 'No payment gateway configured.' - ]); - break; - - } if ($response->isSuccessful()) { @@ -526,11 +426,13 @@ class EventCheckoutController extends Controller } elseif ($response->isRedirect()) { - /* - * As we're going off-site for payment we need to store some data in a session so it's available - * when we return - */ - session()->push('ticket_order_' . $event_id . '.transaction_data', $transaction_data); + Log::error("reference : " . $response->getTransactionReference()); + + //As we're going off-site for payment we need to store some data in a session so it's available + //when we return + + session()->push('ticket_order_' . $event_id . '.transaction_data', $gateway->getTransactionData()); + Log::info("Redirect url: " . $response->getRedirectUrl()); $return = [ @@ -568,34 +470,26 @@ class EventCheckoutController extends Controller } /** - * Attempt to complete a user's payment when they return from - * an off-site gateway + * Handles the return when a payment is off site * * @param Request $request * @param $event_id * @return \Illuminate\Http\JsonResponse|\Illuminate\Http\RedirectResponse + * @throws \Exception */ public function showEventCheckoutPaymentReturn(Request $request, $event_id) { - if ($request->get('is_payment_cancelled') == '1') { - session()->flash('message', trans('Event.payment_cancelled')); - return response()->redirectToRoute('showEventCheckout', [ - 'event_id' => $event_id, - 'is_payment_cancelled' => 1, - ]); - } - $ticket_order = session()->get('ticket_order_' . $event_id); - $gateway = Omnipay::create($ticket_order['payment_gateway']->name); - $gateway->initialize($ticket_order['account_payment_gateway']->config + [ - 'testMode' => config('attendize.enable_test_payments'), - ]); + $payment_gateway_config = $ticket_order['account_payment_gateway']->config + [ + 'testMode' => config('attendize.enable_test_payments')]; - $transaction = $gateway->completePurchase($ticket_order['transaction_data'][0]); + $payment_gateway_factory = new PaymentGatewayFactory(); + $gateway = $payment_gateway_factory->create($ticket_order['payment_gateway']->name, $payment_gateway_config); + $gateway->extractRequestParameters($request); + $response = $gateway->completeTransaction($ticket_order['transaction_data'][0]); - $response = $transaction->send(); if ($response->isSuccessful()) { session()->push('ticket_order_' . $event_id . '.transaction_id', $response->getTransactionReference()); @@ -607,6 +501,7 @@ class EventCheckoutController extends Controller 'is_payment_failed' => 1, ]); } + } /** @@ -637,6 +532,7 @@ class EventCheckoutController extends Controller if (isset($ticket_order['transaction_id'])) { $order->transaction_id = $ticket_order['transaction_id'][0]; } + if ($ticket_order['order_requires_payment'] && !isset($request_data['pay_offline'])) { $order->payment_gateway_id = $ticket_order['payment_gateway']->id; } @@ -899,4 +795,3 @@ class EventCheckoutController extends Controller } } - diff --git a/app/Http/Controllers/ManageAccountController.php b/app/Http/Controllers/ManageAccountController.php index 66d64e00..20ec3be7 100644 --- a/app/Http/Controllers/ManageAccountController.php +++ b/app/Http/Controllers/ManageAccountController.php @@ -14,6 +14,9 @@ use HttpClient; use Illuminate\Http\Request; use Input; use Mail; +use Services\PaymentGateway\Dummy; +use Services\PaymentGateway\Stripe; +use Services\PaymentGateway\StripeSCA; use Validator; use GuzzleHttp\Client; @@ -127,20 +130,24 @@ class ManageAccountController extends MyBaseController $account = Account::find(Auth::user()->account_id); $gateway_id = $request->get('payment_gateway'); - switch ($gateway_id) { - case config('attendize.payment_gateway_stripe') : //Stripe + $payment_gateway = PaymentGateway::where('id', '=', $gateway_id)->first(); + + $config = []; + + switch ($payment_gateway->name) { + case Stripe::GATEWAY_NAME : $config = $request->get('stripe'); break; - case config('attendize.payment_gateway_paypal') : //PayPal - $config = $request->get('paypal'); - break; - case config('attendize.payment_gateway_stripe_sca') : //Stripe SCA + case StripeSCA::GATEWAY_NAME : $config = $request->get('stripe_sca'); break; + case Dummy::GATEWAY_NAME : + break; + } PaymentGateway::query()->update(['default' => 0]); - $payment_gateway = PaymentGateway::where('id', '=', $gateway_id)->first(); + $payment_gateway->default = 1; $payment_gateway->save(); diff --git a/app/Services/PaymentGateway/Dummy.php b/app/Services/PaymentGateway/Dummy.php new file mode 100644 index 00000000..09635bce --- /dev/null +++ b/app/Services/PaymentGateway/Dummy.php @@ -0,0 +1,52 @@ +gateway = $gateway; + $this->options = []; + } + + private function createTransactionData($order_total, $order_email, $event) + { + $token = uniqid(); + $this->transaction_data = [ + 'amount' => $order_total, + 'currency' => $event->currency->code, + 'description' => 'Order for customer: ' . $order_email, + 'card' => config('attendize.fake_card_data'), + 'token' => $token, + 'receipt_email' => $order_email + ]; + + return $this->transaction_data; + } + + public function startTransaction($order_total, $order_email, $event) + { + + $this->createTransactionData($order_total, $order_email, $event); + $transaction = $this->gateway->purchase($this->transaction_data); + $response = $transaction->send(); + + return $response; + } + + public function getTransactionData() { + return $this->transaction_data; + } + + public function extractRequestParameters($request) {} + + public function completeTransaction($transactionId) {} +} \ No newline at end of file diff --git a/app/Services/PaymentGateway/Factory.php b/app/Services/PaymentGateway/Factory.php new file mode 100644 index 00000000..cf4008f1 --- /dev/null +++ b/app/Services/PaymentGateway/Factory.php @@ -0,0 +1,67 @@ +initialize(); + + return new Dummy($gateway, $paymentGatewayConfig); + } + + case Stripe::GATEWAY_NAME : + { + + $gateway = Omnipay::create($name); + $gateway->initialize($paymentGatewayConfig); + + return new Stripe($gateway, $paymentGatewayConfig); + } + + case StripeSCA::GATEWAY_NAME : + { + + $gateway = Omnipay::create($name); + $gateway->initialize($paymentGatewayConfig); + + return new StripeSCA($gateway, $paymentGatewayConfig); + + } + + default : + { + throw New \Exception('Invalid gateway specified'); + } + } + } +} \ No newline at end of file diff --git a/app/Services/PaymentGateway/Stripe.php b/app/Services/PaymentGateway/Stripe.php new file mode 100644 index 00000000..d386d53a --- /dev/null +++ b/app/Services/PaymentGateway/Stripe.php @@ -0,0 +1,60 @@ +gateway = $gateway; + $this->options = []; + } + + private function createTransactionData($order_total, $order_email, $event) + { + $this->transaction_data = [ + 'amount' => $order_total, + 'currency' => $event->currency->code, + 'description' => 'Order for customer: ' . $order_email, + 'token' => $this->options['stripeToken'], + 'receipt_email' => $order_email + ]; + + return $this->transaction_data; + } + + public function startTransaction($order_total, $order_email, $event) + { + + $this->createTransactionData($order_total, $order_email, $event); + $transaction = $this->gateway->purchase($this->transaction_data); + $response = $transaction->send(); + + return $response; + } + + public function getTransactionData() { + return $this->transaction_data; + } + + public function extractRequestParameters($request) + { + foreach ($this->extra_params as $param) { + if (!empty($request->get($param))) { + $this->options[$param] = $request->get($param); + } + } + } + + public function completeTransaction($transactionId) {} + +} \ No newline at end of file diff --git a/app/Services/PaymentGateway/StripeSCA.php b/app/Services/PaymentGateway/StripeSCA.php new file mode 100644 index 00000000..f6a7e64e --- /dev/null +++ b/app/Services/PaymentGateway/StripeSCA.php @@ -0,0 +1,84 @@ +gateway = $gateway; + $this->options = []; + } + + private function createTransactionData($order_total, $order_email, $event) + { + + $returnUrl = route('showEventCheckoutPaymentReturn', [ + 'event_id' => $event->id, + 'is_payment_successful' => 1, + ]); + + $this->transaction_data = [ + 'amount' => $order_total, + 'currency' => $event->currency->code, + 'description' => 'Order for customer: ' . $order_email, + 'paymentMethod' => $this->options['paymentMethod'], + 'receipt_email' => $order_email, + 'returnUrl' => $returnUrl, + 'confirm' => true + ]; + + return $this->transaction_data; + } + + public function startTransaction($order_total, $order_email, $event) + { + + $this->createTransactionData($order_total, $order_email, $event); + $response = $this->gateway->authorize($this->transaction_data)->send(); + + return $response; + } + + public function getTransactionData() { + return $this->transaction_data; + } + + public function extractRequestParameters($request) + { + foreach ($this->extra_params as $param) { + if (!empty($request->get($param))) { + $this->options[$param] = $request->get($param); + } + } + } + + public function completeTransaction($transactionId = '') { + + $intentData = array( + 'paymentIntentReference' => $this->options['payment_intent'], + ); + + $paymentIntent = $this->gateway->fetchPaymentIntent($intentData); + $response = $paymentIntent->send(); + if ($response->requiresConfirmation()) { + $response = $this->gateway->confirm($intentData)->send(); + } + + + return $response; + } + +} \ No newline at end of file diff --git a/composer.json b/composer.json index 9634cab0..ef99dd91 100755 --- a/composer.json +++ b/composer.json @@ -53,7 +53,8 @@ "database", "app/Http/Controllers", "app/Models", - "app/Attendize" + "app/Attendize", + "app/Services" ], "psr-4": { "App\\": "app/", diff --git a/database/migrations/2019_09_04_075835_add_default_gateways.php b/database/migrations/2019_09_04_075835_add_default_gateways.php new file mode 100644 index 00000000..2d7f6daf --- /dev/null +++ b/database/migrations/2019_09_04_075835_add_default_gateways.php @@ -0,0 +1,74 @@ +delete(); + + Schema::table('payment_gateways', function($table) { + $table->boolean('default')->default(0); + $table->string('admin_blade_template', 150)->default(''); + $table->string('checkout_blade_template', 150)->default(''); + }); + + DB::table('payment_gateways') + ->where('provider_name', 'Stripe') + ->update(['admin_blade_template' => 'ManageAccount.Partials.Stripe', + 'checkout_blade_template' => 'Public.ViewEvent.Partials.PaymentStripe']); + + $dummyGateway = DB::table('payment_gateways')->where('name', '=', 'Dummy')->first(); + + if ($dummyGateway === null) { + // user doesn't exist + DB::table('payment_gateways')->insert( + array( + 'provider_name' => 'Dummy/Test Gateway', + 'provider_url' => 'none', + 'is_on_site' => 1, + 'can_refund' => 1, + 'name' => 'Dummy', + 'default' => 0, + 'admin_blade_template' => '', + 'checkout_blade_template' => 'Public.ViewEvent.Partials.Dummy' + ) + ); + } + + $stripePaymentIntents = DB::table('payment_gateways')->where('name', '=', 'Stripe\PaymentIntents')->first(); + if ($stripePaymentIntents === null) { + DB::table('payment_gateways')->insert( + [ + 'provider_name' => 'Stripe SCA', + 'provider_url' => 'https://www.stripe.com', + 'is_on_site' => 0, + 'can_refund' => 1, + 'name' => 'Stripe\PaymentIntents', + 'default' => 0, + 'admin_blade_template' => 'ManageAccount.Partials.StripeSCA', + 'checkout_blade_template' => 'Public.ViewEvent.Partials.PaymentStripeSCA' + ] + ); + } + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + // + } +} \ No newline at end of file diff --git a/database/seeds/PaymentGatewaySeeder.php b/database/seeds/PaymentGatewaySeeder.php index 1795872f..7d792922 100644 --- a/database/seeds/PaymentGatewaySeeder.php +++ b/database/seeds/PaymentGatewaySeeder.php @@ -11,27 +11,56 @@ class PaymentGatewaySeeder extends Seeder */ public function run() { - $payment_gateways = [ - [ - 'id' => 1, - 'name' => 'Stripe', - 'provider_name' => 'Stripe', - 'provider_url' => 'https://www.stripe.com', - 'is_on_site' => 1, - 'can_refund' => 1, - ], - [ - 'id' => 2, - 'name' => 'PayPal_Express', - 'provider_name' => 'PayPal Express', - 'provider_url' => 'https://www.paypal.com', - 'is_on_site' => 0, - 'can_refund' => 0 - ] - ]; + $dummyGateway = DB::table('payment_gateways')->where('name', '=', 'Dummy')->first(); - DB::table('payment_gateways')->insert($payment_gateways); + if ($dummyGateway === null) { + // user doesn't exist + DB::table('payment_gateways')->insert( + [ + 'provider_name' => 'Dummy/Test Gateway', + 'provider_url' => 'none', + 'is_on_site' => 1, + 'can_refund' => 1, + 'name' => 'Dummy', + 'default' => 0, + 'admin_blade_template' => '', + 'checkout_blade_template' => 'Public.ViewEvent.Partials.Dummy' + ] + ); + } + + $stripe = DB::table('payment_gateways')->where('name', '=', 'Stripe')->first(); + if ($stripe === null) { + DB::table('payment_gateways')->insert( + [ + 'name' => 'Stripe', + 'provider_name' => 'Stripe', + 'provider_url' => 'https://www.stripe.com', + 'is_on_site' => 1, + 'can_refund' => 1, + 'default' => 0, + 'admin_blade_template' => 'ManageAccount.Partials.Stripe', + 'checkout_blade_template' => 'Public.ViewEvent.Partials.PaymentStripe' + ] + ); + } + + $stripePaymentIntents = DB::table('payment_gateways')->where('name', '=', 'Stripe\PaymentIntents')->first(); + if ($stripePaymentIntents === null) { + DB::table('payment_gateways')->insert( + [ + 'provider_name' => 'Stripe SCA', + 'provider_url' => 'https://www.stripe.com', + 'is_on_site' => 0, + 'can_refund' => 1, + 'name' => 'Stripe\PaymentIntents', + 'default' => 0, + 'admin_blade_template' => 'ManageAccount.Partials.StripeSCA', + 'checkout_blade_template' => 'Public.ViewEvent.Partials.PaymentStripeSCA' + ] + ); + } } -} +} \ No newline at end of file diff --git a/resources/views/ManageAccount/Partials/PaymentGatewayOptions.blade.php b/resources/views/ManageAccount/Partials/PaymentGatewayOptions.blade.php index 94f0eff7..8c18e970 100644 --- a/resources/views/ManageAccount/Partials/PaymentGatewayOptions.blade.php +++ b/resources/views/ManageAccount/Partials/PaymentGatewayOptions.blade.php @@ -1,10 +1,10 @@ @@ -38,10 +36,9 @@ } }; - var cardElement = elements.create('card', {style: style}); + var cardElement = elements.create('card', {hidePostalCode: true, style: style}); cardElement.mount('#card-element'); - cardElement.addEventListener('change', function(event) { var displayError = document.getElementById('card-errors'); if (event.error) {