diff --git a/app/PaymentDrivers/Stripe/SEPA.php b/app/PaymentDrivers/Stripe/SEPA.php index 7f36e424749e..a20a61f290c5 100644 --- a/app/PaymentDrivers/Stripe/SEPA.php +++ b/app/PaymentDrivers/Stripe/SEPA.php @@ -11,15 +11,15 @@ 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\Exceptions\PaymentFailed; +use App\PaymentDrivers\StripePaymentDriver; class SEPA { @@ -29,6 +29,8 @@ class SEPA public function __construct(StripePaymentDriver $stripe) { $this->stripe = $stripe; + + $this->stripe->init(); } public function authorizeView($data) @@ -36,7 +38,8 @@ class SEPA return render('gateways.stripe.sepa.authorize', $data); } - public function paymentView(array $data) { + public function paymentView(array $data) + { $data['gateway'] = $this->stripe; $data['payment_method_id'] = GatewayType::SEPA; $data['stripe_amount'] = $this->stripe->convertToStripeAmount($data['total']['amount_with_fee'], $this->stripe->client->currency()->precision, $this->stripe->client->currency()); @@ -52,11 +55,19 @@ class SEPA '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; + if (count($data['tokens']) > 0) { + $setup_intent = $this->stripe->stripe->setupIntents->create([ + 'payment_method_types' => ['sepa_debit'], + 'customer' => $this->stripe->findOrCreateCustomer()->id, + ]); + + $data['si_client_secret'] = $setup_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(); @@ -65,28 +76,24 @@ class SEPA public function paymentResponse(PaymentResponseRequest $request) { - $gateway_response = json_decode($request->gateway_response); $this->stripe->payment_hash->data = array_merge((array) $this->stripe->payment_hash->data, $request->all()); $this->stripe->payment_hash->save(); - if (property_exists($gateway_response, 'status') && $gateway_response->status == 'processing') { - - $this->stripe->init(); - $this->storePaymentMethod($gateway_response); + if (property_exists($gateway_response, 'status') && ($gateway_response->status == 'processing' || $gateway_response->status === 'succeeded')) { + if ($request->store_card) { + $this->storePaymentMethod($gateway_response); + } return $this->processSuccessfulPayment($gateway_response->id); } return $this->processUnsuccessfulPayment(); - } public function processSuccessfulPayment(string $payment_intent) { - $this->stripe->init(); - $data = [ 'payment_method' => $payment_intent, 'payment_type' => PaymentType::SEPA, @@ -95,7 +102,7 @@ class SEPA 'gateway_type_id' => GatewayType::SEPA, ]; - $this->stripe->createPayment($data, Payment::STATUS_PENDING); + $payment = $this->stripe->createPayment($data, Payment::STATUS_PENDING); SystemLogger::dispatch( ['response' => $this->stripe->payment_hash->data, 'data' => $data], @@ -106,7 +113,7 @@ class SEPA $this->stripe->client->company, ); - return redirect()->route('client.payments.index'); + return redirect()->route('client.payments.show', $payment->hashed_id); } public function processUnsuccessfulPayment() @@ -141,7 +148,6 @@ class SEPA private function storePaymentMethod($intent) { try { - $method = $this->stripe->getStripePaymentMethod($intent->payment_method); $payment_meta = new \stdClass; diff --git a/public/js/clients/payments/stripe-sepa.js b/public/js/clients/payments/stripe-sepa.js index ba48020c0647..68d075a28360 100644 --- a/public/js/clients/payments/stripe-sepa.js +++ b/public/js/clients/payments/stripe-sepa.js @@ -1,2 +1,2 @@ /*! For license information please see stripe-sepa.js.LICENSE.txt */ -!function(e){var t={};function n(o){if(t[o])return t[o].exports;var r=t[o]={i:o,l:!1,exports:{}};return e[o].call(r.exports,r,r.exports,n),r.l=!0,r.exports}n.m=e,n.c=t,n.d=function(e,t,o){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:o})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var o=Object.create(null);if(n.r(o),Object.defineProperty(o,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var r in e)n.d(o,r,function(t){return e[t]}.bind(null,r));return o},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="/",n(n.s=28)}({28:function(e,t,n){e.exports=n("guV3")},guV3:function(e,t){var n,o,r,a;function i(e,t){for(var n=0;n svg").classList.remove("hidden"),document.querySelector("#pay-now > span").classList.add("hidden"),void o.stripe.confirmSepaDebitPayment(document.querySelector("meta[name=pi-client-secret").content,{payment_method:{sepa_debit:o.iban,billing_details:{name:document.getElementById("sepa-name").value,email:document.getElementById("sepa-email-address").value}}}).then((function(e){return e.error?o.handleFailure(e.error.message):o.handleSuccess(e)}))):(t.textContent="Accept Terms",t.hidden=!1,void console.log("Terms"))}))})),this.key=t,this.errors=document.getElementById("errors"),this.stripeConnect=n}var t,n,o;return t=e,(n=[{key:"handleSuccess",value:function(e){document.querySelector('input[name="gateway_response"]').value=JSON.stringify(e.paymentIntent),document.getElementById("server-response").submit()}},{key:"handleFailure",value:function(e){var t=document.getElementById("errors");t.textContent="",t.textContent=e,t.hidden=!1,document.getElementById("pay-now").disabled=!1,document.querySelector("#pay-now > svg").classList.add("hidden"),document.querySelector("#pay-now > span").classList.remove("hidden")}}])&&i(t.prototype,n),o&&i(t,o),e}(),c=null!==(n=null===(o=document.querySelector('meta[name="stripe-publishable-key"]'))||void 0===o?void 0:o.content)&&void 0!==n?n:"",d=null!==(r=null===(a=document.querySelector('meta[name="stripe-account-id"]'))||void 0===a?void 0:a.content)&&void 0!==r?r:"";new l(c,d).setupStripe().handle()}}); \ No newline at end of file +!function(e){var t={};function n(o){if(t[o])return t[o].exports;var r=t[o]={i:o,l:!1,exports:{}};return e[o].call(r.exports,r,r.exports,n),r.l=!0,r.exports}n.m=e,n.c=t,n.d=function(e,t,o){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:o})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var o=Object.create(null);if(n.r(o),Object.defineProperty(o,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var r in e)n.d(o,r,function(t){return e[t]}.bind(null,r));return o},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="/",n(n.s=28)}({28:function(e,t,n){e.exports=n("guV3")},guV3:function(e,t){var n,o,r,a;function i(e,t){for(var n=0;n svg").classList.remove("hidden"),document.querySelector("#pay-now > span").classList.add("hidden"),void o.stripe.confirmSepaDebitSetup(document.querySelector("meta[name=si-client-secret").content,{payment_method:document.querySelector("input[name=token]").value}).then((function(e){if(!e.error)return document.querySelector('input[name="gateway_response"]').value=JSON.stringify(e.setupIntent),document.querySelector("#server-response").submit();console.error(error)})).catch((function(t){e.textContent=t,e.hidden=!1}))):""===document.getElementById("sepa-name").value?(document.getElementById("sepa-name").focus(),e.textContent=document.querySelector("meta[name=translation-name-required]").content,void(e.hidden=!1)):""===document.getElementById("sepa-email-address").value?(document.getElementById("sepa-email-address").focus(),e.textContent=document.querySelector("meta[name=translation-email-required]").content,void(e.hidden=!1)):document.getElementById("sepa-mandate-acceptance").checked?(document.getElementById("pay-now").disabled=!0,document.querySelector("#pay-now > svg").classList.remove("hidden"),document.querySelector("#pay-now > span").classList.add("hidden"),void o.stripe.confirmSepaDebitPayment(document.querySelector("meta[name=pi-client-secret").content,{payment_method:{sepa_debit:o.iban,billing_details:{name:document.getElementById("sepa-name").value,email:document.getElementById("sepa-email-address").value}}}).then((function(e){return e.error?o.handleFailure(e.error.message):o.handleSuccess(e)}))):(e.textContent=document.querySelector("meta[name=translation-terms-required]").content,e.hidden=!1,void console.log("Terms"))}))})),this.key=t,this.errors=document.getElementById("errors"),this.stripeConnect=n}var t,n,o;return t=e,(n=[{key:"handleSuccess",value:function(e){document.querySelector('input[name="gateway_response"]').value=JSON.stringify(e.paymentIntent);var t=document.querySelector('input[name="token-billing-checkbox"]:checked');t&&(document.querySelector('input[name="store_card"]').value=t.value),document.getElementById("server-response").submit()}},{key:"handleFailure",value:function(e){var t=document.getElementById("errors");t.textContent="",t.textContent=e,t.hidden=!1,document.getElementById("pay-now").disabled=!1,document.querySelector("#pay-now > svg").classList.add("hidden"),document.querySelector("#pay-now > span").classList.remove("hidden")}}])&&i(t.prototype,n),o&&i(t,o),e}(),d=null!==(n=null===(o=document.querySelector('meta[name="stripe-publishable-key"]'))||void 0===o?void 0:o.content)&&void 0!==n?n:"",l=null!==(r=null===(a=document.querySelector('meta[name="stripe-account-id"]'))||void 0===a?void 0:a.content)&&void 0!==r?r:"";new u(d,l).setupStripe().handle()}}); \ No newline at end of file diff --git a/public/mix-manifest.json b/public/mix-manifest.json index 5d5f62e8a7a2..538ca40c0f3e 100755 --- a/public/mix-manifest.json +++ b/public/mix-manifest.json @@ -20,7 +20,7 @@ "/js/clients/payments/stripe-ach.js": "/js/clients/payments/stripe-ach.js?id=81c2623fc1e5769b51c7", "/js/clients/payments/stripe-alipay.js": "/js/clients/payments/stripe-alipay.js?id=665ddf663500767f1a17", "/js/clients/payments/stripe-credit-card.js": "/js/clients/payments/stripe-credit-card.js?id=a30464874dee84678344", - "/js/clients/payments/stripe-sepa.js": "/js/clients/payments/stripe-sepa.js?id=e7dc964c85085314b12c", + "/js/clients/payments/stripe-sepa.js": "/js/clients/payments/stripe-sepa.js?id=3f2fa0857dc804a85dcb", "/js/clients/payments/stripe-sofort.js": "/js/clients/payments/stripe-sofort.js?id=231571942310348aa616", "/js/clients/payments/wepay-credit-card.js": "/js/clients/payments/wepay-credit-card.js?id=f51400e03c5fdb6cdabe", "/js/clients/quotes/action-selectors.js": "/js/clients/quotes/action-selectors.js?id=1b8f9325aa6e8595e7fa", diff --git a/resources/js/clients/payments/stripe-sepa.js b/resources/js/clients/payments/stripe-sepa.js index 8c62df2d7ccd..9760d8a75674 100644 --- a/resources/js/clients/payments/stripe-sepa.js +++ b/resources/js/clients/payments/stripe-sepa.js @@ -18,93 +18,174 @@ class ProcessSEPA { setupStripe = () => { this.stripe = Stripe(this.key); - if(this.stripeConnect) - this.stripe.stripeAccount = stripeConnect; + if (this.stripeConnect) this.stripe.stripeAccount = stripeConnect; const elements = this.stripe.elements(); var style = { base: { - color: "#32325d", + color: '#32325d', fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif', - fontSmoothing: "antialiased", - fontSize: "16px", - "::placeholder": { - color: "#aab7c4" + fontSmoothing: 'antialiased', + fontSize: '16px', + '::placeholder': { + color: '#aab7c4', + }, + ':-webkit-autofill': { + color: '#32325d', }, - ":-webkit-autofill": { - color: "#32325d" - } }, invalid: { - color: "#fa755a", - iconColor: "#fa755a", - ":-webkit-autofill": { - color: "#fa755a" - } - } + color: '#fa755a', + iconColor: '#fa755a', + ':-webkit-autofill': { + color: '#fa755a', + }, + }, }; var options = { style: style, - supportedCountries: ["SEPA"], + 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: document.querySelector('meta[name="country"]').content + placeholderCountry: document.querySelector('meta[name="country"]') + .content, }; - this.iban = elements.create("iban", options); - this.iban.mount("#sepa-iban"); + this.iban = elements.create('iban', options); + this.iban.mount('#sepa-iban'); return this; }; handle = () => { - document.getElementById('pay-now').addEventListener('click', (e) => { - let errors = document.getElementById('errors'); - if (document.getElementById('sepa-name').value === "") { - document.getElementById('sepa-name').focus(); - errors.textContent = "Name required."; - errors.hidden = false; - return; - } + Array.from( + document.getElementsByClassName('toggle-payment-with-token') + ).forEach((element) => + element.addEventListener('click', (element) => { + document + .getElementById('stripe--payment-container') + .classList.add('hidden'); + document.getElementById('save-card--container').style.display = + 'none'; + document.querySelector('input[name=token]').value = + element.target.dataset.token; + }) + ); - if (document.getElementById('sepa-email-address').value === "") { - document.getElementById('sepa-email-address').focus(); - errors.textContent = "Email required."; - errors.hidden = false; - return ; - } + document + .getElementById('toggle-payment-with-new-bank-account') + .addEventListener('click', (element) => { + document + .getElementById('stripe--payment-container') + .classList.remove('hidden'); + document.getElementById('save-card--container').style.display = + 'grid'; + document.querySelector('input[name=token]').value = ''; + }); + document.getElementById('pay-now').addEventListener('click', (e) => { + if ( + document.querySelector('input[name=token]').value.length !== 0 + ) { + document.querySelector('#errors').hidden = true; - if (!document.getElementById('sepa-mandate-acceptance').checked) { - errors.textContent = "Accept Terms"; - errors.hidden = false; - console.log("Terms"); - return ; - } + document.getElementById('pay-now').disabled = true; + document + .querySelector('#pay-now > svg') + .classList.remove('hidden'); + document + .querySelector('#pay-now > span') + .classList.add('hidden'); + + this.stripe + .confirmSepaDebitSetup( + document.querySelector('meta[name=si-client-secret') + .content, + { + payment_method: document.querySelector( + 'input[name=token]' + ).value, + } + ) + .then((result) => { + if (result.error) { + console.error(error); + + return; + } + + document.querySelector( + 'input[name="gateway_response"]' + ).value = JSON.stringify(result.setupIntent); + + return document + .querySelector('#server-response') + .submit(); + }) + .catch((error) => { + errors.textContent = error; + errors.hidden = false; + }); + + return; + } + + if (document.getElementById('sepa-name').value === '') { + document.getElementById('sepa-name').focus(); + errors.textContent = document.querySelector( + 'meta[name=translation-name-required]' + ).content; + errors.hidden = false; + return; + } + + if (document.getElementById('sepa-email-address').value === '') { + document.getElementById('sepa-email-address').focus(); + errors.textContent = document.querySelector( + 'meta[name=translation-email-required]' + ).content; + errors.hidden = false; + return; + } + + if (!document.getElementById('sepa-mandate-acceptance').checked) { + errors.textContent = document.querySelector( + 'meta[name=translation-terms-required]' + ).content; + errors.hidden = false; + console.log('Terms'); + return; + } 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, + 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, + }, }, - }, - } - ).then((result) => { - if (result.error) { - return this.handleFailure(result.error.message); - } + } + ) + .then((result) => { + if (result.error) { + return this.handleFailure(result.error.message); + } - return this.handleSuccess(result); - }); + return this.handleSuccess(result); + }); }); }; @@ -113,6 +194,15 @@ class ProcessSEPA { 'input[name="gateway_response"]' ).value = JSON.stringify(result.paymentIntent); + let tokenBillingCheckbox = document.querySelector( + 'input[name="token-billing-checkbox"]:checked' + ); + + if (tokenBillingCheckbox) { + document.querySelector('input[name="store_card"]').value = + tokenBillingCheckbox.value; + } + document.getElementById('server-response').submit(); } @@ -123,15 +213,15 @@ class ProcessSEPA { errors.textContent = message; errors.hidden = false; - document.getElementById('pay-now').disabled = false; - document.querySelector('#pay-now > svg').classList.add('hidden'); - document.querySelector('#pay-now > span').classList.remove('hidden'); + document.getElementById('pay-now').disabled = false; + document.querySelector('#pay-now > svg').classList.add('hidden'); + document.querySelector('#pay-now > span').classList.remove('hidden'); } } -const publishableKey = document.querySelector( - 'meta[name="stripe-publishable-key"]' -)?.content ?? ''; +const publishableKey = + document.querySelector('meta[name="stripe-publishable-key"]')?.content ?? + ''; const stripeConnect = document.querySelector('meta[name="stripe-account-id"]')?.content ?? ''; diff --git a/resources/lang/en/texts.php b/resources/lang/en/texts.php index 95d83a86f805..00030eba5201 100644 --- a/resources/lang/en/texts.php +++ b/resources/lang/en/texts.php @@ -4327,6 +4327,7 @@ $LANG = array( '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.', 'eps' => 'EPS', + 'you_need_to_accept_the_terms_before_proceeding' => 'You need to accept the terms before proceeding.', ); return $LANG; diff --git a/resources/views/portal/ninja2020/gateways/stripe/sepa/pay.blade.php b/resources/views/portal/ninja2020/gateways/stripe/sepa/pay.blade.php index dd8ff6b605d5..0eb9248ec828 100644 --- a/resources/views/portal/ninja2020/gateways/stripe/sepa/pay.blade.php +++ b/resources/views/portal/ninja2020/gateways/stripe/sepa/pay.blade.php @@ -7,9 +7,24 @@ + + + + + @endsection @section('gateway_content') +
+ @csrf + + + + + + +
+ @include('portal.ninja2020.gateways.includes.payment_details') @@ -18,7 +33,48 @@ {{ ctrans('texts.sepa') }} ({{ ctrans('texts.bank_transfer') }}) @endcomponent - @include('portal.ninja2020.gateways.stripe.sepa.sepa_debit') + @component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.pay_with')]) + @if (count($tokens) > 0) + @foreach ($tokens as $token) + + @endforeach + @endisset + + + @endcomponent + + @component('portal.ninja2020.components.general.card-element-single') +
+ + + +
+ + +
+
+ @endcomponent + @include('portal.ninja2020.gateways.includes.save_card') @include('portal.ninja2020.gateways.includes.pay_now') @endsection 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 deleted file mode 100644 index 7cbab29d9b25..000000000000 --- a/resources/views/portal/ninja2020/gateways/stripe/sepa/sepa_debit.blade.php +++ /dev/null @@ -1,29 +0,0 @@ -
- @component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.name')]) - -
- @csrf - - - - - - - - - -
- - -
-
- @endcomponent -
diff --git a/tests/Browser/ClientPortal/Gateways/Stripe/SEPATest.php b/tests/Browser/ClientPortal/Gateways/Stripe/SEPATest.php new file mode 100644 index 000000000000..44c8f505d4e8 --- /dev/null +++ b/tests/Browser/ClientPortal/Gateways/Stripe/SEPATest.php @@ -0,0 +1,127 @@ +driver->manage()->deleteAllCookies(); + } + + $this->browse(function (Browser $browser) { + $browser + ->visit(new Login()) + ->auth(); + }); + + $this->disableCompanyGateways(); + + // Enable Stripe. + CompanyGateway::where('gateway_key', 'd14dd26a37cecc30fdd65700bfb55b23')->restore(); + + // Enable SEPA. + $cg = CompanyGateway::where('gateway_key', 'd14dd26a37cecc30fdd65700bfb55b23')->firstOrFail(); + $fees_and_limits = $cg->fees_and_limits; + $fees_and_limits->{GatewayType::SEPA} = new FeesAndLimits(); + $cg->fees_and_limits = $fees_and_limits; + $cg->save(); + + // SEPA required DE to be billing country. + $client = Client::first(); + $client->country_id = 276; + + $settings = $client->settings; + $settings->currency_id = "3"; + + $client->settings = $settings; + $client->save(); + } + + public function testPayingWithNewSEPABankAccount(): void + { + $this->browse(function (Browser $browser) { + $browser + ->visitRoute('client.invoices.index') + ->click('@pay-now') + ->click('@pay-now-dropdown') + ->clickLink('SEPA Direct Debit') + ->type('#sepa-name', 'John Doe') + ->type('#sepa-email-address', 'test@invoiceninja.com') + ->withinFrame('iframe', function (Browser $browser) { + $browser->type('iban', 'DE89370400440532013000'); + }) + ->check('#sepa-mandate-acceptance', true) + ->click('#pay-now') + ->waitForText('Details of the payment', 60); + }); + } + + public function testPayingWithNewSEPABankAccountAndSaveForFuture(): void + { + $this->browse(function (Browser $browser) { + $browser + ->visitRoute('client.invoices.index') + ->click('@pay-now') + ->click('@pay-now-dropdown') + ->clickLink('SEPA Direct Debit') + ->type('#sepa-name', 'John Doe') + ->type('#sepa-email-address', 'test@invoiceninja.com') + ->withinFrame('iframe', function (Browser $browser) { + $browser->type('iban', 'DE89370400440532013000'); + }) + ->check('#sepa-mandate-acceptance', true) + ->radio('#proxy_is_default', true) + ->click('#pay-now') + ->waitForText('Details of the payment', 60); + }); + } + + public function testPayWithSavedBankAccount() + { + $this->browse(function (Browser $browser) { + $browser + ->visitRoute('client.invoices.index') + ->click('@pay-now') + ->click('@pay-now-dropdown') + ->clickLink('SEPA Direct Debit') + ->click('.toggle-payment-with-token') + ->click('#pay-now') + ->waitForText('Details of the payment', 60); + }); + } + + public function testRemoveBankAccount() + { + $this->browse(function (Browser $browser) { + $browser + ->visitRoute('client.payment_methods.index') + ->clickLink('View') + ->press('Remove Payment Method') + ->waitForText('Confirmation') + ->click('@confirm-payment-removal') + ->assertSee('Payment method has been successfully removed.'); + }); + } +}