From a1892c908bddcc24e7cc8c1b86345b7669f0103c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Mon, 4 Oct 2021 15:41:15 +0200 Subject: [PATCH 01/13] Scaffold `iDEAL` --- app/PaymentDrivers/Mollie/IDEAL.php | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 app/PaymentDrivers/Mollie/IDEAL.php diff --git a/app/PaymentDrivers/Mollie/IDEAL.php b/app/PaymentDrivers/Mollie/IDEAL.php new file mode 100644 index 000000000000..21977f104fb8 --- /dev/null +++ b/app/PaymentDrivers/Mollie/IDEAL.php @@ -0,0 +1,28 @@ + Date: Mon, 4 Oct 2021 15:41:39 +0200 Subject: [PATCH 02/13] Define `iDEAL` constant --- app/Models/GatewayType.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/Models/GatewayType.php b/app/Models/GatewayType.php index a7e06fb64cd8..5f923a36506f 100644 --- a/app/Models/GatewayType.php +++ b/app/Models/GatewayType.php @@ -27,6 +27,7 @@ class GatewayType extends StaticModel const CREDIT = 10; const KBC = 11; const BANCONTACT = 12; + const IDEAL = 13; public function gateway() { From 7a6243fc7ed50b8632c2ae15b008fa95c51ba306 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Mon, 4 Oct 2021 15:42:20 +0200 Subject: [PATCH 03/13] Add `iDEAL` to Gateway --- app/Models/Gateway.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/Models/Gateway.php b/app/Models/Gateway.php index 7b65f9fb93b1..2fcfa274aaf9 100644 --- a/app/Models/Gateway.php +++ b/app/Models/Gateway.php @@ -93,6 +93,7 @@ class Gateway extends StaticModel GatewayType::BANK_TRANSFER => ['refund' => false, 'token_billing' => true], GatewayType::KBC => ['refund' => false, 'token_billing' => false], GatewayType::BANCONTACT => ['refund' => false, 'token_billing' => false], + GatewayType::IDEAL => ['refund' => false, 'token_billing' => false], ]; case 15: return [GatewayType::PAYPAL => ['refund' => true, 'token_billing' => false]]; //Paypal From 49f60ce7bae480cc7f89bffe3adfe62e74147f4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Mon, 4 Oct 2021 15:43:11 +0200 Subject: [PATCH 04/13] Add `iDEAL` to MolliePaymentDriver --- app/PaymentDrivers/MolliePaymentDriver.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/PaymentDrivers/MolliePaymentDriver.php b/app/PaymentDrivers/MolliePaymentDriver.php index e5566eb7a1af..cb09fbb2db57 100644 --- a/app/PaymentDrivers/MolliePaymentDriver.php +++ b/app/PaymentDrivers/MolliePaymentDriver.php @@ -27,6 +27,7 @@ use App\Models\SystemLog; use App\PaymentDrivers\Mollie\Bancontact; use App\PaymentDrivers\Mollie\BankTransfer; use App\PaymentDrivers\Mollie\CreditCard; +use App\PaymentDrivers\Mollie\IDEAL; use App\PaymentDrivers\Mollie\KBC; use App\Utils\Traits\MakesHash; use Illuminate\Support\Facades\Validator; @@ -70,6 +71,7 @@ class MolliePaymentDriver extends BaseDriver GatewayType::BANCONTACT => Bancontact::class, GatewayType::BANK_TRANSFER => BankTransfer::class, GatewayType::KBC => KBC::class, + GatewayType::IDEAL => IDEAL::class, ]; const SYSTEM_LOG_TYPE = SystemLog::TYPE_MOLLIE; @@ -93,6 +95,7 @@ class MolliePaymentDriver extends BaseDriver $types[] = GatewayType::BANCONTACT; $types[] = GatewayType::BANK_TRANSFER; $types[] = GatewayType::KBC; + $types[] = GatewayType::IDEAL; return $types; } From 046e467332fdd6b314e7f1592da558596db68524 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Mon, 4 Oct 2021 15:43:48 +0200 Subject: [PATCH 05/13] Initialize MollieApiClient --- app/PaymentDrivers/Mollie/IDEAL.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/PaymentDrivers/Mollie/IDEAL.php b/app/PaymentDrivers/Mollie/IDEAL.php index 21977f104fb8..95d629dc83fb 100644 --- a/app/PaymentDrivers/Mollie/IDEAL.php +++ b/app/PaymentDrivers/Mollie/IDEAL.php @@ -15,9 +15,19 @@ namespace App\PaymentDrivers\Mollie; use App\Http\Requests\Request; use App\Http\Requests\ClientPortal\Payments\PaymentResponseRequest; use App\PaymentDrivers\Common\MethodInterface; +use App\PaymentDrivers\MolliePaymentDriver; class IDEAL implements MethodInterface { + protected MolliePaymentDriver $mollie; + + public function __construct(MolliePaymentDriver $mollie) + { + $this->mollie = $mollie; + + $this->mollie->init(); + } + public function authorizeView(array $data) { } public function authorizeResponse(Request $request) { } From 1d0c09ab6ff8060b826bf605789fd5a9bbf0c0c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Mon, 4 Oct 2021 15:48:13 +0200 Subject: [PATCH 06/13] Authorization --- app/PaymentDrivers/Mollie/IDEAL.php | 24 +++++++++++++++++-- .../gateways/mollie/ideal/authorize.blade.php | 8 +++++++ 2 files changed, 30 insertions(+), 2 deletions(-) create mode 100644 resources/views/portal/ninja2020/gateways/mollie/ideal/authorize.blade.php diff --git a/app/PaymentDrivers/Mollie/IDEAL.php b/app/PaymentDrivers/Mollie/IDEAL.php index 95d629dc83fb..1b0251278e45 100644 --- a/app/PaymentDrivers/Mollie/IDEAL.php +++ b/app/PaymentDrivers/Mollie/IDEAL.php @@ -16,6 +16,8 @@ use App\Http\Requests\Request; use App\Http\Requests\ClientPortal\Payments\PaymentResponseRequest; use App\PaymentDrivers\Common\MethodInterface; use App\PaymentDrivers\MolliePaymentDriver; +use Illuminate\Http\RedirectResponse; +use Illuminate\View\View; class IDEAL implements MethodInterface { @@ -28,9 +30,27 @@ class IDEAL implements MethodInterface $this->mollie->init(); } - public function authorizeView(array $data) { } + /** + * Show the authorization page for iDEAL. + * + * @param array $data + * @return View + */ + public function authorizeView(array $data): View + { + return render('gateways.mollie.ideal.authorize', $data); + } - public function authorizeResponse(Request $request) { } + /** + * Handle the authorization for iDEAL. + * + * @param Request $request + * @return RedirectResponse + */ + public function authorizeResponse(Request $request): RedirectResponse + { + return redirect()->route('client.payment_methods.index'); + } public function paymentView(array $data) { } diff --git a/resources/views/portal/ninja2020/gateways/mollie/ideal/authorize.blade.php b/resources/views/portal/ninja2020/gateways/mollie/ideal/authorize.blade.php new file mode 100644 index 000000000000..1603a11799b6 --- /dev/null +++ b/resources/views/portal/ninja2020/gateways/mollie/ideal/authorize.blade.php @@ -0,0 +1,8 @@ +@extends('portal.ninja2020.layout.payments', ['gateway_title' => 'iDEAL', 'card_title' => +'iDEAL']) + +@section('gateway_content') + @component('portal.ninja2020.components.general.card-element-single') + {{ __('texts.payment_method_cannot_be_preauthorized') }} + @endcomponent +@endsection From 3b2652fefb738ddd568c36f1f0d57d3556902169 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Mon, 4 Oct 2021 15:52:05 +0200 Subject: [PATCH 07/13] Add new PaymentType --- app/Models/PaymentType.php | 1 + ...0_04_134908_add_ideal_to_payment_types.php | 28 +++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 database/migrations/2021_10_04_134908_add_ideal_to_payment_types.php diff --git a/app/Models/PaymentType.php b/app/Models/PaymentType.php index 7b64be5733cd..ff396181578e 100644 --- a/app/Models/PaymentType.php +++ b/app/Models/PaymentType.php @@ -45,6 +45,7 @@ class PaymentType extends StaticModel const MOLLIE_BANK_TRANSFER = 34; const KBC = 35; const BANCONTACT = 36; + const IDEAL = 37; public static function parseCardType($cardName) { diff --git a/database/migrations/2021_10_04_134908_add_ideal_to_payment_types.php b/database/migrations/2021_10_04_134908_add_ideal_to_payment_types.php new file mode 100644 index 000000000000..c1a3840c018f --- /dev/null +++ b/database/migrations/2021_10_04_134908_add_ideal_to_payment_types.php @@ -0,0 +1,28 @@ +id = 37; + $type->name = 'iDEAL'; + $type->gateway_type_id = GatewayType::IDEAL; + + $type->save(); + }); + } +} From 5afb78b1b4e47e14e7039e92ad1ef65f70dea0b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Mon, 4 Oct 2021 16:06:41 +0200 Subject: [PATCH 08/13] Payments --- app/PaymentDrivers/Mollie/IDEAL.php | 165 +++++++++++++++++++++++++++- 1 file changed, 161 insertions(+), 4 deletions(-) diff --git a/app/PaymentDrivers/Mollie/IDEAL.php b/app/PaymentDrivers/Mollie/IDEAL.php index 1b0251278e45..f496db5b6f20 100644 --- a/app/PaymentDrivers/Mollie/IDEAL.php +++ b/app/PaymentDrivers/Mollie/IDEAL.php @@ -12,8 +12,15 @@ namespace App\PaymentDrivers\Mollie; -use App\Http\Requests\Request; +use App\Exceptions\PaymentFailed; use App\Http\Requests\ClientPortal\Payments\PaymentResponseRequest; +use App\Http\Requests\Request; +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\Common\MethodInterface; use App\PaymentDrivers\MolliePaymentDriver; use Illuminate\Http\RedirectResponse; @@ -52,7 +59,157 @@ class IDEAL implements MethodInterface return redirect()->route('client.payment_methods.index'); } - public function paymentView(array $data) { } + /** + * Show the payment page for iDEAL. + * + * @param array $data + * @return Redirector|RedirectResponse + */ + public function paymentView(array $data) + { + $this->mollie->payment_hash + ->withData('gateway_type_id', GatewayType::IDEAL) + ->withData('client_id', $this->mollie->client->id); - public function paymentResponse(PaymentResponseRequest $request) { } -} \ No newline at end of file + try { + $payment = $this->mollie->gateway->payments->create([ + 'method' => 'ideal', + 'amount' => [ + 'currency' => $this->mollie->client->currency()->code, + 'value' => $this->mollie->convertToMollieAmount((float) $this->mollie->payment_hash->data->amount_with_fee), + ], + 'description' => \sprintf('Invoices: %s', collect($data['invoices'])->pluck('invoice_number')), + 'redirectUrl' => route('client.payments.response', [ + 'company_gateway_id' => $this->mollie->company_gateway->id, + 'payment_hash' => $this->mollie->payment_hash->hash, + 'payment_method_id' => GatewayType::IDEAL, + ]), + 'webhookUrl' => $this->mollie->company_gateway->webhookUrl(), + 'metadata' => [ + 'client_id' => $this->mollie->client->hashed_id, + ], + ]); + + $this->mollie->payment_hash->withData('payment_id', $payment->id); + + return redirect( + $payment->getCheckoutUrl() + ); + } catch (\Mollie\Api\Exceptions\ApiException | \Exception $exception) { + return $this->processUnsuccessfulPayment($exception); + } + } + + /** + * Handle unsuccessful payment. + * + * @param Exception $exception + * @throws PaymentFailed + * @return void + */ + public function processUnsuccessfulPayment(\Exception $exception): void + { + PaymentFailureMailer::dispatch( + $this->mollie->client, + $exception->getMessage(), + $this->mollie->client->company, + $this->mollie->payment_hash->data->amount_with_fee + ); + + SystemLogger::dispatch( + $exception->getMessage(), + SystemLog::CATEGORY_GATEWAY_RESPONSE, + SystemLog::EVENT_GATEWAY_FAILURE, + SystemLog::TYPE_MOLLIE, + $this->mollie->client, + $this->mollie->client->company, + ); + + throw new PaymentFailed($exception->getMessage(), $exception->getCode()); + } + + /** + * Handle the payments for the iDEAL. + * + * @param PaymentResponseRequest $request + * @return mixed + */ + public function paymentResponse(PaymentResponseRequest $request) + { + if (!\property_exists($this->mollie->payment_hash->data, 'payment_id')) { + return $this->processUnsuccessfulPayment( + new PaymentFailed('Whoops, something went wrong. Missing required [payment_id] parameter. Please contact administrator. Reference hash: ' . $this->mollie->payment_hash->hash) + ); + } + + try { + $payment = $this->mollie->gateway->payments->get( + $this->mollie->payment_hash->data->payment_id + ); + + if ($payment->status === 'paid') { + return $this->processSuccessfulPayment($payment); + } + + if ($payment->status === 'open') { + return $this->processOpenPayment($payment); + } + + if ($payment->status === 'failed') { + return $this->processUnsuccessfulPayment( + new PaymentFailed(ctrans('texts.status_failed')) + ); + } + + return $this->processUnsuccessfulPayment( + new PaymentFailed(ctrans('texts.status_voided')) + ); + } catch (\Mollie\Api\Exceptions\ApiException | \Exception $exception) { + return $this->processUnsuccessfulPayment($exception); + } + } + + /** + * Handle the successful payment for iDEAL. + * + * @param string $status + * @param ResourcesPayment $payment + * @return RedirectResponse + */ + public function processSuccessfulPayment(\Mollie\Api\Resources\Payment $payment, string $status = 'paid'): RedirectResponse + { + $data = [ + 'gateway_type_id' => GatewayType::IDEAL, + 'amount' => array_sum(array_column($this->mollie->payment_hash->invoices(), 'amount')) + $this->mollie->payment_hash->fee_total, + 'payment_type' => PaymentType::IDEAL, + 'transaction_reference' => $payment->id, + ]; + + $payment_record = $this->mollie->createPayment( + $data, + $status === 'paid' ? Payment::STATUS_COMPLETED : Payment::STATUS_PENDING + ); + + SystemLogger::dispatch( + ['response' => $payment, 'data' => $data], + SystemLog::CATEGORY_GATEWAY_RESPONSE, + SystemLog::EVENT_GATEWAY_SUCCESS, + SystemLog::TYPE_MOLLIE, + $this->mollie->client, + $this->mollie->client->company, + ); + + return redirect()->route('client.payments.show', ['payment' => $this->mollie->encodePrimaryKey($payment_record->id)]); + } + + /** + * Handle 'open' payment status for IDEAL. + * + * @param ResourcesPayment $payment + * @return RedirectResponse + */ + public function processOpenPayment(\Mollie\Api\Resources\Payment $payment): RedirectResponse + { + return $this->processSuccessfulPayment($payment, 'open'); + } +} From c461212d8ea4347a833bc8d0ac6467b75d8ad001 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Mon, 4 Oct 2021 16:07:53 +0200 Subject: [PATCH 09/13] Scaffold IDEALTest --- .../Gateways/Mollie/IDEALTest.php | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 tests/Browser/ClientPortal/Gateways/Mollie/IDEALTest.php diff --git a/tests/Browser/ClientPortal/Gateways/Mollie/IDEALTest.php b/tests/Browser/ClientPortal/Gateways/Mollie/IDEALTest.php new file mode 100644 index 000000000000..875eb8b1b91f --- /dev/null +++ b/tests/Browser/ClientPortal/Gateways/Mollie/IDEALTest.php @@ -0,0 +1,40 @@ +driver->manage()->deleteAllCookies(); + } + + $this->disableCompanyGateways(); + + CompanyGateway::where('gateway_key', '1bd651fb213ca0c9d66ae3c336dc77e8')->restore(); + + $this->browse(function (Browser $browser) { + $browser + ->visit(new Login()) + ->auth(); + }); + } +} \ No newline at end of file From 225de1592caa1aa732103731dd206bdd4d9ef5b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Mon, 4 Oct 2021 16:11:39 +0200 Subject: [PATCH 10/13] Add iDEAL to GatewayType --- app/Models/GatewayType.php | 3 ++- resources/lang/en/texts.php | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/Models/GatewayType.php b/app/Models/GatewayType.php index 5f923a36506f..e0b0fbfdcd35 100644 --- a/app/Models/GatewayType.php +++ b/app/Models/GatewayType.php @@ -64,7 +64,8 @@ class GatewayType extends StaticModel return ctrans('texts.kbc_cbc'); case self::BANCONTACT: return ctrans('texts.bancontact'); - + case self::IDEAL: + return ctrans('texts.ideal'); default: return 'Undefined.'; break; diff --git a/resources/lang/en/texts.php b/resources/lang/en/texts.php index 14df4963a7c2..2bffc05fda02 100644 --- a/resources/lang/en/texts.php +++ b/resources/lang/en/texts.php @@ -4316,6 +4316,7 @@ $LANG = array( 'payment_method_cannot_be_preauthorized' => 'This payment method cannot be preauthorized.', 'kbc_cbc' => 'KBC/CBC', 'bancontact' => 'Bancontact', + 'ideal' => 'iDEAL', ); return $LANG; From e48018067b523fd0a68ed97fa5078097bbd7d7fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Mon, 4 Oct 2021 16:11:50 +0200 Subject: [PATCH 11/13] Tests: Successful payments --- .../ClientPortal/Gateways/Mollie/IDEALTest.php | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/Browser/ClientPortal/Gateways/Mollie/IDEALTest.php b/tests/Browser/ClientPortal/Gateways/Mollie/IDEALTest.php index 875eb8b1b91f..8ece57b89ba9 100644 --- a/tests/Browser/ClientPortal/Gateways/Mollie/IDEALTest.php +++ b/tests/Browser/ClientPortal/Gateways/Mollie/IDEALTest.php @@ -37,4 +37,21 @@ class IDEALTest extends DuskTestCase ->auth(); }); } + + public function testSuccessfulPayment(): void + { + $this->browse(function (Browser $browser) { + $browser + ->visitRoute('client.invoices.index') + ->click('@pay-now') + ->press('Pay Now') + ->clickLink('iDEAL') + ->waitForText('Test profile') + ->press('ABN AMRO') + ->radio('final_state', 'paid') + ->press('Continue') + ->waitForText('Details of the payment') + ->assertSee('Completed'); + }); + } } \ No newline at end of file From bc6719e4fb834a5470d166cf2920afda99629bec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Mon, 4 Oct 2021 16:12:51 +0200 Subject: [PATCH 12/13] Tests: Open payments --- .../ClientPortal/Gateways/Mollie/IDEALTest.php | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/Browser/ClientPortal/Gateways/Mollie/IDEALTest.php b/tests/Browser/ClientPortal/Gateways/Mollie/IDEALTest.php index 8ece57b89ba9..ae3e15fff8d6 100644 --- a/tests/Browser/ClientPortal/Gateways/Mollie/IDEALTest.php +++ b/tests/Browser/ClientPortal/Gateways/Mollie/IDEALTest.php @@ -54,4 +54,21 @@ class IDEALTest extends DuskTestCase ->assertSee('Completed'); }); } + + public function testOpenPayments(): void + { + $this->browse(function (Browser $browser) { + $browser + ->visitRoute('client.invoices.index') + ->click('@pay-now') + ->press('Pay Now') + ->clickLink('iDEAL') + ->waitForText('Test profile') + ->press('ABN AMRO') + ->radio('final_state', 'open') + ->press('Continue') + ->waitForText('Details of the payment') + ->assertSee('Pending'); + }); + } } \ No newline at end of file From de73fbc23432c857efa0fbb666e4936307ff207a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Mon, 4 Oct 2021 16:15:21 +0200 Subject: [PATCH 13/13] Tests: Failed/cancelled payments --- .../Gateways/Mollie/IDEALTest.php | 34 ++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/tests/Browser/ClientPortal/Gateways/Mollie/IDEALTest.php b/tests/Browser/ClientPortal/Gateways/Mollie/IDEALTest.php index ae3e15fff8d6..b369fc30248f 100644 --- a/tests/Browser/ClientPortal/Gateways/Mollie/IDEALTest.php +++ b/tests/Browser/ClientPortal/Gateways/Mollie/IDEALTest.php @@ -55,7 +55,7 @@ class IDEALTest extends DuskTestCase }); } - public function testOpenPayments(): void + public function testOpenPayment(): void { $this->browse(function (Browser $browser) { $browser @@ -71,4 +71,36 @@ class IDEALTest extends DuskTestCase ->assertSee('Pending'); }); } + + public function testFailedPayment(): void + { + $this->browse(function (Browser $browser) { + $browser + ->visitRoute('client.invoices.index') + ->click('@pay-now') + ->press('Pay Now') + ->clickLink('iDEAL') + ->waitForText('Test profile') + ->press('ABN AMRO') + ->radio('final_state', 'failed') + ->press('Continue') + ->waitForText('Failed.'); + }); + } + + public function testCancelledPayment(): void + { + $this->browse(function (Browser $browser) { + $browser + ->visitRoute('client.invoices.index') + ->click('@pay-now') + ->press('Pay Now') + ->clickLink('iDEAL') + ->waitForText('Test profile') + ->press('ABN AMRO') + ->radio('final_state', 'canceled') + ->press('Continue') + ->waitForText('Cancelled.'); + }); + } } \ No newline at end of file