diff --git a/.env.example b/.env.example index 3f43511243f0..107e27b0a143 100644 --- a/.env.example +++ b/.env.example @@ -65,4 +65,7 @@ APPLE_REDIRECT_URI= NORDIGEN_SECRET_ID= NORDIGEN_SECRET_KEY= +GOCARDLESS_CLIENT_ID= +GOCARDLESS_CLIENT_SECRET= + OPENEXCHANGE_APP_ID= diff --git a/app/Http/Controllers/Gateways/GoCardlessOAuthController.php b/app/Http/Controllers/Gateways/GoCardlessOAuthController.php new file mode 100644 index 000000000000..b4aa0688fa9b --- /dev/null +++ b/app/Http/Controllers/Gateways/GoCardlessOAuthController.php @@ -0,0 +1,109 @@ +getCompany(); + + $params = [ + 'client_id' => config('services.gocardless.client_id'), + 'redirect_uri' => config('services.gocardless.redirect_uri'), + 'scope' => 'read_write', + 'response_type' => 'code', + 'state' => $company->company_key, + 'prefill[email]' => $company->settings->email, + 'prefill[organisation_name]' => $company->settings->name, + 'prefill[country_code]' => $company->country()->iso_3166_2, + ]; + + $url = config('services.gocardless.environment') === 'production' + ? 'https://connect.gocardless.com/oauth/authorize?%s' + : 'https://connect-sandbox.gocardless.com/oauth/authorize?%s'; + + + return redirect()->to( + sprintf($url, http_build_query($params)) + ); + } + + public function confirm(OAuthConnectConfirmRequest $request): \Illuminate\Http\RedirectResponse|\Illuminate\View\View + { + /** @var \App\Models\Company $company */ + $company = $request->getCompany(); + + // LBo0v_561xgFGnFUae6uEQEfrWoSEMnZ&state=5O2O85C8dPv1Gp1UPVq0xs4FVTZdq5dO + // https://invoicing.co/gocardless/oauth/connect/confirm?code=sH55_xb-2s1JtuEw-j7W0hT0Z1sFkM7l + + $url = config('services.gocardless.environment') === 'production' + ? 'https://connect.gocardless.com/oauth/access_token' + : 'https://connect-sandbox.gocardless.com/oauth/access_token'; + + $response = Http::post($url, [ + 'client_id' => config('services.gocardless.client_id'), + 'client_secret' => config('services.gocardless.client_secret'), + 'grant_type' => 'authorization_code', + 'code' => $request->query('code'), + 'redirect_uri' => config('services.gocardless.redirect_uri'), + ]); + + if ($response->failed()) { + return view('auth.gocardless_connect.access_denied'); + } + + $company_gateway = CompanyGateway::query() + ->where('gateway_key', 'b9886f9257f0c6ee7c302f1c74475f6c') + ->where('company_id', $company->id) + ->first(); + + if ($company_gateway === null) { + $company_gateway = CompanyGatewayFactory::create($company->id, $company->owner()->id); + $fees_and_limits = new \stdClass(); + $fees_and_limits->{GatewayType::INSTANT_BANK_PAY} = new FeesAndLimits(); + $company_gateway->gateway_key = 'b9886f9257f0c6ee7c302f1c74475f6c'; + $company_gateway->fees_and_limits = $fees_and_limits; + $company_gateway->setConfig([]); + } + + $response = $response->json(); + + $payload = [ + '__current' => $company_gateway->getConfig(), + 'account_id' => $response['organisation_id'], + 'token_type' => $response['token_type'], + 'scope' => $response['scope'], + 'active' => $response['active'], + 'accessToken' => $response['access_token'], + 'testMode' => $company_gateway->getConfigField('testMode'), + 'oauth2' => true, + ]; + + $company_gateway->setConfig($payload); + $company_gateway->save(); + + return view('auth.gocardless_connect.completed'); + } +} diff --git a/app/Http/Controllers/Gateways/GoCardlessOAuthWebhookController.php b/app/Http/Controllers/Gateways/GoCardlessOAuthWebhookController.php new file mode 100644 index 000000000000..0d91a288225f --- /dev/null +++ b/app/Http/Controllers/Gateways/GoCardlessOAuthWebhookController.php @@ -0,0 +1,53 @@ +events as $event) { + nlog($event['action']); + + $e = Arr::dot($event); + + if ($event['action'] === 'disconnected') { + + /** @var \App\Models\CompanyGateway $company_gateway */ + $company_gateway = CompanyGateway::query() + ->whereJsonContains('config->account_id', $e['links.organisation']) + ->firstOrFail(); + + $current = $company_gateway->getConfig('__current'); + + if ($current) { + $company_gateway->setConfig($current); + $company_gateway->save(); + } + + $this->company_repository->archive($company_gateway); + } + } + } +} diff --git a/app/Http/Requests/GoCardless/OAuthConnectConfirmRequest.php b/app/Http/Requests/GoCardless/OAuthConnectConfirmRequest.php new file mode 100644 index 000000000000..45db2feac5f3 --- /dev/null +++ b/app/Http/Requests/GoCardless/OAuthConnectConfirmRequest.php @@ -0,0 +1,47 @@ +|string> + */ + public function rules(): array + { + return [ + 'state' => ['required', 'string'], + 'code' => ['required','string'], + ]; + } + + public function getCompany(): \Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\Builder|\App\Models\BaseModel + { + MultiDB::findAndSetDbByCompanyKey( + $this->query('state'), + ); + + return Company::query() + ->where('company_key', $this->query('state')) + ->firstOrFail(); + } +} diff --git a/app/Http/Requests/GoCardless/OAuthConnectRequest.php b/app/Http/Requests/GoCardless/OAuthConnectRequest.php new file mode 100644 index 000000000000..56722de8912d --- /dev/null +++ b/app/Http/Requests/GoCardless/OAuthConnectRequest.php @@ -0,0 +1,51 @@ +|string> + */ + public function rules(): array + { + return [ + // + ]; + } + + public function getCompany(): \Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\Builder|\App\Models\BaseModel + { + $data = Cache::get( + key: $this->token, + ); + + MultiDB::findAndSetDbByCompanyKey( + company_key: $data['company_key'], + ); + + return Company::query() + ->where('company_key', $data['company_key']) + ->firstOrFail(); + } +} diff --git a/app/Http/Requests/GoCardless/WebhookRequest.php b/app/Http/Requests/GoCardless/WebhookRequest.php new file mode 100644 index 000000000000..41d5b709f4c5 --- /dev/null +++ b/app/Http/Requests/GoCardless/WebhookRequest.php @@ -0,0 +1,34 @@ +|string> + */ + public function rules(): array + { + return [ + 'events' => ['required', 'array'], + 'events.*.resource_type' => ['required', 'string'], + ]; + } +} diff --git a/config/services.php b/config/services.php index 8ec5a41d7f8e..35f9ceea31f5 100644 --- a/config/services.php +++ b/config/services.php @@ -131,6 +131,12 @@ return [ 'client_id' => env('CHORUS_CLIENT_ID', false), 'secret' => env('CHORUS_SECRET', false), ], + 'gocardless' => [ + 'client_id' => env('GOCARDLESS_CLIENT_ID', null), + 'client_secret' => env('GOCARDLESS_CLIENT_SECRET', null), + 'environment' => env('GOCARDLESS_ENVIRONMENT', 'production'), + 'redirect_uri' => env('GOCARDLESS_REDIRECT_URI', 'https://invoicing.co/gocardless/oauth/connect/confirm'), + ], 'quickbooks' => [ 'client_id' => env('QUICKBOOKS_CLIENT_ID', false), 'client_secret' => env('QUICKBOOKS_CLIENT_SECRET', false), @@ -139,4 +145,4 @@ return [ 'quickbooks_webhook' => [ 'verifier_token' => env('QUICKBOOKS_VERIFIER_TOKEN', false), ], -]; \ No newline at end of file +]; diff --git a/resources/views/auth/gocardless_connect/access_denied.blade.php b/resources/views/auth/gocardless_connect/access_denied.blade.php new file mode 100644 index 000000000000..2ed1404d122a --- /dev/null +++ b/resources/views/auth/gocardless_connect/access_denied.blade.php @@ -0,0 +1,30 @@ +@extends('layouts.ninja') +@section('meta_title', ctrans('texts.error_title')) + +@section('body') +
+
+ + + + + +
+ +

We were unable to connect to GoCardless as access was denied.

+ Click here to + continue. +
+@endsection \ No newline at end of file diff --git a/resources/views/auth/gocardless_connect/completed.blade.php b/resources/views/auth/gocardless_connect/completed.blade.php new file mode 100644 index 000000000000..85f8a111eb7d --- /dev/null +++ b/resources/views/auth/gocardless_connect/completed.blade.php @@ -0,0 +1,31 @@ +@extends('layouts.ninja') +@section('meta_title', ctrans('texts.success')) + +@section('body') +
+
+ + + + + +
+ +

Connecting your account using GoCardless has been successfully completed.

+ Click here to + continue. +
+@endsection \ No newline at end of file diff --git a/routes/web.php b/routes/web.php index df8eba49f21a..107bcba8584d 100644 --- a/routes/web.php +++ b/routes/web.php @@ -10,6 +10,8 @@ use App\Http\Controllers\ClientPortal\ApplePayDomainController; use App\Http\Controllers\EInvoice\SelfhostController; use App\Http\Controllers\Gateways\Checkout3dsController; use App\Http\Controllers\Gateways\GoCardlessController; +use App\Http\Controllers\Gateways\GoCardlessOAuthController; +use App\Http\Controllers\Gateways\GoCardlessOAuthWebhookController; use App\Http\Controllers\Gateways\Mollie3dsController; use App\Http\Controllers\SetupController; use App\Http\Controllers\StripeConnectController; @@ -51,6 +53,10 @@ Route::get('mollie/3ds_redirect/{company_key}/{company_gateway_id}/{hash}', [Mol Route::get('gocardless/ibp_redirect/{company_key}/{company_gateway_id}/{hash}', [GoCardlessController::class, 'ibpRedirect'])->middleware('domain_db')->name('gocardless.ibp_redirect'); Route::get('.well-known/apple-developer-merchantid-domain-association', [ApplePayDomainController::class, 'showAppleMerchantId']); +Route::get('gocardless/oauth/connect/confirm', [GoCardlessOAuthController::class, 'confirm'])->name('gocardless.oauth.confirm'); +Route::post('gocardless/oauth/connect/webhook', GoCardlessOAuthWebhookController::class)->name('gocardless.oauth.webhook'); +Route::get('gocardless/oauth/connect/{token}', [GoCardlessOAuthController::class, 'connect']); + Route::get('einvoice/beta', [SelfhostController::class, 'index'])->name('einvoice.beta'); \Illuminate\Support\Facades\Broadcast::routes(['middleware' => ['token_auth']]);