From f41b49ad7ec581c2f9a3877d7fedbcc651ecf350 Mon Sep 17 00:00:00 2001 From: Lars Kusch Date: Sat, 9 Oct 2021 10:09:04 +0200 Subject: [PATCH] Implement Stripe Giropay --- app/Models/Gateway.php | 3 +- app/Models/GatewayType.php | 5 +- app/PaymentDrivers/Stripe/GIROPAY.php | 141 ++++++++++++++++++ app/PaymentDrivers/StripePaymentDriver.php | 7 + .../js/clients/payments/stripe-giropay.js | 57 +++++++ 5 files changed, 211 insertions(+), 2 deletions(-) create mode 100644 app/PaymentDrivers/Stripe/GIROPAY.php create mode 100644 resources/js/clients/payments/stripe-giropay.js diff --git a/app/Models/Gateway.php b/app/Models/Gateway.php index 8bd0b64eb09f..5c34d4cf057b 100644 --- a/app/Models/Gateway.php +++ b/app/Models/Gateway.php @@ -104,7 +104,8 @@ class Gateway extends StaticModel GatewayType::ALIPAY => ['refund' => false, 'token_billing' => false], GatewayType::APPLE_PAY => ['refund' => false, 'token_billing' => false], GatewayType::SOFORT => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded']], //Stripe - GatewayType::SEPA => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded']]]; + GatewayType::SEPA => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded']], + GatewayType::GIROPAY => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded']]]; case 39: return [GatewayType::CREDIT_CARD => ['refund' => true, 'token_billing' => true]]; //Checkout break; diff --git a/app/Models/GatewayType.php b/app/Models/GatewayType.php index 7a77a56e5142..6397cef7fc6e 100644 --- a/app/Models/GatewayType.php +++ b/app/Models/GatewayType.php @@ -29,7 +29,8 @@ class GatewayType extends StaticModel const BANCONTACT = 12; const IDEAL = 13; const HOSTED_PAGE = 14; // For gateways that contain multiple methods. - + const GIROPAY = 15; + public function gateway() { return $this->belongsTo(Gateway::class); @@ -69,6 +70,8 @@ class GatewayType extends StaticModel return ctrans('texts.ideal'); case self::HOSTED_PAGE: return ctrans('texts.aio_checkout'); + case self::GIROPAY: + return ctrans('texts.giropay'); default: return 'Undefined.'; break; diff --git a/app/PaymentDrivers/Stripe/GIROPAY.php b/app/PaymentDrivers/Stripe/GIROPAY.php new file mode 100644 index 000000000000..ceb2b9f09ca8 --- /dev/null +++ b/app/PaymentDrivers/Stripe/GIROPAY.php @@ -0,0 +1,141 @@ +stripe = $stripe; + } + + public function authorizeView($data) + { + return render('gateways.stripe.giropay.authorize', $data); + } + + public function paymentView(array $data) + { + $data['gateway'] = $this->stripe; + $data['return_url'] = $this->buildReturnUrl(); + $data['stripe_amount'] = $this->stripe->convertToStripeAmount($data['total']['amount_with_fee'], $this->stripe->client->currency()->precision, $this->stripe->client->currency()); + $data['client'] = $this->stripe->client; + $data['customer'] = $this->stripe->findOrCreateCustomer()->id; + $data['country'] = $this->stripe->client->country->iso_3166_2; + + $intent = \Stripe\PaymentIntent::create([ + 'amount' => $data['stripe_amount'], + 'currency' => 'eur', + 'payment_method_types' => ['giropay'], + 'customer' => $this->stripe->findOrCreateCustomer(), + 'description' => $this->stripe->decodeUnicodeString(ctrans('texts.invoices') . ': ' . collect($data['invoices'])->pluck('invoice_number')), + + ]); + + $data['pi_client_secret'] = $intent->client_secret; + + $this->stripe->payment_hash->data = array_merge((array) $this->stripe->payment_hash->data, ['stripe_amount' => $data['stripe_amount']]); + $this->stripe->payment_hash->save(); + + return render('gateways.stripe.giropay.pay', $data); + } + + private function buildReturnUrl(): string + { + return route('client.payments.response', [ + 'company_gateway_id' => $this->stripe->company_gateway->id, + 'payment_hash' => $this->stripe->payment_hash->hash, + 'payment_method_id' => GatewayType::GIROPAY, + ]); + } + + public function paymentResponse($request) + { + $this->stripe->payment_hash->data = array_merge((array) $this->stripe->payment_hash->data, $request->all()); + $this->stripe->payment_hash->save(); + + if ($request->redirect_status == 'succeeded') { + return $this->processSuccessfulPayment($request->payment_intent); + } + + return $this->processUnsuccessfulPayment(); + } + + public function processSuccessfulPayment(string $payment_intent) + { + /* @todo: https://github.com/invoiceninja/invoiceninja/pull/3789/files#r436175798 */ + + $this->stripe->init(); + + $data = [ + 'payment_method' => $payment_intent, + 'payment_type' => PaymentType::GIROPAY, + 'amount' => $this->stripe->convertFromStripeAmount($this->stripe->payment_hash->data->stripe_amount, $this->stripe->client->currency()->precision, $this->stripe->client->currency()), + 'transaction_reference' => $payment_intent, + 'gateway_type_id' => GatewayType::GIROPAY, + ]; + + $this->stripe->createPayment($data, Payment::STATUS_PENDING); + + SystemLogger::dispatch( + ['response' => $this->stripe->payment_hash->data, 'data' => $data], + SystemLog::CATEGORY_GATEWAY_RESPONSE, + SystemLog::EVENT_GATEWAY_SUCCESS, + SystemLog::TYPE_STRIPE, + $this->stripe->client, + $this->stripe->client->company, + ); + + return redirect()->route('client.payments.index'); + } + + public function processUnsuccessfulPayment() + { + $server_response = $this->stripe->payment_hash->data; + + PaymentFailureMailer::dispatch( + $this->stripe->client, + $server_response, + $this->stripe->client->company, + $this->stripe->convertFromStripeAmount($this->stripe->payment_hash->data->stripe_amount, $this->stripe->client->currency()->precision, $this->stripe->client->currency()) + ); + + $message = [ + 'server_response' => $server_response, + 'data' => $this->stripe->payment_hash->data, + ]; + + SystemLogger::dispatch( + $message, + SystemLog::CATEGORY_GATEWAY_RESPONSE, + SystemLog::EVENT_GATEWAY_FAILURE, + SystemLog::TYPE_STRIPE, + $this->stripe->client, + $this->stripe->client->company, + ); + + throw new PaymentFailed('Failed to process the payment.', 500); + } +} diff --git a/app/PaymentDrivers/StripePaymentDriver.php b/app/PaymentDrivers/StripePaymentDriver.php index 604a4a89c5ba..ac71bad6bbab 100644 --- a/app/PaymentDrivers/StripePaymentDriver.php +++ b/app/PaymentDrivers/StripePaymentDriver.php @@ -33,6 +33,7 @@ use App\PaymentDrivers\Stripe\CreditCard; use App\PaymentDrivers\Stripe\ImportCustomers; use App\PaymentDrivers\Stripe\SOFORT; use App\PaymentDrivers\Stripe\SEPA; +use App\PaymentDrivers\Stripe\GIROPAY; use App\PaymentDrivers\Stripe\UpdatePaymentMethods; use App\PaymentDrivers\Stripe\Utilities; use App\Utils\Traits\MakesHash; @@ -77,6 +78,7 @@ class StripePaymentDriver extends BaseDriver GatewayType::SOFORT => SOFORT::class, GatewayType::APPLE_PAY => ApplePay::class, GatewayType::SEPA => SEPA::class, + GatewayType::GIROPAY => GIROPAY::class, ]; const SYSTEM_LOG_TYPE = SystemLog::TYPE_STRIPE; @@ -153,6 +155,11 @@ class StripePaymentDriver extends BaseDriver $types[] = GatewayType::SEPA; } + if ($this -> client + && isset($this->client->country) + && in_array($this->client->country->iso_3166_3, ["DEU"])) + $types[] = GatewayType::GIROPAY; + return $types; } diff --git a/resources/js/clients/payments/stripe-giropay.js b/resources/js/clients/payments/stripe-giropay.js new file mode 100644 index 000000000000..141a83acdebd --- /dev/null +++ b/resources/js/clients/payments/stripe-giropay.js @@ -0,0 +1,57 @@ +/** + * Invoice Ninja (https://invoiceninja.com) + * + * @link https://github.com/invoiceninja/invoiceninja source repository + * + * @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com) + * + * @license https://opensource.org/licenses/AAL + */ + +class ProcessGiroPay { + constructor(key, stripeConnect) { + this.key = key; + this.errors = document.getElementById('errors'); + this.stripeConnect = stripeConnect; + } + + setupStripe = () => { + this.stripe = Stripe(this.key); + + if(this.stripeConnect) + this.stripe.stripeAccount = stripeConnect; + + return this; + }; + + handle = () => { + document.getElementById('pay-now').addEventListener('click', (e) => { + document.getElementById('pay-now').disabled = true; + document.querySelector('#pay-now > svg').classList.remove('hidden'); + document.querySelector('#pay-now > span').classList.add('hidden'); + + this.stripe.confirmGiropayPayment( + document.querySelector('meta[name=pi-client-secret').content, + { + payment_method: { + billing_details: { + name: "", + }, + }, + return_url: document.querySelector( + 'meta[name="return-url"]' + ).content, + } + ); + }); + }; +} + +const publishableKey = document.querySelector( + 'meta[name="stripe-publishable-key"]' +)?.content ?? ''; + +const stripeConnect = + document.querySelector('meta[name="stripe-account-id"]')?.content ?? ''; + +new ProcessGiroPay(publishableKey, stripeConnect).setupStripe().handle();