mirror of
				https://github.com/invoiceninja/invoiceninja.git
				synced 2025-11-04 04:07:32 -05:00 
			
		
		
		
	Merge pull request #6827 from beganovich/v5-726
Stripe: SEPA improvements
This commit is contained in:
		
						commit
						f038073b4a
					
				@ -11,15 +11,15 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
namespace App\PaymentDrivers\Stripe;
 | 
					namespace App\PaymentDrivers\Stripe;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use App\Exceptions\PaymentFailed;
 | 
				
			||||||
use App\Http\Requests\ClientPortal\Payments\PaymentResponseRequest;
 | 
					use App\Http\Requests\ClientPortal\Payments\PaymentResponseRequest;
 | 
				
			||||||
use App\PaymentDrivers\StripePaymentDriver;
 | 
					 | 
				
			||||||
use App\Jobs\Mail\PaymentFailureMailer;
 | 
					use App\Jobs\Mail\PaymentFailureMailer;
 | 
				
			||||||
use App\Jobs\Util\SystemLogger;
 | 
					use App\Jobs\Util\SystemLogger;
 | 
				
			||||||
use App\Models\GatewayType;
 | 
					use App\Models\GatewayType;
 | 
				
			||||||
use App\Models\Payment;
 | 
					use App\Models\Payment;
 | 
				
			||||||
use App\Models\PaymentType;
 | 
					use App\Models\PaymentType;
 | 
				
			||||||
use App\Models\SystemLog;
 | 
					use App\Models\SystemLog;
 | 
				
			||||||
use App\Exceptions\PaymentFailed;
 | 
					use App\PaymentDrivers\StripePaymentDriver;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class SEPA
 | 
					class SEPA
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
@ -29,6 +29,8 @@ class SEPA
 | 
				
			|||||||
    public function __construct(StripePaymentDriver $stripe)
 | 
					    public function __construct(StripePaymentDriver $stripe)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $this->stripe = $stripe;
 | 
					        $this->stripe = $stripe;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $this->stripe->init();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function authorizeView($data)
 | 
					    public function authorizeView($data)
 | 
				
			||||||
@ -36,7 +38,8 @@ class SEPA
 | 
				
			|||||||
        return render('gateways.stripe.sepa.authorize', $data);
 | 
					        return render('gateways.stripe.sepa.authorize', $data);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function paymentView(array $data) {
 | 
					    public function paymentView(array $data)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
        $data['gateway'] = $this->stripe;
 | 
					        $data['gateway'] = $this->stripe;
 | 
				
			||||||
        $data['payment_method_id'] = GatewayType::SEPA;
 | 
					        $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());
 | 
					        $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',
 | 
					            'setup_future_usage' => 'off_session',
 | 
				
			||||||
            'customer' => $this->stripe->findOrCreateCustomer(),
 | 
					            'customer' => $this->stripe->findOrCreateCustomer(),
 | 
				
			||||||
            'description' => $this->stripe->decodeUnicodeString(ctrans('texts.invoices') . ': ' . collect($data['invoices'])->pluck('invoice_number')),
 | 
					            'description' => $this->stripe->decodeUnicodeString(ctrans('texts.invoices') . ': ' . collect($data['invoices'])->pluck('invoice_number')),
 | 
				
			||||||
 | 
					 | 
				
			||||||
        ]);
 | 
					        ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $data['pi_client_secret'] = $intent->client_secret;
 | 
					        $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->data = array_merge((array) $this->stripe->payment_hash->data, ['stripe_amount' => $data['stripe_amount']]);
 | 
				
			||||||
        $this->stripe->payment_hash->save();
 | 
					        $this->stripe->payment_hash->save();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -65,28 +76,24 @@ class SEPA
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    public function paymentResponse(PaymentResponseRequest $request)
 | 
					    public function paymentResponse(PaymentResponseRequest $request)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
 | 
					 | 
				
			||||||
        $gateway_response = json_decode($request->gateway_response);
 | 
					        $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->data = array_merge((array) $this->stripe->payment_hash->data, $request->all());
 | 
				
			||||||
        $this->stripe->payment_hash->save();
 | 
					        $this->stripe->payment_hash->save();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (property_exists($gateway_response, 'status') && $gateway_response->status == 'processing') {
 | 
					        if (property_exists($gateway_response, 'status') && ($gateway_response->status == 'processing' || $gateway_response->status === 'succeeded')) {
 | 
				
			||||||
            
 | 
					            if ($request->store_card) {
 | 
				
			||||||
            $this->stripe->init();
 | 
					 | 
				
			||||||
                $this->storePaymentMethod($gateway_response);
 | 
					                $this->storePaymentMethod($gateway_response);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            return $this->processSuccessfulPayment($gateway_response->id);
 | 
					            return $this->processSuccessfulPayment($gateway_response->id);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return $this->processUnsuccessfulPayment();
 | 
					        return $this->processUnsuccessfulPayment();
 | 
				
			||||||
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function processSuccessfulPayment(string $payment_intent)
 | 
					    public function processSuccessfulPayment(string $payment_intent)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $this->stripe->init();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $data = [
 | 
					        $data = [
 | 
				
			||||||
            'payment_method' => $payment_intent,
 | 
					            'payment_method' => $payment_intent,
 | 
				
			||||||
            'payment_type' => PaymentType::SEPA,
 | 
					            'payment_type' => PaymentType::SEPA,
 | 
				
			||||||
@ -95,7 +102,7 @@ class SEPA
 | 
				
			|||||||
            'gateway_type_id' => GatewayType::SEPA,
 | 
					            'gateway_type_id' => GatewayType::SEPA,
 | 
				
			||||||
        ];
 | 
					        ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $this->stripe->createPayment($data, Payment::STATUS_PENDING);
 | 
					        $payment = $this->stripe->createPayment($data, Payment::STATUS_PENDING);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        SystemLogger::dispatch(
 | 
					        SystemLogger::dispatch(
 | 
				
			||||||
            ['response' => $this->stripe->payment_hash->data, 'data' => $data],
 | 
					            ['response' => $this->stripe->payment_hash->data, 'data' => $data],
 | 
				
			||||||
@ -106,7 +113,7 @@ class SEPA
 | 
				
			|||||||
            $this->stripe->client->company,
 | 
					            $this->stripe->client->company,
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return redirect()->route('client.payments.index');
 | 
					        return redirect()->route('client.payments.show', $payment->hashed_id);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function processUnsuccessfulPayment()
 | 
					    public function processUnsuccessfulPayment()
 | 
				
			||||||
@ -141,7 +148,6 @@ class SEPA
 | 
				
			|||||||
    private function storePaymentMethod($intent)
 | 
					    private function storePaymentMethod($intent)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        try {
 | 
					        try {
 | 
				
			||||||
 | 
					 | 
				
			||||||
            $method = $this->stripe->getStripePaymentMethod($intent->payment_method);
 | 
					            $method = $this->stripe->getStripePaymentMethod($intent->payment_method);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            $payment_meta = new \stdClass;
 | 
					            $payment_meta = new \stdClass;
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										2
									
								
								public/js/clients/payments/stripe-sepa.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								public/js/clients/payments/stripe-sepa.js
									
									
									
									
										vendored
									
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							@ -20,7 +20,7 @@
 | 
				
			|||||||
    "/js/clients/payments/stripe-ach.js": "/js/clients/payments/stripe-ach.js?id=81c2623fc1e5769b51c7",
 | 
					    "/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-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-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/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/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",
 | 
					    "/js/clients/quotes/action-selectors.js": "/js/clients/quotes/action-selectors.js?id=1b8f9325aa6e8595e7fa",
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										168
									
								
								resources/js/clients/payments/stripe-sepa.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										168
									
								
								resources/js/clients/payments/stripe-sepa.js
									
									
									
									
										vendored
									
									
								
							@ -18,87 +18,168 @@ class ProcessSEPA {
 | 
				
			|||||||
    setupStripe = () => {
 | 
					    setupStripe = () => {
 | 
				
			||||||
        this.stripe = Stripe(this.key);
 | 
					        this.stripe = Stripe(this.key);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if(this.stripeConnect)
 | 
					        if (this.stripeConnect) this.stripe.stripeAccount = stripeConnect;
 | 
				
			||||||
            this.stripe.stripeAccount = stripeConnect;
 | 
					 | 
				
			||||||
        const elements = this.stripe.elements();
 | 
					        const elements = this.stripe.elements();
 | 
				
			||||||
        var style = {
 | 
					        var style = {
 | 
				
			||||||
            base: {
 | 
					            base: {
 | 
				
			||||||
                color: "#32325d",
 | 
					                color: '#32325d',
 | 
				
			||||||
                fontFamily:
 | 
					                fontFamily:
 | 
				
			||||||
                    '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
 | 
					                    '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
 | 
				
			||||||
                fontSmoothing: "antialiased",
 | 
					                fontSmoothing: 'antialiased',
 | 
				
			||||||
                fontSize: "16px",
 | 
					                fontSize: '16px',
 | 
				
			||||||
                "::placeholder": {
 | 
					                '::placeholder': {
 | 
				
			||||||
                    color: "#aab7c4"
 | 
					                    color: '#aab7c4',
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					                ':-webkit-autofill': {
 | 
				
			||||||
 | 
					                    color: '#32325d',
 | 
				
			||||||
                },
 | 
					                },
 | 
				
			||||||
                ":-webkit-autofill": {
 | 
					 | 
				
			||||||
                    color: "#32325d"
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            invalid: {
 | 
					            invalid: {
 | 
				
			||||||
                color: "#fa755a",
 | 
					                color: '#fa755a',
 | 
				
			||||||
                iconColor: "#fa755a",
 | 
					                iconColor: '#fa755a',
 | 
				
			||||||
                ":-webkit-autofill": {
 | 
					                ':-webkit-autofill': {
 | 
				
			||||||
                    color: "#fa755a"
 | 
					                    color: '#fa755a',
 | 
				
			||||||
                }
 | 
					                },
 | 
				
			||||||
            }
 | 
					            },
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
        var options = {
 | 
					        var options = {
 | 
				
			||||||
            style: style,
 | 
					            style: style,
 | 
				
			||||||
            supportedCountries: ["SEPA"],
 | 
					            supportedCountries: ['SEPA'],
 | 
				
			||||||
            // If you know the country of the customer, you can optionally pass it to
 | 
					            // 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
 | 
					            // the Element as placeholderCountry. The example IBAN that is being used
 | 
				
			||||||
            // as placeholder reflects the IBAN format of that country.
 | 
					            // 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 = elements.create('iban', options);
 | 
				
			||||||
        this.iban.mount("#sepa-iban");
 | 
					        this.iban.mount('#sepa-iban');
 | 
				
			||||||
        return this;
 | 
					        return this;
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    handle = () => {
 | 
					    handle = () => {
 | 
				
			||||||
        document.getElementById('pay-now').addEventListener('click', (e) => {
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        let errors = document.getElementById('errors');
 | 
					        let errors = document.getElementById('errors');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (document.getElementById('sepa-name').value === "") {
 | 
					        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;
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        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;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                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();
 | 
					                document.getElementById('sepa-name').focus();
 | 
				
			||||||
            errors.textContent = "Name required.";
 | 
					                errors.textContent = document.querySelector(
 | 
				
			||||||
 | 
					                    'meta[name=translation-name-required]'
 | 
				
			||||||
 | 
					                ).content;
 | 
				
			||||||
                errors.hidden = false;
 | 
					                errors.hidden = false;
 | 
				
			||||||
                return;
 | 
					                return;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (document.getElementById('sepa-email-address').value === "") {
 | 
					            if (document.getElementById('sepa-email-address').value === '') {
 | 
				
			||||||
                document.getElementById('sepa-email-address').focus();
 | 
					                document.getElementById('sepa-email-address').focus();
 | 
				
			||||||
            errors.textContent = "Email required.";
 | 
					                errors.textContent = document.querySelector(
 | 
				
			||||||
 | 
					                    'meta[name=translation-email-required]'
 | 
				
			||||||
 | 
					                ).content;
 | 
				
			||||||
                errors.hidden = false;
 | 
					                errors.hidden = false;
 | 
				
			||||||
            return ;
 | 
					                return;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
            if (!document.getElementById('sepa-mandate-acceptance').checked) {
 | 
					            if (!document.getElementById('sepa-mandate-acceptance').checked) {
 | 
				
			||||||
            errors.textContent = "Accept Terms";
 | 
					                errors.textContent = document.querySelector(
 | 
				
			||||||
 | 
					                    'meta[name=translation-terms-required]'
 | 
				
			||||||
 | 
					                ).content;
 | 
				
			||||||
                errors.hidden = false;
 | 
					                errors.hidden = false;
 | 
				
			||||||
            console.log("Terms");
 | 
					                console.log('Terms');
 | 
				
			||||||
            return ;
 | 
					                return;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            document.getElementById('pay-now').disabled = true;
 | 
					            document.getElementById('pay-now').disabled = true;
 | 
				
			||||||
            document.querySelector('#pay-now > svg').classList.remove('hidden');
 | 
					            document.querySelector('#pay-now > svg').classList.remove('hidden');
 | 
				
			||||||
            document.querySelector('#pay-now > span').classList.add('hidden');
 | 
					            document.querySelector('#pay-now > span').classList.add('hidden');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            this.stripe.confirmSepaDebitPayment(
 | 
					            this.stripe
 | 
				
			||||||
                document.querySelector('meta[name=pi-client-secret').content,
 | 
					                .confirmSepaDebitPayment(
 | 
				
			||||||
 | 
					                    document.querySelector('meta[name=pi-client-secret')
 | 
				
			||||||
 | 
					                        .content,
 | 
				
			||||||
                    {
 | 
					                    {
 | 
				
			||||||
                        payment_method: {
 | 
					                        payment_method: {
 | 
				
			||||||
                            sepa_debit: this.iban,
 | 
					                            sepa_debit: this.iban,
 | 
				
			||||||
                            billing_details: {
 | 
					                            billing_details: {
 | 
				
			||||||
                            name: document.getElementById("sepa-name").value,
 | 
					                                name: document.getElementById('sepa-name')
 | 
				
			||||||
                            email: document.getElementById("sepa-email-address").value,
 | 
					                                    .value,
 | 
				
			||||||
 | 
					                                email: document.getElementById(
 | 
				
			||||||
 | 
					                                    'sepa-email-address'
 | 
				
			||||||
 | 
					                                ).value,
 | 
				
			||||||
                            },
 | 
					                            },
 | 
				
			||||||
                        },
 | 
					                        },
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
            ).then((result) => {
 | 
					                )
 | 
				
			||||||
 | 
					                .then((result) => {
 | 
				
			||||||
                    if (result.error) {
 | 
					                    if (result.error) {
 | 
				
			||||||
                        return this.handleFailure(result.error.message);
 | 
					                        return this.handleFailure(result.error.message);
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
@ -113,6 +194,15 @@ class ProcessSEPA {
 | 
				
			|||||||
            'input[name="gateway_response"]'
 | 
					            'input[name="gateway_response"]'
 | 
				
			||||||
        ).value = JSON.stringify(result.paymentIntent);
 | 
					        ).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();
 | 
					        document.getElementById('server-response').submit();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -129,9 +219,9 @@ class ProcessSEPA {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const publishableKey = document.querySelector(
 | 
					const publishableKey =
 | 
				
			||||||
    'meta[name="stripe-publishable-key"]'
 | 
					    document.querySelector('meta[name="stripe-publishable-key"]')?.content ??
 | 
				
			||||||
)?.content ?? '';
 | 
					    '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const stripeConnect =
 | 
					const stripeConnect =
 | 
				
			||||||
    document.querySelector('meta[name="stripe-account-id"]')?.content ?? '';
 | 
					    document.querySelector('meta[name="stripe-account-id"]')?.content ?? '';
 | 
				
			||||||
 | 
				
			|||||||
@ -4327,6 +4327,7 @@ $LANG = array(
 | 
				
			|||||||
    'giropay' => 'GiroPay',
 | 
					    '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.',
 | 
					    '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',
 | 
					    'eps' => 'EPS',
 | 
				
			||||||
 | 
					    'you_need_to_accept_the_terms_before_proceeding' => 'You need to accept the terms before proceeding.',
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
return $LANG;
 | 
					return $LANG;
 | 
				
			||||||
 | 
				
			|||||||
@ -7,9 +7,24 @@
 | 
				
			|||||||
    <meta name="country" content="{{ $country }}">
 | 
					    <meta name="country" content="{{ $country }}">
 | 
				
			||||||
    <meta name="customer" content="{{ $customer }}">
 | 
					    <meta name="customer" content="{{ $customer }}">
 | 
				
			||||||
    <meta name="pi-client-secret" content="{{ $pi_client_secret }}">
 | 
					    <meta name="pi-client-secret" content="{{ $pi_client_secret }}">
 | 
				
			||||||
 | 
					    <meta name="si-client-secret" content="{{ $si_client_secret ?? '' }}">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <meta name="translation-name-required" content="{{ ctrans('texts.missing_account_holder_name') }}">
 | 
				
			||||||
 | 
					    <meta name="translation-email-required" content="{{ ctrans('texts.provide_email') }}">
 | 
				
			||||||
 | 
					    <meta name="translation-terms-required" content="{{ ctrans('texts.you_need_to_accept_the_terms_before_proceeding') }}">
 | 
				
			||||||
@endsection
 | 
					@endsection
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@section('gateway_content')
 | 
					@section('gateway_content')
 | 
				
			||||||
 | 
					    <form action="{{ route('client.payments.response') }}" method="post" id="server-response">
 | 
				
			||||||
 | 
					        @csrf
 | 
				
			||||||
 | 
					        <input type="hidden" name="gateway_response">
 | 
				
			||||||
 | 
					        <input type="hidden" name="company_gateway_id" value="{{ $gateway->getCompanyGatewayId() }}">
 | 
				
			||||||
 | 
					        <input type="hidden" name="payment_method_id" value="{{ $payment_method_id }}">
 | 
				
			||||||
 | 
					        <input type="hidden" name="payment_hash" value="{{ $payment_hash }}">
 | 
				
			||||||
 | 
					        <input type="hidden" name="store_card">
 | 
				
			||||||
 | 
					        <input type="hidden" name="token">
 | 
				
			||||||
 | 
					    </form>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <div class="alert alert-failure mb-4" hidden id="errors"></div>
 | 
					    <div class="alert alert-failure mb-4" hidden id="errors"></div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @include('portal.ninja2020.gateways.includes.payment_details')
 | 
					    @include('portal.ninja2020.gateways.includes.payment_details')
 | 
				
			||||||
@ -18,7 +33,48 @@
 | 
				
			|||||||
        {{ ctrans('texts.sepa') }} ({{ ctrans('texts.bank_transfer') }})
 | 
					        {{ ctrans('texts.sepa') }} ({{ ctrans('texts.bank_transfer') }})
 | 
				
			||||||
    @endcomponent
 | 
					    @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)
 | 
				
			||||||
 | 
					                <label class="mr-4">
 | 
				
			||||||
 | 
					                    <input type="radio" data-token="{{ $token->token }}" name="payment-type"
 | 
				
			||||||
 | 
					                        class="form-radio cursor-pointer toggle-payment-with-token" />
 | 
				
			||||||
 | 
					                    <span class="ml-1 cursor-pointer">**** {{ optional($token->meta)->last4 }}</span>
 | 
				
			||||||
 | 
					                </label>
 | 
				
			||||||
 | 
					            @endforeach
 | 
				
			||||||
 | 
					        @endisset
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <label>
 | 
				
			||||||
 | 
					            <input type="radio" id="toggle-payment-with-new-bank-account" class="form-radio cursor-pointer" name="payment-type"
 | 
				
			||||||
 | 
					                checked />
 | 
				
			||||||
 | 
					            <span class="ml-1 cursor-pointer">{{ __('texts.new_bank_account') }}</span>
 | 
				
			||||||
 | 
					        </label>
 | 
				
			||||||
 | 
					    @endcomponent
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @component('portal.ninja2020.components.general.card-element-single')
 | 
				
			||||||
 | 
					        <div id="stripe--payment-container">
 | 
				
			||||||
 | 
					            <label for="sepa-name">
 | 
				
			||||||
 | 
					                <input class="input w-full" id="sepa-name" type="text"
 | 
				
			||||||
 | 
					                    placeholder="{{ ctrans('texts.bank_account_holder') }}">
 | 
				
			||||||
 | 
					            </label>
 | 
				
			||||||
 | 
					            <label for="sepa-email" class="mt-4">
 | 
				
			||||||
 | 
					                <input class="input w-full" id="sepa-email-address" type="email"
 | 
				
			||||||
 | 
					                    placeholder="{{ ctrans('texts.email') }}">
 | 
				
			||||||
 | 
					            </label>
 | 
				
			||||||
 | 
					            <label>
 | 
				
			||||||
 | 
					                <div class="border p-3 rounded mt-2">
 | 
				
			||||||
 | 
					                    <div id="sepa-iban"></div>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					            </label>
 | 
				
			||||||
 | 
					            <div id="mandate-acceptance" class="mt-4">
 | 
				
			||||||
 | 
					                <input type="checkbox" id="sepa-mandate-acceptance" class="input mr-4">
 | 
				
			||||||
 | 
					                <label for="sepa-mandate-acceptance" class="cursor-pointer">
 | 
				
			||||||
 | 
					                    {{ ctrans('texts.sepa_mandat', ['company' => $contact->company->present()->name()]) }}
 | 
				
			||||||
 | 
					                </label>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					    @endcomponent
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @include('portal.ninja2020.gateways.includes.save_card')
 | 
					    @include('portal.ninja2020.gateways.includes.save_card')
 | 
				
			||||||
    @include('portal.ninja2020.gateways.includes.pay_now')
 | 
					    @include('portal.ninja2020.gateways.includes.pay_now')
 | 
				
			||||||
@endsection
 | 
					@endsection
 | 
				
			||||||
 | 
				
			|||||||
@ -1,29 +0,0 @@
 | 
				
			|||||||
<div id="stripe--payment-container">
 | 
					 | 
				
			||||||
    @component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.name')])
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    <form action="{{ route('client.payments.response') }}" method="post" id="server-response">
 | 
					 | 
				
			||||||
        @csrf
 | 
					 | 
				
			||||||
        <input type="hidden" name="gateway_response">
 | 
					 | 
				
			||||||
        <input type="hidden" name="company_gateway_id" value="{{ $gateway->getCompanyGatewayId() }}">
 | 
					 | 
				
			||||||
        <input type="hidden" name="payment_method_id" value="{{ $payment_method_id }}">
 | 
					 | 
				
			||||||
        <input type="hidden" name="payment_hash" value="{{ $payment_hash }}">
 | 
					 | 
				
			||||||
        <input type="hidden" name="store_card">
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        <label for="sepa-name">
 | 
					 | 
				
			||||||
            <input class="input w-full" id="sepa-name" type="text" placeholder="{{ ctrans('texts.bank_account_holder') }}">
 | 
					 | 
				
			||||||
        </label>
 | 
					 | 
				
			||||||
        <label for="sepa-email" >
 | 
					 | 
				
			||||||
            <input class="input w-full" id="sepa-email-address" type="email" placeholder="{{ ctrans('texts.email') }}">
 | 
					 | 
				
			||||||
        </label>
 | 
					 | 
				
			||||||
        <label>
 | 
					 | 
				
			||||||
            <div class="border p-4 rounded">
 | 
					 | 
				
			||||||
                <div id="sepa-iban"></div>
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
        </label>
 | 
					 | 
				
			||||||
        <div id="mandate-acceptance">
 | 
					 | 
				
			||||||
            <input type="checkbox" id="sepa-mandate-acceptance" class="input mr-4">
 | 
					 | 
				
			||||||
            <label for="sepa-mandate-acceptance">{{ctrans('texts.sepa_mandat', ['company' => auth('contact')->user()->company->present()->name()])}}</label>
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
    </form>
 | 
					 | 
				
			||||||
    @endcomponent
 | 
					 | 
				
			||||||
</div>
 | 
					 | 
				
			||||||
							
								
								
									
										127
									
								
								tests/Browser/ClientPortal/Gateways/Stripe/SEPATest.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										127
									
								
								tests/Browser/ClientPortal/Gateways/Stripe/SEPATest.php
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,127 @@
 | 
				
			|||||||
 | 
					<?php
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * 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
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace Tests\Browser\ClientPortal\Gateways\Stripe;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use App\DataMapper\FeesAndLimits;
 | 
				
			||||||
 | 
					use App\Models\Client;
 | 
				
			||||||
 | 
					use App\Models\CompanyGateway;
 | 
				
			||||||
 | 
					use App\Models\GatewayType;
 | 
				
			||||||
 | 
					use Laravel\Dusk\Browser;
 | 
				
			||||||
 | 
					use Tests\Browser\Pages\ClientPortal\Login;
 | 
				
			||||||
 | 
					use Tests\DuskTestCase;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class SEPATest extends DuskTestCase
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    protected function setUp(): void
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        parent::setUp();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        foreach (static::$browsers as $browser) {
 | 
				
			||||||
 | 
					            $browser->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.');
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user