diff --git a/app/Models/Gateway.php b/app/Models/Gateway.php index 1fc248c0b61e..47f35f9c9863 100644 --- a/app/Models/Gateway.php +++ b/app/Models/Gateway.php @@ -134,13 +134,14 @@ class Gateway extends StaticModel GatewayType::BANK_TRANSFER => ['refund' => true, 'token_billing' => true, 'webhooks' => [' ']], ]; break; - case 56: + case 56: //Stripe return [ GatewayType::CREDIT_CARD => ['refund' => true, 'token_billing' => true, 'webhooks' => ['payment_intent.succeeded']], GatewayType::BANK_TRANSFER => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded']], 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', 'payment_intent.succeeded']], //Stripe + GatewayType::SOFORT => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'payment_intent.succeeded']], + GatewayType::KLARNA => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'payment_intent.succeeded']], GatewayType::SEPA => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'payment_intent.succeeded']], GatewayType::PRZELEWY24 => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'payment_intent.succeeded']], GatewayType::GIROPAY => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'payment_intent.succeeded']], diff --git a/app/Models/GatewayType.php b/app/Models/GatewayType.php index a3e1bc4fd2e6..9802edae51fa 100644 --- a/app/Models/GatewayType.php +++ b/app/Models/GatewayType.php @@ -61,6 +61,8 @@ class GatewayType extends StaticModel const FPX = 22; + const KLARNA = 23; + public function gateway() { return $this->belongsTo(Gateway::class); @@ -116,6 +118,8 @@ class GatewayType extends StaticModel return ctrans('texts.payment_type_instant_bank_pay'); case self::FPX: return ctrans('texts.fpx'); + case self::KLARNA: + return ctrans('texts.klarna'); default: return ' '; break; diff --git a/app/Models/PaymentType.php b/app/Models/PaymentType.php index 22eb885b787d..21b165014c65 100644 --- a/app/Models/PaymentType.php +++ b/app/Models/PaymentType.php @@ -55,6 +55,7 @@ class PaymentType extends StaticModel const ACSS = 44; const INSTANT_BANK_PAY = 45; const FPX = 46; + const KLARNA = 47; public static function parseCardType($cardName) { diff --git a/app/PaymentDrivers/Stripe/Klarna.php b/app/PaymentDrivers/Stripe/Klarna.php new file mode 100644 index 000000000000..d3e2678d215c --- /dev/null +++ b/app/PaymentDrivers/Stripe/Klarna.php @@ -0,0 +1,154 @@ +stripe = $stripe; + } + + public function authorizeView($data) + { + return render('gateways.stripe.klarna.authorize', $data); + } + + public function paymentView(array $data) + { + $this->stripe->init(); + + $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; + + $amount = $data['total']['amount_with_fee']; + + $invoice_numbers = collect($data['invoices'])->pluck('invoice_number'); + + if ($invoice_numbers > 0) { + $description = ctrans('texts.payment_provider_paymenttext', ['invoicenumber' => $invoice_numbers->implode(', '), 'amount' => Number::formatMoney($amount, $this->stripe->client), 'client' => $this->stripe->client->present()->name()]); + } else { + $description = ctrans('texts.payment_prvoder_paymenttext_without_invoice', ['amount' => Number::formatMoney($amount, $this->stripe->client), 'client' => $this->stripe->client->present()->name()]); + } + + $intent = \Stripe\PaymentIntent::create([ + 'amount' => $data['stripe_amount'], + 'currency' => $this->stripe->client->getCurrencyCode(), + 'payment_method_types' => ['klarna'], + 'customer' => $this->stripe->findOrCreateCustomer(), + 'description' => $description, + 'metadata' => [ + 'payment_hash' => $this->stripe->payment_hash->hash, + 'gateway_type_id' => GatewayType::KLARNA, + ], + ], array_merge($this->stripe->stripe_connect_auth, ['idempotency_key' => uniqid("st",true)])); + + $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.klarna.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::KLARNA, + ]); + } + + 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 (in_array($request->redirect_status, ['succeeded','pending'])) { + return $this->processSuccessfulPayment($request->payment_intent); + } + + return $this->processUnsuccessfulPayment(); + } + + public function processSuccessfulPayment(string $payment_intent) + { + + $this->stripe->init(); + + //catch duplicate submissions. + if (Payment::where('transaction_reference', $payment_intent)->exists()) { + return redirect()->route('client.payments.index'); + } + + $data = [ + 'payment_method' => $payment_intent, + 'payment_type' => PaymentType::KLARNA, + '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::KLARNA, + ]; + + $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; + + $this->stripe->sendFailureMail($server_response); + + $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(ctrans('texts.payment_provider_failed_process_payment'), 500); + } +} diff --git a/app/PaymentDrivers/StripePaymentDriver.php b/app/PaymentDrivers/StripePaymentDriver.php index 178ce44bbb59..e6fa402b3a00 100644 --- a/app/PaymentDrivers/StripePaymentDriver.php +++ b/app/PaymentDrivers/StripePaymentDriver.php @@ -37,6 +37,7 @@ use App\PaymentDrivers\Stripe\CreditCard; use App\PaymentDrivers\Stripe\EPS; use App\PaymentDrivers\Stripe\FPX; use App\PaymentDrivers\Stripe\GIROPAY; +use App\PaymentDrivers\Stripe\Klarna; use App\PaymentDrivers\Stripe\iDeal; use App\PaymentDrivers\Stripe\ImportCustomers; use App\PaymentDrivers\Stripe\Jobs\PaymentIntentFailureWebhook; @@ -97,6 +98,7 @@ class StripePaymentDriver extends BaseDriver GatewayType::BECS => BECS::class, GatewayType::ACSS => ACSS::class, GatewayType::FPX => FPX::class, + GatewayType::KLARNA => KLARNA::class, ]; const SYSTEM_LOG_TYPE = SystemLog::TYPE_STRIPE; @@ -236,6 +238,13 @@ class StripePaymentDriver extends BaseDriver && in_array($this->client->country->iso_3166_3, ['CAN', 'USA'])) { $types[] = GatewayType::ACSS; } + if ($this->client + && $this->client->currency() + && in_array($this->client->currency()->code, ['EUR', 'DKK', 'GBP', 'NOK', 'SEK', 'USD', 'AUD', 'NZD', 'CAD', 'PLN', 'CHF']) + && isset($this->client->country) + && in_array($this->client->country->iso_3166_3, ['AUT','BEL','DNK','FIN','FRA','DEU','IRL','ITA','NLD','NOR','ESP','SWE','GBR','USA'])) { + $types[] = GatewayType::KLARNA; + } if ( $this->client @@ -274,6 +283,9 @@ class StripePaymentDriver extends BaseDriver case GatewayType::GIROPAY: return 'gateways.stripe.giropay'; break; + case GatewayType::KLARNA: + return 'gateways.stripe.klarna'; + break; case GatewayType::IDEAL: return 'gateways.stripe.ideal'; case GatewayType::EPS: diff --git a/database/migrations/2022_05_12_56879_add_stripe_klarna.php b/database/migrations/2022_05_12_56879_add_stripe_klarna.php new file mode 100644 index 000000000000..969553634d52 --- /dev/null +++ b/database/migrations/2022_05_12_56879_add_stripe_klarna.php @@ -0,0 +1,34 @@ +id = 47; + $type->name = 'Klarna'; + $type->gateway_type_id = GatewayType::KLARNA; + + $type->save(); + }); + $type = new GatewayType(); + + $type->id = 23; + $type->alias = 'klarna'; + $type->name = 'Klarna'; + + $type->save(); + } +}; diff --git a/lang/en/texts.php b/lang/en/texts.php index c55a6ec412d3..e9ff188f2082 100644 --- a/lang/en/texts.php +++ b/lang/en/texts.php @@ -4202,7 +4202,6 @@ $LANG = array( 'count_minutes' => ':count Minutes', 'password_timeout' => 'Password Timeout', 'shared_invoice_credit_counter' => 'Share Invoice/Credit Counter', - 'activity_80' => ':user created subscription :subscription', 'activity_81' => ':user updated subscription :subscription', 'activity_82' => ':user archived subscription :subscription', @@ -4210,7 +4209,6 @@ $LANG = array( 'activity_84' => ':user restored subscription :subscription', 'amount_greater_than_balance_v5' => 'The amount is greater than the invoice balance. You cannot overpay an invoice.', 'click_to_continue' => 'Click to continue', - 'notification_invoice_created_body' => 'The following invoice :invoice was created for client :client for :amount.', 'notification_invoice_created_subject' => 'Invoice :invoice was created for :client', 'notification_quote_created_body' => 'The following quote :invoice was created for client :client for :amount.', @@ -4296,6 +4294,7 @@ $LANG = array( 'przelewy24_accept' => 'I declare that I have familiarized myself with the regulations and information obligation of the Przelewy24 service.', 'giropay' => 'GiroPay', 'giropay_law' => 'By entering your Customer information (such as name, sort code and account number) you (the Customer) agree that this information is given voluntarily.', + 'klarna' => 'Klarna', 'eps' => 'EPS', 'becs' => 'BECS Direct Debit', 'becs_mandate' => 'By providing your bank account details, you agree to this Direct Debit Request and the Direct Debit Request service agreement, and authorise Stripe Payments Australia Pty Ltd ACN 160 180 343 Direct Debit User ID number 507156 (“Stripe”) to debit your account through the Bulk Electronic Clearing System (BECS) on behalf of :company (the “Merchant”) for any amounts separately communicated to you by the Merchant. You certify that you are either an account holder or an authorised signatory on the account listed above.', @@ -4780,7 +4779,7 @@ $LANG = array( 'invoice_task_project_help' => 'Add the project to the invoice line items', 'bulk_action' => 'Bulk Action', 'phone_validation_error' => 'This mobile/cell phone number is not valid, please enter in E.164 format', - 'transaction' => 'Transaction', + 'transaction' => 'Transaction', 'disable_2fa' => 'Disable 2FA', 'change_number' => 'Change Number', 'resend_code' => 'Resend Code', diff --git a/resources/js/clients/payments/stripe-klarna.js b/resources/js/clients/payments/stripe-klarna.js new file mode 100644 index 000000000000..dfaa63092919 --- /dev/null +++ b/resources/js/clients/payments/stripe-klarna.js @@ -0,0 +1,68 @@ +/** + * 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://www.elastic.co/licensing/elastic-license + */ + +class ProcessKlarna { + constructor(key, stripeConnect) { + this.key = key; + this.errors = document.getElementById('errors'); + this.stripeConnect = stripeConnect; + } + + setupStripe = () => { + + if (this.stripeConnect){ + // this.stripe.stripeAccount = this.stripeConnect; + + this.stripe = Stripe(this.key, { + stripeAccount: this.stripeConnect, + }); + + } + else { + this.stripe = Stripe(this.key); + } + + + return this; + }; + + handle = () => { + document.getElementById('pay-now').addEventListener('click', (e) => { + let errors = document.getElementById('errors'); + + document.getElementById('pay-now').disabled = true; + document.querySelector('#pay-now > svg').classList.remove('hidden'); + document.querySelector('#pay-now > span').classList.add('hidden'); + + this.stripe.confirmKlarnaPayment( + document.querySelector('meta[name=pi-client-secret').content, + { + payment_method: { + billing_details: { + name: document.getElementById("giropay-name").value, + }, + }, + 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 ProcessKlarna(publishableKey, stripeConnect).setupStripe().handle(); diff --git a/resources/views/portal/ninja2020/gateways/stripe/klarna/authorize.blade.php b/resources/views/portal/ninja2020/gateways/stripe/klarna/authorize.blade.php new file mode 100644 index 000000000000..ceb2d28000d5 --- /dev/null +++ b/resources/views/portal/ninja2020/gateways/stripe/klarna/authorize.blade.php @@ -0,0 +1,7 @@ +@extends('portal.ninja2020.layout.payments', ['gateway_title' => ctrans('texts.bank_account'), 'card_title' => ctrans('texts.bank_account')]) + +@section('gateway_content') + @component('portal.ninja2020.components.general.card-element-single', ['title' => ctrans('texts.bank_account'), 'show_title' => false]) + {{ __('texts.sofort_authorize_label') }} + @endcomponent +@endsection diff --git a/resources/views/portal/ninja2020/gateways/stripe/klarna/pay.blade.php b/resources/views/portal/ninja2020/gateways/stripe/klarna/pay.blade.php new file mode 100644 index 000000000000..54989216165f --- /dev/null +++ b/resources/views/portal/ninja2020/gateways/stripe/klarna/pay.blade.php @@ -0,0 +1,31 @@ +@extends('portal.ninja2020.layout.payments', ['gateway_title' => 'Klarna', 'card_title' => 'Klarna']) + +@section('gateway_head') + @if($gateway->company_gateway->getConfigField('account_id')) + + + @else + + @endif + + + + + +@endsection + +@section('gateway_content') + + + @include('portal.ninja2020.gateways.includes.payment_details') + + @component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.payment_type')]) + {{ ctrans('texts.klarna') }} ({{ ctrans('texts.bank_transfer') }}) + @endcomponent + @include('portal.ninja2020.gateways.includes.pay_now') +@endsection + +@push('footer') + + +@endpush diff --git a/webpack.mix.js b/webpack.mix.js index 2ed78e49227c..df0604426709 100644 --- a/webpack.mix.js +++ b/webpack.mix.js @@ -22,6 +22,10 @@ mix.js("resources/js/app.js", "public/js") "resources/js/clients/payments/stripe-ach.js", "public/js/clients/payments/stripe-ach.js" ) + .js( + "resources/js/clients/payments/stripe-klarna.js", + "public/js/clients/payments/stripe-klarna.js" + ) .js( "resources/js/clients/invoices/action-selectors.js", "public/js/clients/invoices/action-selectors.js"