diff --git a/app/Models/Gateway.php b/app/Models/Gateway.php index 5d5a5578b920..8bd0b64eb09f 100644 --- a/app/Models/Gateway.php +++ b/app/Models/Gateway.php @@ -103,7 +103,8 @@ class Gateway extends StaticModel 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']]]; //Stripe + 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']]]; case 39: return [GatewayType::CREDIT_CARD => ['refund' => true, 'token_billing' => true]]; //Checkout break; diff --git a/app/PaymentDrivers/Stripe/SEPA.php b/app/PaymentDrivers/Stripe/SEPA.php index 74b564f6ed14..cba8c6bdccec 100644 --- a/app/PaymentDrivers/Stripe/SEPA.php +++ b/app/PaymentDrivers/Stripe/SEPA.php @@ -12,98 +12,129 @@ namespace App\PaymentDrivers\Stripe; -use App\Exceptions\PaymentFailed; use App\Http\Requests\ClientPortal\Payments\PaymentResponseRequest; +use App\PaymentDrivers\StripePaymentDriver; use App\Jobs\Mail\PaymentFailureMailer; use App\Jobs\Util\SystemLogger; use App\Models\GatewayType; use App\Models\Payment; use App\Models\PaymentType; use App\Models\SystemLog; -use App\PaymentDrivers\StripePaymentDriver; -use App\PaymentDrivers\Stripe\CreditCard; -use App\Utils\Ninja; +use App\Exceptions\PaymentFailed; class SEPA { /** @var StripePaymentDriver */ - public $stripe_driver; + public StripePaymentDriver $stripe; - public function __construct(StripePaymentDriver $stripe_driver) + public function __construct(StripePaymentDriver $stripe) { - $this->stripe_driver = $stripe_driver; + $this->stripe = $stripe; } - public function authorizeView(array $data) + public function authorizeView($data) { - $customer = $this->stripe_driver->findOrCreateCustomer(); - - $setup_intent = \Stripe\SetupIntent::create([ - 'payment_method_types' => ['sepa_debit'], - 'customer' => $customer->id, - ], $this->stripe_driver->stripe_connect_auth); - - $client_secret = $setup_intent->client_secret; - // Pass the client secret to the client - + return render('gateways.stripe.sepa.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; - return render('gateways.stripe.sepa.authorize', array_merge($data)); + $intent = \Stripe\PaymentIntent::create([ + 'amount' => $data['stripe_amount'], + 'currency' => 'eur', + 'payment_method_types' => ['sepa_debit'], + 'setup_future_usage' => 'off_session', + '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.sepa.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::SEPA, + ]); + } public function paymentResponse(PaymentResponseRequest $request) { + $this->stripe->payment_hash->data = array_merge((array) $this->stripe->payment_hash->data, $request->all()); + $this->stripe->payment_hash->save(); - // $this->stripe_driver->init(); - - // $state = [ - // 'server_response' => json_decode($request->gateway_response), - // 'payment_hash' => $request->payment_hash, - // ]; - - // $state['payment_intent'] = \Stripe\PaymentIntent::retrieve($state['server_response']->id, $this->stripe_driver->stripe_connect_auth); - - // $state['customer'] = $state['payment_intent']->customer; - - // $this->stripe_driver->payment_hash->data = array_merge((array) $this->stripe_driver->payment_hash->data, $state); - // $this->stripe_driver->payment_hash->save(); - - // $server_response = $this->stripe_driver->payment_hash->data->server_response; - - // $response_handler = new CreditCard($this->stripe_driver); - - // if ($server_response->status == 'succeeded') { - - // $this->stripe_driver->logSuccessfulGatewayResponse(['response' => json_decode($request->gateway_response), 'data' => $this->stripe_driver->payment_hash], SystemLog::TYPE_STRIPE); - - // return $response_handler->processSuccessfulPayment(); - // } - - // return $response_handler->processUnsuccessfulPayment($server_response); - + if ($request->redirect_status == 'succeeded') { + return $this->processSuccessfulPayment($request->payment_intent); + } + return $this->processUnsuccessfulPayment(); } - /* Searches for a stripe customer by email - otherwise searches by gateway tokens in StripePaymentdriver - finally creates a new customer if none found - */ - private function getCustomer() + public function processSuccessfulPayment(string $payment_intent) { - $searchResults = \Stripe\Customer::all([ - "email" => $this->stripe_driver->client->present()->email(), - "limit" => 1, - "starting_after" => null - ], $this->stripe_driver->stripe_connect_auth); - + $this->stripe->init(); - if(count($searchResults) >= 1) - return $searchResults[0]; + $data = [ + 'payment_method' => $payment_intent, + 'payment_type' => PaymentType::SEPA, + '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::SEPA, + ]; - return $this->stripe_driver->findOrCreateCustomer(); + $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/StripeConnectPaymentDriver.php b/app/PaymentDrivers/StripeConnectPaymentDriver.php index 2a64438271bb..9199391185a2 100644 --- a/app/PaymentDrivers/StripeConnectPaymentDriver.php +++ b/app/PaymentDrivers/StripeConnectPaymentDriver.php @@ -28,6 +28,7 @@ use App\PaymentDrivers\Stripe\Alipay; use App\PaymentDrivers\Stripe\Charge; use App\PaymentDrivers\Stripe\CreditCard; use App\PaymentDrivers\Stripe\SOFORT; +use App\PaymentDrivers\Stripe\SEPA; use App\PaymentDrivers\Stripe\Utilities; use App\Utils\Traits\MakesHash; use Exception; diff --git a/app/PaymentDrivers/StripePaymentDriver.php b/app/PaymentDrivers/StripePaymentDriver.php index 62537689beb7..604a4a89c5ba 100644 --- a/app/PaymentDrivers/StripePaymentDriver.php +++ b/app/PaymentDrivers/StripePaymentDriver.php @@ -32,6 +32,7 @@ use App\PaymentDrivers\Stripe\Connect\Verify; use App\PaymentDrivers\Stripe\CreditCard; use App\PaymentDrivers\Stripe\ImportCustomers; use App\PaymentDrivers\Stripe\SOFORT; +use App\PaymentDrivers\Stripe\SEPA; use App\PaymentDrivers\Stripe\UpdatePaymentMethods; use App\PaymentDrivers\Stripe\Utilities; use App\Utils\Traits\MakesHash; @@ -75,7 +76,7 @@ class StripePaymentDriver extends BaseDriver GatewayType::ALIPAY => Alipay::class, GatewayType::SOFORT => SOFORT::class, GatewayType::APPLE_PAY => ApplePay::class, - GatewayType::SEPA => 1, // TODO + GatewayType::SEPA => SEPA::class, ]; const SYSTEM_LOG_TYPE = SystemLog::TYPE_STRIPE; @@ -125,7 +126,7 @@ class StripePaymentDriver extends BaseDriver $types = [ // GatewayType::CRYPTO, GatewayType::CREDIT_CARD - ]; + ]; if ($this->client && isset($this->client->country) @@ -146,6 +147,12 @@ class StripePaymentDriver extends BaseDriver $types[] = GatewayType::ALIPAY; } + if ($this->client + && isset($this->client->country) + && in_array($this->client->country->iso_3166_3, ['AUS', 'DNK', 'DEU', 'ITA', 'LUX', 'NOR', 'SVN', 'GBR', 'EST', 'GRC', 'JPN', 'PRT', 'ESP', 'USA', 'BEL', 'FIN'])) { // TODO: More has to be added https://stripe.com/docs/payments/sepa-debit + $types[] = GatewayType::SEPA; + } + return $types; } @@ -326,7 +333,7 @@ class StripePaymentDriver extends BaseDriver if($customer) return $customer; - } + } //Search by email $searchResults = \Stripe\Customer::all([ @@ -337,11 +344,11 @@ class StripePaymentDriver extends BaseDriver if(count($searchResults) == 1) return $searchResults->data[0]; - + //Else create a new record $data['name'] = $this->client->present()->name(); $data['phone'] = $this->client->present()->phone(); - + if (filter_var($this->client->present()->email(), FILTER_VALIDATE_EMAIL)) { $data['email'] = $this->client->present()->email(); } @@ -370,7 +377,7 @@ class StripePaymentDriver extends BaseDriver // ->create(['charge' => $payment->transaction_reference, 'amount' => $this->convertToStripeAmount($amount, $this->client->currency()->precision, $this->client->currency())], $meta); $response = \Stripe\Refund::create([ - 'charge' => $payment->transaction_reference, + 'charge' => $payment->transaction_reference, 'amount' => $this->convertToStripeAmount($amount, $this->client->currency()->precision, $this->client->currency()) ], $meta); diff --git a/public/js/clients/payments/stripe-sepa.js b/public/js/clients/payments/stripe-sepa.js new file mode 100644 index 000000000000..1fdf03776a70 --- /dev/null +++ b/public/js/clients/payments/stripe-sepa.js @@ -0,0 +1,91 @@ +/** + * 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 ProcessSEPA { + 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; + const elements = this.stripe.elements(); + var style = { + base: { + color: "#32325d", + fontFamily: + '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif', + fontSmoothing: "antialiased", + fontSize: "16px", + "::placeholder": { + color: "#aab7c4" + }, + ":-webkit-autofill": { + color: "#32325d" + } + }, + invalid: { + color: "#fa755a", + iconColor: "#fa755a", + ":-webkit-autofill": { + color: "#fa755a" + } + } + }; + var options = { + style: style, + supportedCountries: ["SEPA"], + // If you know the country of the customer, you can optionally pass it to + // the Element as placeholderCountry. The example IBAN that is being used + // as placeholder reflects the IBAN format of that country. + placeholderCountry: "DE" + }; + this.iban = elements.create("iban", options); + this.iban.mount("#sepa-iban"); + 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.confirmSepaDebitPayment( + document.querySelector('meta[name=pi-client-secret').content, + { + payment_method: { + sepa_debit: this.iban, + billing_details: { + name: document.getElementById("sepa-name").value, + email: document.getElementById("sepa-email-address").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 ProcessSEPA(publishableKey, stripeConnect).setupStripe().handle(); diff --git a/public/js/clients/payments/stripe-sepa.js.LICENSE.txt b/public/js/clients/payments/stripe-sepa.js.LICENSE.txt new file mode 100644 index 000000000000..585c6ab0e4fc --- /dev/null +++ b/public/js/clients/payments/stripe-sepa.js.LICENSE.txt @@ -0,0 +1,9 @@ +/** + * 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 + */ diff --git a/resources/js/clients/payments/stripe-sepa.js b/resources/js/clients/payments/stripe-sepa.js new file mode 100644 index 000000000000..5a216d81c2f6 --- /dev/null +++ b/resources/js/clients/payments/stripe-sepa.js @@ -0,0 +1,91 @@ +/** + * 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 ProcessSEPA { + 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; + const elements = this.stripe.elements(); + var style = { + base: { + color: "#32325d", + fontFamily: + '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif', + fontSmoothing: "antialiased", + fontSize: "16px", + "::placeholder": { + color: "#aab7c4" + }, + ":-webkit-autofill": { + color: "#32325d" + } + }, + invalid: { + color: "#fa755a", + iconColor: "#fa755a", + ":-webkit-autofill": { + color: "#fa755a" + } + } + }; + var options = { + style: style, + supportedCountries: ["SEPA"], + // If you know the country of the customer, you can optionally pass it to + // the Element as placeholderCountry. The example IBAN that is being used + // as placeholder reflects the IBAN format of that country. + placeholderCountry: "DE" + }; + this.iban = elements.create("iban", options); + this.iban.mount("#sepa-iban"); + 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.confirmSepaDebitPayment( + document.querySelector('meta[name=pi-client-secret').content, + { + payment_method: { + sepa_debit: this.iban, + billing_details: { + name: document.getElementById("sepa-name").value, + email: document.getElementById("sepa-email-address").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 ProcessSEPA(publishableKey, stripeConnect).setupStripe().handle(); diff --git a/resources/lang/en/texts.php b/resources/lang/en/texts.php index a79ec3208054..7611693792fe 100644 --- a/resources/lang/en/texts.php +++ b/resources/lang/en/texts.php @@ -1779,7 +1779,7 @@ $LANG = array( 'lang_Bulgarian' => 'Bulgarian', 'lang_Russian (Russia)' => 'Russian (Russia)', - + // Industries 'industry_Accounting & Legal' => 'Accounting & Legal', 'industry_Advertising' => 'Advertising', @@ -4316,7 +4316,9 @@ $LANG = array( 'payment_method_cannot_be_preauthorized' => 'This payment method cannot be preauthorized.', 'kbc_cbc' => 'KBC/CBC', 'bancontact' => 'Bancontact', + 'sepa_mandat' => 'By providing your IBAN and confirming this payment, you are authorizing Rocketship Inc. and Stripe, our payment service provider, to send instructions to your bank to debit your account and your bank to debit your account in accordance with those instructions. You are entitled to a refund from your bank under the terms and conditions of your agreement with your bank. A refund must be claimed within 8 weeks starting from the date on which your account was debited.', 'ideal' => 'iDEAL', + 'bank_account_holder' => 'Bank Account Holder', 'aio_checkout' => 'All-in-one checkout', ); diff --git a/resources/views/portal/ninja2020/gateways/stripe/sepa/authorize.blade.php b/resources/views/portal/ninja2020/gateways/stripe/sepa/authorize.blade.php index def2150fb30d..539346205bb3 100644 --- a/resources/views/portal/ninja2020/gateways/stripe/sepa/authorize.blade.php +++ b/resources/views/portal/ninja2020/gateways/stripe/sepa/authorize.blade.php @@ -1,4 +1,4 @@ -@extends('portal.ninja2020.layout.payments', ['gateway_title' => 'SEPA', 'card_title' => 'SEPA']) +@extends('portal.ninja2020.layout.payments', ['gateway_title' => 'SEPA', 'card_title' => 'SEPA-Lastschrift']) @section('gateway_head') @if($gateway->company_gateway->getConfigField('account_id')) @@ -10,9 +10,9 @@ @endsection @section('gateway_content') - @if(session()->has('ach_error')) + @if(session()->has('sepa_error'))
-

{{ session('ach_error') }}

+

{{ session('sepa_error') }}

@endif @@ -78,5 +78,5 @@ @section('gateway_footer') - + @endsection diff --git a/resources/views/portal/ninja2020/gateways/stripe/sepa/pay.blade.php b/resources/views/portal/ninja2020/gateways/stripe/sepa/pay.blade.php new file mode 100644 index 000000000000..508ccf57aaa9 --- /dev/null +++ b/resources/views/portal/ninja2020/gateways/stripe/sepa/pay.blade.php @@ -0,0 +1,30 @@ +@extends('portal.ninja2020.layout.payments', ['gateway_title' => 'SEPA', 'card_title' => 'SEPA']) + +@section('gateway_head') + + + + + + + +@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.sepa') }} ({{ ctrans('texts.bank_transfer') }}) + @endcomponent + + @include('portal.ninja2020.gateways.stripe.sepa.sepa_debit') + + @include('portal.ninja2020.gateways.includes.pay_now') +@endsection + +@push('footer') + + +@endpush diff --git a/resources/views/portal/ninja2020/gateways/stripe/sepa/sepa_debit.blade.php b/resources/views/portal/ninja2020/gateways/stripe/sepa/sepa_debit.blade.php new file mode 100644 index 000000000000..126c5b48bd87 --- /dev/null +++ b/resources/views/portal/ninja2020/gateways/stripe/sepa/sepa_debit.blade.php @@ -0,0 +1,19 @@ +
+ @component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.name')]) + + + +
+ + +
+ @endcomponent +
diff --git a/resources/views/portal/ninja2020/gateways/stripe/sofort/pay.blade.php b/resources/views/portal/ninja2020/gateways/stripe/sofort/pay.blade.php index 9fcec3e1c957..85c223516e75 100644 --- a/resources/views/portal/ninja2020/gateways/stripe/sofort/pay.blade.php +++ b/resources/views/portal/ninja2020/gateways/stripe/sofort/pay.blade.php @@ -18,7 +18,6 @@ @component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.payment_type')]) {{ ctrans('texts.sofort') }} ({{ ctrans('texts.bank_transfer') }}) @endcomponent - @include('portal.ninja2020.gateways.includes.pay_now') @endsection