From 8ae1d86f8034fb4f62cd756936f92e3f16165591 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Tue, 28 Sep 2021 17:59:02 +0200 Subject: [PATCH 01/25] Activate GoCardless in `PaymentLibrariesSeeder` --- database/seeders/PaymentLibrariesSeeder.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/seeders/PaymentLibrariesSeeder.php b/database/seeders/PaymentLibrariesSeeder.php index 818431f7de96..2fcca4588d5b 100644 --- a/database/seeders/PaymentLibrariesSeeder.php +++ b/database/seeders/PaymentLibrariesSeeder.php @@ -97,7 +97,7 @@ class PaymentLibrariesSeeder extends Seeder Gateway::query()->update(['visible' => 0]); - Gateway::whereIn('id', [1,7,11,15,20,39,46,55,50,57])->update(['visible' => 1]); + Gateway::whereIn('id', [1,7,11,15,20,39,46,55,50,57,52])->update(['visible' => 1]); if (Ninja::isHosted()) { Gateway::whereIn('id', [20])->update(['visible' => 0]); From d8142161211666b7332fed24d70b6a8afacfc075 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Tue, 28 Sep 2021 18:01:42 +0200 Subject: [PATCH 02/25] Activate GoCardless w/ migration --- ...647_activate_gocardless_payment_driver.php | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 database/migrations/2021_09_28_154647_activate_gocardless_payment_driver.php diff --git a/database/migrations/2021_09_28_154647_activate_gocardless_payment_driver.php b/database/migrations/2021_09_28_154647_activate_gocardless_payment_driver.php new file mode 100644 index 000000000000..8356413a9f96 --- /dev/null +++ b/database/migrations/2021_09_28_154647_activate_gocardless_payment_driver.php @@ -0,0 +1,32 @@ +visible = true; + $gateway->save(); + } + } +} From 9acf7b8b3b91c1ec663e456ede42b9f1982c0324 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Tue, 28 Sep 2021 18:24:35 +0200 Subject: [PATCH 03/25] Add GoCardless to Gateway.php --- app/Models/Gateway.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/Models/Gateway.php b/app/Models/Gateway.php index 209be095aff8..209c689a4603 100644 --- a/app/Models/Gateway.php +++ b/app/Models/Gateway.php @@ -131,6 +131,10 @@ class Gateway extends StaticModel GatewayType::CREDIT_CARD => ['refund' => false, 'token_billing' => true], //Square ]; break; + case 52: + return [ + GatewayType::BANK_TRANSFER => ['refund' => true, 'token_billing' => true] // GoCardless + ]; break; default: return []; From 77c95b5535ea60c306817ca501566add010ca47c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Tue, 28 Sep 2021 18:24:44 +0200 Subject: [PATCH 04/25] Add TYPE_GOCARDLESS constant in SystemLog --- app/Models/SystemLog.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/Models/SystemLog.php b/app/Models/SystemLog.php index 47794de1a79c..316ada10063b 100644 --- a/app/Models/SystemLog.php +++ b/app/Models/SystemLog.php @@ -73,6 +73,7 @@ class SystemLog extends Model const TYPE_MOLLIE = 312; const TYPE_EWAY = 313; const TYPE_SQUARE = 320; + const TYPE_GOCARDLESS = 321; const TYPE_QUOTA_EXCEEDED = 400; const TYPE_UPSTREAM_FAILURE = 401; From 9ad4fabb3f89ec3aba44e6ddb6cd35cd85b1fcea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Tue, 28 Sep 2021 18:24:52 +0200 Subject: [PATCH 05/25] Scaffold GoCardlessPaymentDriver --- .../GoCardlessPaymentDriver.php | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 app/PaymentDrivers/GoCardlessPaymentDriver.php diff --git a/app/PaymentDrivers/GoCardlessPaymentDriver.php b/app/PaymentDrivers/GoCardlessPaymentDriver.php new file mode 100644 index 000000000000..e03204c650b1 --- /dev/null +++ b/app/PaymentDrivers/GoCardlessPaymentDriver.php @@ -0,0 +1,79 @@ + ACH::class, + ]; + + const SYSTEM_LOG_TYPE = SystemLog::TYPE_GOCARDLESS; + + public function setPaymentMethod($payment_method_id) + { + $class = self::$methods[$payment_method_id]; + + $this->payment_method = new $class($this); + + return $this; + } + + public function authorizeView(array $data) + { + return $this->payment_method->authorizeView($data); + } + + public function authorizeResponse($request) + { + return $this->payment_method->authorizeResponse($request); + } + + public function processPaymentView(array $data) + { + return $this->payment_method->paymentView($data); + } + + public function processPaymentResponse($request) + { + return $this->payment_method->paymentResponse($request); + } + + public function refund(Payment $payment, $amount, $return_client_response = false) + { + // .. + } + + public function tokenBilling(ClientGatewayToken $cgt, PaymentHash $payment_hash) + { + // .. + } +} From 7e922cbc2ae5fa4abd45f69f0c0fb8cb1d8a0365 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Wed, 29 Sep 2021 14:12:33 +0200 Subject: [PATCH 06/25] Update GoCardless provider class --- .../2021_09_28_154647_activate_gocardless_payment_driver.php | 1 + database/seeders/PaymentLibrariesSeeder.php | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/database/migrations/2021_09_28_154647_activate_gocardless_payment_driver.php b/database/migrations/2021_09_28_154647_activate_gocardless_payment_driver.php index 8356413a9f96..ace55614bdb8 100644 --- a/database/migrations/2021_09_28_154647_activate_gocardless_payment_driver.php +++ b/database/migrations/2021_09_28_154647_activate_gocardless_payment_driver.php @@ -25,6 +25,7 @@ class ActivateGocardlessPaymentDriver extends Migration $gateway = Gateway::find(52); if ($gateway) { + $gateway->provider = 'GoCardless'; $gateway->visible = true; $gateway->save(); } diff --git a/database/seeders/PaymentLibrariesSeeder.php b/database/seeders/PaymentLibrariesSeeder.php index 2fcca4588d5b..d512c31b7350 100644 --- a/database/seeders/PaymentLibrariesSeeder.php +++ b/database/seeders/PaymentLibrariesSeeder.php @@ -76,7 +76,7 @@ class PaymentLibrariesSeeder extends Seeder ['id' => 49, 'name' => 'WePay', 'provider' => 'WePay', 'is_offsite' => false, 'sort_order' => 3, 'key' => '8fdeed552015b3c7b44ed6c8ebd9e992', 'fields' => '{"accountId":"","accessToken":"","type":"goods","testMode":false,"feePayer":"payee"}'], ['id' => 50, 'name' => 'Braintree', 'provider' => 'Braintree', 'sort_order' => 3, 'key' => 'f7ec488676d310683fb51802d076d713', 'fields' => '{"merchantId":"","merchantAccountId":"","publicKey":"","privateKey":"","testMode":false}'], ['id' => 51, 'name' => 'FirstData Payeezy', 'provider' => 'FirstData_Payeezy', 'key' => '30334a52fb698046572c627ca10412e8', 'fields' => '{"gatewayId":"","password":"","keyId":"","hmac":"","testMode":false}'], - ['id' => 52, 'name' => 'GoCardless', 'provider' => 'GoCardlessV2\Redirect', 'sort_order' => 9, 'is_offsite' => true, 'key' => 'b9886f9257f0c6ee7c302f1c74475f6c', 'fields' => '{"accessToken":"","webhookSecret":"","testMode":true}'], + ['id' => 52, 'name' => 'GoCardless', 'provider' => 'GoCardless', 'sort_order' => 9, 'is_offsite' => true, 'key' => 'b9886f9257f0c6ee7c302f1c74475f6c', 'fields' => '{"accessToken":"","webhookSecret":"","testMode":true}'], ['id' => 53, 'name' => 'PagSeguro', 'provider' => 'PagSeguro', 'key' => 'ef498756b54db63c143af0ec433da803', 'fields' => '{"email":"","token":"","sandbox":false}'], ['id' => 54, 'name' => 'PAYMILL', 'provider' => 'Paymill', 'key' => 'ca52f618a39367a4c944098ebf977e1c', 'fields' => '{"apiKey":""}'], ['id' => 55, 'name' => 'Custom', 'provider' => 'Custom', 'is_offsite' => true, 'sort_order' => 21, 'key' => '54faab2ab6e3223dbe848b1686490baa', 'fields' => '{"name":"","text":""}'], From 51123493066badd6782caa06e5a1c6e41f32f621 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Wed, 29 Sep 2021 14:12:46 +0200 Subject: [PATCH 07/25] Add `gatewayTypes` method to GoCardless --- app/PaymentDrivers/GoCardlessPaymentDriver.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/PaymentDrivers/GoCardlessPaymentDriver.php b/app/PaymentDrivers/GoCardlessPaymentDriver.php index e03204c650b1..85ba330df389 100644 --- a/app/PaymentDrivers/GoCardlessPaymentDriver.php +++ b/app/PaymentDrivers/GoCardlessPaymentDriver.php @@ -18,6 +18,7 @@ use App\Models\PaymentHash; use App\Models\SystemLog; use App\Utils\Traits\MakesHash; + class GoCardlessPaymentDriver extends BaseDriver { use MakesHash; @@ -47,6 +48,13 @@ class GoCardlessPaymentDriver extends BaseDriver return $this; } + public function gatewayTypes(): array + { + return [ + GatewayType::BANK_TRANSFER, + ]; + } + public function authorizeView(array $data) { return $this->payment_method->authorizeView($data); From f179b615c1380bb885f4c576726efc83ee2eb737 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Wed, 29 Sep 2021 14:15:21 +0200 Subject: [PATCH 08/25] Scaffold ACH class --- app/PaymentDrivers/GoCardless/ACH.php | 36 +++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 app/PaymentDrivers/GoCardless/ACH.php diff --git a/app/PaymentDrivers/GoCardless/ACH.php b/app/PaymentDrivers/GoCardless/ACH.php new file mode 100644 index 000000000000..7e4a7a80f985 --- /dev/null +++ b/app/PaymentDrivers/GoCardless/ACH.php @@ -0,0 +1,36 @@ +go_cardless = $go_cardless; + } + + public function authorizeView(array $data) { } + + public function authorizeResponse(Request $request) { } + + public function paymentView(array $data) { } + + public function paymentResponse(PaymentResponseRequest $request) { } +} From ba0210fff6fcc227717677501a10ea61951ac296 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Wed, 29 Sep 2021 14:15:33 +0200 Subject: [PATCH 09/25] Update namespace in `GoCardlessPaymentDriver` --- app/PaymentDrivers/GoCardlessPaymentDriver.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/PaymentDrivers/GoCardlessPaymentDriver.php b/app/PaymentDrivers/GoCardlessPaymentDriver.php index 85ba330df389..0c325bb477bf 100644 --- a/app/PaymentDrivers/GoCardlessPaymentDriver.php +++ b/app/PaymentDrivers/GoCardlessPaymentDriver.php @@ -34,7 +34,7 @@ class GoCardlessPaymentDriver extends BaseDriver public $payment_method; public static $methods = [ - GatewayType::BANK_TRANSFER => ACH::class, + GatewayType::BANK_TRANSFER => \App\PaymentDrivers\GoCardless\ACH::class, ]; const SYSTEM_LOG_TYPE = SystemLog::TYPE_GOCARDLESS; From 33f0b6b7c6ad5aa14f6359a02d928f661ab11a2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Wed, 29 Sep 2021 14:17:30 +0200 Subject: [PATCH 10/25] Conditionally show ACH for US customers --- app/PaymentDrivers/GoCardlessPaymentDriver.php | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/app/PaymentDrivers/GoCardlessPaymentDriver.php b/app/PaymentDrivers/GoCardlessPaymentDriver.php index 0c325bb477bf..f445ec14b22d 100644 --- a/app/PaymentDrivers/GoCardlessPaymentDriver.php +++ b/app/PaymentDrivers/GoCardlessPaymentDriver.php @@ -50,9 +50,17 @@ class GoCardlessPaymentDriver extends BaseDriver public function gatewayTypes(): array { - return [ - GatewayType::BANK_TRANSFER, - ]; + $types = []; + + if ( + $this->client + && isset($this->client->country) + && in_array($this->client->country->iso_3166_3, ['USA']) + ) { + $types[] = GatewayType::BANK_TRANSFER; + } + + return $types; } public function authorizeView(array $data) From b3f97054c56366d98f00d275829047b7abf6da24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Wed, 29 Sep 2021 14:44:23 +0200 Subject: [PATCH 11/25] Initialization for GoCardless --- app/PaymentDrivers/GoCardlessPaymentDriver.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/PaymentDrivers/GoCardlessPaymentDriver.php b/app/PaymentDrivers/GoCardlessPaymentDriver.php index f445ec14b22d..f04796cfe8d4 100644 --- a/app/PaymentDrivers/GoCardlessPaymentDriver.php +++ b/app/PaymentDrivers/GoCardlessPaymentDriver.php @@ -63,6 +63,16 @@ class GoCardlessPaymentDriver extends BaseDriver return $types; } + public function init(): self + { + $this->gateway = new \GoCardlessPro\Client([ + 'access_token' => $this->company_gateway->getConfigField('accessToken'), + 'environment' => $this->company_gateway->getConfigField('testMode') ? \GoCardlessPro\Environment::SANDBOX : \GoCardlessPro\Environment::LIVE, + ]); + + return $this; + } + public function authorizeView(array $data) { return $this->payment_method->authorizeView($data); From 54baa5eb8dae62c92de647f5bf85040ab3628a9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Wed, 29 Sep 2021 14:44:30 +0200 Subject: [PATCH 12/25] Init GoCardless in ACH --- app/PaymentDrivers/GoCardless/ACH.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/PaymentDrivers/GoCardless/ACH.php b/app/PaymentDrivers/GoCardless/ACH.php index 7e4a7a80f985..9fe8e930a9bc 100644 --- a/app/PaymentDrivers/GoCardless/ACH.php +++ b/app/PaymentDrivers/GoCardless/ACH.php @@ -24,6 +24,8 @@ class ACH implements MethodInterface public function __construct(GoCardlessPaymentDriver $go_cardless) { $this->go_cardless = $go_cardless; + + $this->go_cardless->init(); } public function authorizeView(array $data) { } From 16059e306964d4645cd7ce0a385343c4c9e71be3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Wed, 29 Sep 2021 16:26:15 +0200 Subject: [PATCH 13/25] Add `confirmation` route for payment methods --- routes/client.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/routes/client.php b/routes/client.php index f2e0cd8485a3..c824bbe303c9 100644 --- a/routes/client.php +++ b/routes/client.php @@ -56,6 +56,8 @@ Route::group(['middleware' => ['auth:contact', 'locale', 'check_client_existence Route::get('payment_methods/{payment_method}/verification', 'ClientPortal\PaymentMethodController@verify')->name('payment_methods.verification'); Route::post('payment_methods/{payment_method}/verification', 'ClientPortal\PaymentMethodController@processVerification'); + Route::get('payment_methods/confirm', 'ClientPortal\PaymentMethodController@store')->name('payment_methods.confirm'); + Route::resource('payment_methods', 'ClientPortal\PaymentMethodController')->except(['edit', 'update']); Route::match(['GET', 'POST'], 'quotes/approve', 'ClientPortal\QuoteController@bulk')->name('quotes.bulk'); From b0b814ec36547c7842f6756d646c7931ee19b0a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Wed, 29 Sep 2021 16:54:14 +0200 Subject: [PATCH 14/25] Typehint $gateway --- app/PaymentDrivers/GoCardlessPaymentDriver.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/PaymentDrivers/GoCardlessPaymentDriver.php b/app/PaymentDrivers/GoCardlessPaymentDriver.php index f04796cfe8d4..8e28b86286a9 100644 --- a/app/PaymentDrivers/GoCardlessPaymentDriver.php +++ b/app/PaymentDrivers/GoCardlessPaymentDriver.php @@ -29,7 +29,7 @@ class GoCardlessPaymentDriver extends BaseDriver public $can_authorise_credit_card = true; - public $gateway; + public \GoCardlessPro\Client $gateway; public $payment_method; From 142b94c5cf9b6d019cb8aa3b737ed4ab43798b90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Wed, 29 Sep 2021 16:54:19 +0200 Subject: [PATCH 15/25] Authorization --- app/PaymentDrivers/GoCardless/ACH.php | 110 ++++++++++++++++++++++++-- 1 file changed, 105 insertions(+), 5 deletions(-) diff --git a/app/PaymentDrivers/GoCardless/ACH.php b/app/PaymentDrivers/GoCardless/ACH.php index 9fe8e930a9bc..2002929fe512 100644 --- a/app/PaymentDrivers/GoCardless/ACH.php +++ b/app/PaymentDrivers/GoCardless/ACH.php @@ -12,10 +12,17 @@ namespace App\PaymentDrivers\GoCardless; -use App\Http\Requests\Request; +use App\Exceptions\PaymentFailed; use App\Http\Requests\ClientPortal\Payments\PaymentResponseRequest; +use App\Http\Requests\Request; +use App\Jobs\Util\SystemLogger; +use App\Models\GatewayType; +use App\Models\SystemLog; use App\PaymentDrivers\Common\MethodInterface; use App\PaymentDrivers\GoCardlessPaymentDriver; +use Exception; +use Illuminate\Http\RedirectResponse; +use Illuminate\Routing\Redirector; class ACH implements MethodInterface { @@ -28,11 +35,104 @@ class ACH implements MethodInterface $this->go_cardless->init(); } - public function authorizeView(array $data) { } + /** + * Authorization page for ACH. + * + * @param array $data + * @return Redirector|RedirectResponse + */ + public function authorizeView(array $data) + { + $session_token = \Illuminate\Support\Str::uuid()->toString(); - public function authorizeResponse(Request $request) { } + try { + $redirect = $this->go_cardless->gateway->redirectFlows()->create([ + "params" => [ + "session_token" => $session_token, + "success_redirect_url" => route('client.payment_methods.confirm', [ + 'method' => GatewayType::BANK_TRANSFER, + 'session_token' => $session_token, + ]), + "prefilled_customer" => [ + "given_name" => auth('contact')->user()->first_name, + "family_name" => auth('contact')->user()->last_name, + "email" => auth('contact')->user()->email, + "address_line1" => auth('contact')->user()->client->address1, + "city" => auth('contact')->user()->client->city, + "postal_code" => auth('contact')->user()->client->postal_code, + ], + ], + ]); - public function paymentView(array $data) { } + return redirect( + $redirect->redirect_url + ); + } catch (\Exception $exception) { + return $this->processUnsuccessfulAuthorization($exception); + } + } - public function paymentResponse(PaymentResponseRequest $request) { } + /** + * Handle unsuccessful authorization. + * + * @param Exception $exception + * @throws PaymentFailed + * @return void + */ + public function processUnsuccessfulAuthorization(\Exception $exception): void + { + SystemLogger::dispatch( + $exception->getMessage(), + SystemLog::CATEGORY_GATEWAY_RESPONSE, + SystemLog::EVENT_GATEWAY_FAILURE, + SystemLog::TYPE_GOCARDLESS, + $this->go_cardless->client, + $this->go_cardless->client->company, + ); + + throw new PaymentFailed($exception->getMessage(), $exception->getCode()); + } + + /** + * Handle ACH post-redirect authorization. + * + * @param Request $request + * @return RedirectResponse|void + */ + public function authorizeResponse(Request $request) + { + try { + $redirect_flow = $this->go_cardless->gateway->redirectFlows()->complete( + $request->redirect_flow_id, + ['params' => [ + 'session_token' => $request->session_token + ]], + ); + + $payment_meta = new \stdClass; + $payment_meta->brand = ctrans('texts.ach'); + $payment_meta->type = GatewayType::BANK_TRANSFER; + $payment_meta->state = 'authorized'; + + $data = [ + 'payment_meta' => $payment_meta, + 'token' => $redirect_flow->links->mandate, + 'payment_method_id' => GatewayType::BANK_TRANSFER, + ]; + + $payment_method = $this->go_cardless->storeGatewayToken($data, ['gateway_customer_reference' => $redirect_flow->links->customer]); + + return redirect()->route('client.payment_methods.show', $payment_method->hashed_id); + } catch (\Exception $exception) { + return $this->processUnsuccessfulAuthorization($exception); + } + } + + public function paymentView(array $data) + { + } + + public function paymentResponse(PaymentResponseRequest $request) + { + } } From f4ddc5a974b01e0169d3594e709b689e4119f90d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Thu, 30 Sep 2021 08:02:58 +0200 Subject: [PATCH 16/25] Hide archived payment methods --- app/Http/Livewire/PaymentMethodsTable.php | 1 - 1 file changed, 1 deletion(-) diff --git a/app/Http/Livewire/PaymentMethodsTable.php b/app/Http/Livewire/PaymentMethodsTable.php index d50a6f35372f..b4906176fd05 100644 --- a/app/Http/Livewire/PaymentMethodsTable.php +++ b/app/Http/Livewire/PaymentMethodsTable.php @@ -37,7 +37,6 @@ class PaymentMethodsTable extends Component ->where('company_id', $this->company->id) ->where('client_id', $this->client->id) ->orderBy($this->sort_field, $this->sort_asc ? 'asc' : 'desc') - ->withTrashed() ->paginate($this->per_page); return render('components.livewire.payment-methods-table', [ From c913c61493d2ceb1e84cb0f5eccb4c7c0a9bc7a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Thu, 30 Sep 2021 08:42:30 +0200 Subject: [PATCH 17/25] Add `convertToGoCardlessAmount` --- app/PaymentDrivers/GoCardlessPaymentDriver.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/PaymentDrivers/GoCardlessPaymentDriver.php b/app/PaymentDrivers/GoCardlessPaymentDriver.php index 8e28b86286a9..d36074107945 100644 --- a/app/PaymentDrivers/GoCardlessPaymentDriver.php +++ b/app/PaymentDrivers/GoCardlessPaymentDriver.php @@ -102,4 +102,9 @@ class GoCardlessPaymentDriver extends BaseDriver { // .. } + + public function convertToGoCardlessAmount($amount, $precision) + { + return \round(($amount * pow(10, $precision)), 0); + } } From 50c5136eb1a8c49cb8304e471e4d45dc73f51bc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Thu, 30 Sep 2021 08:42:41 +0200 Subject: [PATCH 18/25] Payments --- app/PaymentDrivers/GoCardless/ACH.php | 100 +++++++++++++++++- .../gateways/gocardless/ach/pay.blade.php | 55 ++++++++++ 2 files changed, 154 insertions(+), 1 deletion(-) create mode 100644 resources/views/portal/ninja2020/gateways/gocardless/ach/pay.blade.php diff --git a/app/PaymentDrivers/GoCardless/ACH.php b/app/PaymentDrivers/GoCardless/ACH.php index 2002929fe512..ae3e2073d279 100644 --- a/app/PaymentDrivers/GoCardless/ACH.php +++ b/app/PaymentDrivers/GoCardless/ACH.php @@ -15,17 +15,25 @@ namespace App\PaymentDrivers\GoCardless; 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\ClientGatewayToken; use App\Models\GatewayType; +use App\Models\Payment; +use App\Models\PaymentType; use App\Models\SystemLog; use App\PaymentDrivers\Common\MethodInterface; use App\PaymentDrivers\GoCardlessPaymentDriver; +use App\Utils\Traits\MakesHash; use Exception; use Illuminate\Http\RedirectResponse; use Illuminate\Routing\Redirector; +use Illuminate\View\View; class ACH implements MethodInterface { + use MakesHash; + public GoCardlessPaymentDriver $go_cardless; public function __construct(GoCardlessPaymentDriver $go_cardless) @@ -128,11 +136,101 @@ class ACH implements MethodInterface } } - public function paymentView(array $data) + /** + * Show the payment page for ACH. + * + * @param array $data + * @return View + */ + public function paymentView(array $data): View { + $data['gateway'] = $this->go_cardless; + $data['amount'] = $this->go_cardless->convertToGoCardlessAmount($data['total']['amount_with_fee'], $this->go_cardless->client->currency()->precision); + $data['currency'] = $this->go_cardless->client->getCurrencyCode(); + + return render('gateways.gocardless.ach.pay', $data); } public function paymentResponse(PaymentResponseRequest $request) { + $token = ClientGatewayToken::find( + $this->decodePrimaryKey($request->source) + )->firstOrFail(); + + try { + $payment = $this->go_cardless->gateway->payments()->create([ + 'params' => [ + 'amount' => $request->amount, + 'currency' => $request->currency, + 'metadata' => [ + 'payment_hash' => $this->go_cardless->payment_hash->hash, + ], + 'links' => [ + 'mandate' => $token->token, + ], + ], + ]); + + + if ($payment->status === 'pending_submission') { + return $this->processPendingPayment($payment, ['token' => $token->hashed_id]); + } + + return $this->processUnsuccessfulPayment($payment); + } catch (\Exception $exception) { + throw new PaymentFailed($exception->getMessage(), $exception->getCode()); + } + } + + public function processPendingPayment(\GoCardlessPro\Resources\Payment $payment, array $data = []) + { + $data = [ + 'payment_method' => $data['token'], + 'payment_type' => PaymentType::ACH, + 'amount' => $this->go_cardless->payment_hash->data->amount_with_fee, + 'transaction_reference' => $payment->id, + 'gateway_type_id' => GatewayType::BANK_TRANSFER, + ]; + + $payment = $this->go_cardless->createPayment($data, Payment::STATUS_PENDING); + + SystemLogger::dispatch( + ['response' => $payment, 'data' => $data], + SystemLog::CATEGORY_GATEWAY_RESPONSE, + SystemLog::EVENT_GATEWAY_SUCCESS, + SystemLog::TYPE_GOCARDLESS, + $this->go_cardless->client, + $this->go_cardless->client->company, + ); + + return redirect()->route('client.payments.show', ['payment' => $this->go_cardless->encodePrimaryKey($payment->id)]); + } + + public function processUnsuccessfulPayment(\GoCardlessPro\Resources\Payment $payment) + { + PaymentFailureMailer::dispatch($this->go_cardless->client, $payment->status, $this->go_cardless->client->company, $this->go_cardless->payment_hash->data->amount_with_fee); + + PaymentFailureMailer::dispatch( + $this->go_cardless->client, + $payment, + $this->go_cardless->client->company, + $payment->amount + ); + + $message = [ + 'server_response' => $payment, + 'data' => $this->go_cardless->payment_hash->data, + ]; + + SystemLogger::dispatch( + $message, + SystemLog::CATEGORY_GATEWAY_RESPONSE, + SystemLog::EVENT_GATEWAY_FAILURE, + SystemLog::TYPE_GOCARDLESS, + $this->go_cardless->client, + $this->go_cardless->client->company, + ); + + throw new PaymentFailed('Failed to process the payment.', 500); } } diff --git a/resources/views/portal/ninja2020/gateways/gocardless/ach/pay.blade.php b/resources/views/portal/ninja2020/gateways/gocardless/ach/pay.blade.php new file mode 100644 index 000000000000..98dd992b5b5a --- /dev/null +++ b/resources/views/portal/ninja2020/gateways/gocardless/ach/pay.blade.php @@ -0,0 +1,55 @@ +@extends('portal.ninja2020.layout.payments', ['gateway_title' => 'ACH', 'card_title' => 'ACH']) + +@section('gateway_content') + @if (count($tokens) > 0) + + + @include('portal.ninja2020.gateways.includes.payment_details') + +
+ @csrf + + + + + + +
+ + @component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.pay_with')]) + @if (count($tokens) > 0) + @foreach ($tokens as $token) + + @endforeach + @endisset + @endcomponent + + @else + @component('portal.ninja2020.components.general.card-element-single', ['title' => 'ACH', 'show_title' => false]) + {{ ctrans('texts.bank_account_not_linked') }} + {{ ctrans('texts.add_payment_method') }} + @endcomponent + @endif + + @include('portal.ninja2020.gateways.includes.pay_now') +@endsection + +@push('footer') + +@endpush From d01c3ccdf29a9cddf1eff9db88fbec1917c6f2e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Thu, 30 Sep 2021 09:19:48 +0200 Subject: [PATCH 19/25] Add docblocks for methods --- app/PaymentDrivers/GoCardless/ACH.php | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/app/PaymentDrivers/GoCardless/ACH.php b/app/PaymentDrivers/GoCardless/ACH.php index ae3e2073d279..2dd94a453b25 100644 --- a/app/PaymentDrivers/GoCardless/ACH.php +++ b/app/PaymentDrivers/GoCardless/ACH.php @@ -26,6 +26,7 @@ use App\PaymentDrivers\Common\MethodInterface; use App\PaymentDrivers\GoCardlessPaymentDriver; use App\Utils\Traits\MakesHash; use Exception; +use GoCardlessPro\Resources\Payment as ResourcesPayment; use Illuminate\Http\RedirectResponse; use Illuminate\Routing\Redirector; use Illuminate\View\View; @@ -151,6 +152,12 @@ class ACH implements MethodInterface return render('gateways.gocardless.ach.pay', $data); } + /** + * Process payments for ACH. + * + * @param PaymentResponseRequest $request + * @return RedirectResponse|void + */ public function paymentResponse(PaymentResponseRequest $request) { $token = ClientGatewayToken::find( @@ -182,6 +189,13 @@ class ACH implements MethodInterface } } + /** + * Handle pending payments for ACH. + * + * @param ResourcesPayment $payment + * @param array $data + * @return RedirectResponse + */ public function processPendingPayment(\GoCardlessPro\Resources\Payment $payment, array $data = []) { $data = [ @@ -206,6 +220,12 @@ class ACH implements MethodInterface return redirect()->route('client.payments.show', ['payment' => $this->go_cardless->encodePrimaryKey($payment->id)]); } + /** + * Process unsuccessful payments for ACH. + * + * @param ResourcesPayment $payment + * @return never + */ public function processUnsuccessfulPayment(\GoCardlessPro\Resources\Payment $payment) { PaymentFailureMailer::dispatch($this->go_cardless->client, $payment->status, $this->go_cardless->client->company, $this->go_cardless->payment_hash->data->amount_with_fee); From f848fd83a8e69f612bdd30f1b2034b32687bb748 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Thu, 30 Sep 2021 10:03:44 +0200 Subject: [PATCH 20/25] Token billing --- .../GoCardlessPaymentDriver.php | 85 ++++++++++++++++++- 1 file changed, 83 insertions(+), 2 deletions(-) diff --git a/app/PaymentDrivers/GoCardlessPaymentDriver.php b/app/PaymentDrivers/GoCardlessPaymentDriver.php index d36074107945..a2448214fc6d 100644 --- a/app/PaymentDrivers/GoCardlessPaymentDriver.php +++ b/app/PaymentDrivers/GoCardlessPaymentDriver.php @@ -11,14 +11,16 @@ namespace App\PaymentDrivers; +use App\Jobs\Mail\PaymentFailureMailer; +use App\Jobs\Util\SystemLogger; use App\Models\ClientGatewayToken; use App\Models\GatewayType; use App\Models\Payment; use App\Models\PaymentHash; +use App\Models\PaymentType; use App\Models\SystemLog; use App\Utils\Traits\MakesHash; - class GoCardlessPaymentDriver extends BaseDriver { use MakesHash; @@ -100,7 +102,86 @@ class GoCardlessPaymentDriver extends BaseDriver public function tokenBilling(ClientGatewayToken $cgt, PaymentHash $payment_hash) { - // .. + $amount = array_sum(array_column($payment_hash->invoices(), 'amount')) + $payment_hash->fee_total; + $converted_amount = $this->convertToGoCardlessAmount($amount, $this->client->currency()->precision); + + $this->init(); + + try { + $payment = $this->gateway->payments()->create([ + 'params' => [ + 'amount' => $converted_amount, + 'currency' => $this->client->getCurrencyCode(), + 'metadata' => [ + 'payment_hash' => $this->payment_hash->hash, + ], + 'links' => [ + 'mandate' => $cgt->token, + ], + ], + ]); + + + if ($payment->status === 'pending_submission') { + $this->confirmGatewayFee(); + + $data = [ + 'payment_method' => $cgt->hashed_id, + 'payment_type' => PaymentType::ACH, + 'amount' => $amount, + 'transaction_reference' => $payment->id, + 'gateway_type_id' => GatewayType::BANK_TRANSFER, + ]; + + $payment = $this->createPayment($data, Payment::STATUS_COMPLETED); + + SystemLogger::dispatch( + ['response' => $payment, 'data' => $data], + SystemLog::CATEGORY_GATEWAY_RESPONSE, + SystemLog::EVENT_GATEWAY_SUCCESS, + SystemLog::TYPE_GOCARDLESS, + $this->client, + $this->client->company + ); + + return $payment; + } + + PaymentFailureMailer::dispatch( + $this->client, + $payment->status, + $this->client->company, + $amount + ); + + $message = [ + 'server_response' => $payment, + 'data' => $payment_hash->data, + ]; + + SystemLogger::dispatch( + $message, + SystemLog::CATEGORY_GATEWAY_RESPONSE, + SystemLog::EVENT_GATEWAY_FAILURE, + SystemLog::TYPE_GOCARDLESS, + $this->client, + $this->client->company + ); + + return false; + } catch (\Exception $exception) { + $this->unWindGatewayFees($this->payment_hash); + + $data = [ + 'status' => '', + 'error_type' => '', + 'error_code' => $exception->getCode(), + 'param' => '', + 'message' => $exception->getMessage(), + ]; + + SystemLogger::dispatch($data, SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_FAILURE, SystemLog::TYPE_GOCARDLESS, $this->client, $this->client->company); + } } public function convertToGoCardlessAmount($amount, $precision) From 42991e18133e24ab0c0a9cf024120387b6fe72e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Thu, 30 Sep 2021 14:55:46 +0200 Subject: [PATCH 21/25] Ability to detach payment method --- .../GoCardlessPaymentDriver.php | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/app/PaymentDrivers/GoCardlessPaymentDriver.php b/app/PaymentDrivers/GoCardlessPaymentDriver.php index a2448214fc6d..2e8990177d3f 100644 --- a/app/PaymentDrivers/GoCardlessPaymentDriver.php +++ b/app/PaymentDrivers/GoCardlessPaymentDriver.php @@ -188,4 +188,27 @@ class GoCardlessPaymentDriver extends BaseDriver { return \round(($amount * pow(10, $precision)), 0); } + + public function detach(ClientGatewayToken $token) + { + $this->init(); + + try { + $this->gateway->mandates()->cancel($token->token); + } catch (\Exception $e) { + nlog($e->getMessage()); + + SystemLogger::dispatch( + [ + 'server_response' => $e->getMessage(), + 'data' => request()->all(), + ], + SystemLog::CATEGORY_GATEWAY_RESPONSE, + SystemLog::EVENT_GATEWAY_FAILURE, + SystemLog::TYPE_GOCARDLESS, + $this->client, + $this->client->company + ); + } + } } From 2d824e4d1ece5b09a5052272cb81a802e5144e12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Thu, 30 Sep 2021 15:55:34 +0200 Subject: [PATCH 22/25] Handling webhooks --- .../GoCardlessPaymentDriver.php | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/app/PaymentDrivers/GoCardlessPaymentDriver.php b/app/PaymentDrivers/GoCardlessPaymentDriver.php index 2e8990177d3f..770a272c7d37 100644 --- a/app/PaymentDrivers/GoCardlessPaymentDriver.php +++ b/app/PaymentDrivers/GoCardlessPaymentDriver.php @@ -11,6 +11,7 @@ namespace App\PaymentDrivers; +use App\Http\Requests\Payments\PaymentWebhookRequest; use App\Jobs\Mail\PaymentFailureMailer; use App\Jobs\Util\SystemLogger; use App\Models\ClientGatewayToken; @@ -211,4 +212,42 @@ class GoCardlessPaymentDriver extends BaseDriver ); } } + + public function processWebhookRequest(PaymentWebhookRequest $request) + { + // Allow app to catch up with webhook request. + sleep(2); + + $this->init(); + + foreach ($request->events as $event) { + if ($event['action'] === 'confirmed') { + $payment = Payment::query() + ->where('transaction_reference', $event['links']['payment']) + ->where('company_id', $request->getCompany()->id) + ->first(); + + if ($payment) { + $payment->status_id = Payment::STATUS_COMPLETED; + $payment->save(); + } + } + + if ($event['action'] === 'failed') { + // Update invoices, etc? + + $payment = Payment::query() + ->where('transaction_reference', $event['links']['payment']) + ->where('company_id', $request->getCompany()->id) + ->first(); + + if ($payment) { + $payment->status_id = Payment::STATUS_FAILED; + $payment->save(); + } + } + } + + return response()->json([], 200); + } } From 078330e1f0726836a6cb775e5497273875b25936 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Mon, 4 Oct 2021 13:40:16 +0200 Subject: [PATCH 23/25] Scaffold ACHTest --- .../Gateways/GoCardless/ACHTest.php | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 tests/Browser/ClientPortal/Gateways/GoCardless/ACHTest.php diff --git a/tests/Browser/ClientPortal/Gateways/GoCardless/ACHTest.php b/tests/Browser/ClientPortal/Gateways/GoCardless/ACHTest.php new file mode 100644 index 000000000000..13783b328c4c --- /dev/null +++ b/tests/Browser/ClientPortal/Gateways/GoCardless/ACHTest.php @@ -0,0 +1,23 @@ +browse(function (Browser $browser) { + $browser->visit('/') + ->assertSee('Laravel'); + }); + } +} From cd3d0efb7ae280b86715dae4505187ed3dea740e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Mon, 4 Oct 2021 13:40:48 +0200 Subject: [PATCH 24/25] Tests: Setup --- .../Gateways/GoCardless/ACHTest.php | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/tests/Browser/ClientPortal/Gateways/GoCardless/ACHTest.php b/tests/Browser/ClientPortal/Gateways/GoCardless/ACHTest.php index 13783b328c4c..9de9e9736bff 100644 --- a/tests/Browser/ClientPortal/Gateways/GoCardless/ACHTest.php +++ b/tests/Browser/ClientPortal/Gateways/GoCardless/ACHTest.php @@ -2,22 +2,29 @@ namespace Tests\Browser\ClientPortal\Gateways\GoCardless; -use Illuminate\Foundation\Testing\DatabaseMigrations; +use App\Models\CompanyGateway; use Laravel\Dusk\Browser; +use Tests\Browser\Pages\ClientPortal\Login; use Tests\DuskTestCase; class ACHTest extends DuskTestCase { - /** - * A Dusk test example. - * - * @return void - */ - public function testExample() + protected function setUp(): void { + parent::setUp(); + + foreach (static::$browsers as $browser) { + $browser->driver->manage()->deleteAllCookies(); + } + + $this->disableCompanyGateways(); + + CompanyGateway::where('gateway_key', 'b9886f9257f0c6ee7c302f1c74475f6c')->restore(); + $this->browse(function (Browser $browser) { - $browser->visit('/') - ->assertSee('Laravel'); + $browser + ->visit(new Login()) + ->auth(); }); } } From 9b52ad39e9afa470d2f0da41fefd128cc3767596 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Mon, 4 Oct 2021 13:44:57 +0200 Subject: [PATCH 25/25] Tests: Paying without method preauthorized --- .../ClientPortal/Gateways/GoCardless/ACHTest.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/Browser/ClientPortal/Gateways/GoCardless/ACHTest.php b/tests/Browser/ClientPortal/Gateways/GoCardless/ACHTest.php index 9de9e9736bff..5867f8329f35 100644 --- a/tests/Browser/ClientPortal/Gateways/GoCardless/ACHTest.php +++ b/tests/Browser/ClientPortal/Gateways/GoCardless/ACHTest.php @@ -27,4 +27,16 @@ class ACHTest extends DuskTestCase ->auth(); }); } + + public function testPayingWithNoPreauthorizedIsntPossible() + { + $this->browse(function (Browser $browser) { + $browser + ->visitRoute('client.invoices.index') + ->click('@pay-now') + ->press('Pay Now') + ->clickLink('Bank Transfer') + ->assertSee('To pay with a bank account, first you have to add it as payment method.'); + }); + } }