diff --git a/src/app/Providers/Payment/Mollie.php b/src/app/Providers/Payment/Mollie.php --- a/src/app/Providers/Payment/Mollie.php +++ b/src/app/Providers/Payment/Mollie.php @@ -7,6 +7,7 @@ use App\Wallet; use Illuminate\Support\Facades\DB; use Mollie\Api\Exceptions\ApiException; +use Mollie\Api\Types; class Mollie extends \App\Providers\PaymentProvider { @@ -352,6 +353,18 @@ } } } + + // In case there were multiple auto-payment setup requests (e.g. caused by a double + // form submission) we end up with multiple payment records and mollie_mandate_id + // pointing to the one from the last payment not the successful one. + // We make sure to use mandate id from the successful "first" payment. + if ( + $payment->type == self::TYPE_MANDATE + && $mollie_payment->mandateId + && $mollie_payment->sequenceType == Types\SequenceType::SEQUENCETYPE_FIRST + ) { + $payment->wallet->setSetting('mollie_mandate_id', $mollie_payment->mandateId); + } } elseif ($mollie_payment->isFailed()) { // Note: I didn't find a way to get any description of the problem with a payment \Log::info(sprintf('Mollie payment failed (%s)', $payment->id)); diff --git a/src/resources/js/app.js b/src/resources/js/app.js --- a/src/resources/js/app.js +++ b/src/resources/js/app.js @@ -381,7 +381,10 @@ // Add a axios response interceptor for general/validation error handler window.axios.interceptors.response.use( response => { - // Do nothing + if (response.config.onFinish) { + response.config.onFinish() + } + return response }, error => { @@ -393,6 +396,10 @@ return Promise.reject(error) } + if (error.config.onFinish) { + error.config.onFinish() + } + if (error.response && status == 422) { error_msg = "Form validation error" diff --git a/src/resources/vue/Wallet.vue b/src/resources/vue/Wallet.vue --- a/src/resources/vue/Wallet.vue +++ b/src/resources/vue/Wallet.vue @@ -247,6 +247,7 @@ }) } + this.formLock = false this.paymentForm = 'init' this.paymentDialogTitle = 'Top up your wallet' @@ -255,9 +256,17 @@ }).modal() }, payment() { + if (this.formLock) { + return + } + + // Lock the form to prevent from double submission + this.formLock = true + let onFinish = () => { this.formLock = false } + this.$root.clearFormValidation($('#payment-form')) - axios.post('/api/v4/payments', {amount: this.amount}) + axios.post('/api/v4/payments', {amount: this.amount}, { onFinish }) .then(response => { if (response.data.redirectUrl) { location.href = response.data.redirectUrl @@ -267,6 +276,14 @@ }) }, autoPayment() { + if (this.formLock) { + return + } + + // Lock the form to prevent from double submission + this.formLock = true + let onFinish = () => { this.formLock = false } + const method = this.mandate.id && (this.mandate.isValid || this.mandate.isPending) ? 'put' : 'post' const post = { amount: this.mandate.amount, @@ -275,7 +292,7 @@ this.$root.clearFormValidation($('#auto-payment form')) - axios[method]('/api/v4/payments/mandate', post) + axios[method]('/api/v4/payments/mandate', post, { onFinish }) .then(response => { if (method == 'post') { this.mandate.id = null @@ -310,6 +327,7 @@ autoPaymentForm(event, title) { this.paymentForm = 'auto' this.paymentDialogTitle = title || 'Add auto-payment' + this.formLock = false setTimeout(() => { this.dialog.find('#mandate_amount').focus()}, 10) }, receiptDownload() { diff --git a/src/tests/Browser/PaymentMollieTest.php b/src/tests/Browser/PaymentMollieTest.php --- a/src/tests/Browser/PaymentMollieTest.php +++ b/src/tests/Browser/PaymentMollieTest.php @@ -46,7 +46,7 @@ 'password' => 'simple123', ]); - $this->browse(function (Browser $browser) { + $this->browse(function (Browser $browser) use ($user) { $browser->visit(new Home()) ->submitLogon('payment-test@kolabnow.com', 'simple123', true, ['paymentProvider' => 'mollie']) ->on(new Dashboard()) @@ -66,12 +66,16 @@ ->assertSeeIn('#amount + span + .invalid-feedback', 'The amount must be a number.') // Submit valid data ->type('@body #amount', '12.34') + // Note we use double click to assert it does not create redundant requests + ->click('@body #payment-form button') ->click('@body #payment-form button'); }) ->on(new PaymentMollie()) ->assertSeeIn('@title', \config('app.name') . ' Payment') ->assertSeeIn('@amount', 'CHF 12.34'); + $this->assertSame(1, $user->wallets()->first()->payments()->count()); + // Looks like the Mollie testing mode is limited. // We'll select credit card method and mark the payment as paid // We can't do much more, we have to trust Mollie their page works ;) @@ -109,7 +113,7 @@ 'password' => 'simple123', ]); - $this->browse(function (Browser $browser) { + $this->browse(function (Browser $browser) use ($user) { $browser->visit(new Home()) ->submitLogon('payment-test@kolabnow.com', 'simple123', true, ['paymentProvider' => 'mollie']) ->on(new Dashboard()) @@ -150,6 +154,8 @@ // Submit valid data ->type('@body #mandate_amount', '100') ->type('@body #mandate_balance', '0') + // Note we use double click to assert it does not create redundant requests + ->click('@button-action') ->click('@button-action'); }) ->on(new PaymentMollie()) @@ -171,6 +177,8 @@ ->assertMissing('@body .alert') ->click('@button-cancel'); }); + + $this->assertSame(1, $user->wallets()->first()->payments()->count()); }); // Test updating (disabled) auto-payment