├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── src ├── Facades │ └── Gocardless.php ├── Controllers │ └── GocardlessWebhookController.php ├── Middlewares │ └── VerifySignature.php ├── config │ └── gocardless.php ├── Exceptions │ └── WebhookFailed.php ├── GocardlessWebhookCall.php └── GocardlessServiceProvider.php ├── phpunit.xml ├── database └── migrations │ └── create_gocardless_webhook_calls_table.php.stub ├── composer.json ├── LICENSE ├── tests └── FacadeTest.php └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | \.idea/ 3 | composer.lock 4 | vendor/ 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | php: 3 | - 7.1 4 | - 7.2 5 | - 7.3 6 | 7 | sudo: false 8 | 9 | cache: 10 | directories: 11 | - vendor 12 | - $HOME/.composer/cache 13 | 14 | install: 15 | - composer install -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ## [0.1.0] - 2019-02-28 10 | ### Added 11 | - A new facade to wrap the client 12 | - The basic Service Provider 13 | - Basic project structure 14 | - The basic Gocardless config file 15 | - Basic Facade unit test 16 | 17 | ### Changed 18 | 19 | ### Removed 20 | 21 | ## [0.1.1] - 2019-02-28 22 | ### Added 23 | - Publishable config file 24 | 25 | ### Changed 26 | 27 | ### Removed 28 | 29 | ## [0.1.2] - 2019-02-28 30 | ### Added 31 | 32 | ### Changed 33 | - Config route 34 | ### Removed -------------------------------------------------------------------------------- /src/Facades/Gocardless.php: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | ./tests/ 16 | 17 | 18 | 19 | 20 | src/ 21 | 22 | ./src/GocardlessServiceProvider.php 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /database/migrations/create_gocardless_webhook_calls_table.php.stub: -------------------------------------------------------------------------------- 1 | increments('id'); 17 | 18 | $table->string('resource_type')->nullable(); 19 | $table->string('action')->nullable(); 20 | $table->text('payload')->nullable(); 21 | $table->text('exception')->nullable(); 22 | 23 | $table->timestamps(); 24 | }); 25 | } 26 | 27 | /** 28 | * Reverse the migrations. 29 | * 30 | * @return void 31 | */ 32 | public function down() 33 | { 34 | Schema::dropIfExists('gocardless_webhook_calls'); 35 | } 36 | } -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nestednet/gocardless-laravel", 3 | "description": "GoCardless Pro PHP Client package integration for Laravel.", 4 | "keywords": [ 5 | "php", 6 | "nestednet", 7 | "laravel", 8 | "gocardless" 9 | ], 10 | "license": "MIT", 11 | "authors": [ 12 | { 13 | "name": "Nested", 14 | "email": "dev@nested.net", 15 | "homepage": "https://nested.net" 16 | } 17 | ], 18 | "require": { 19 | "php": ">=7.1.3", 20 | "illuminate/support": ">=5.5.0", 21 | "gocardless/gocardless-pro": "^3.0" 22 | }, 23 | "require-dev": { 24 | "phpunit/phpunit": "~7.0" 25 | }, 26 | "autoload": { 27 | "psr-4": { 28 | "Nestednet\\Gocardless\\": "src/" 29 | } 30 | }, 31 | "extra": { 32 | "laravel": { 33 | "providers": [ 34 | "Nestednet\\Gocardless\\GocardlessServiceProvider" 35 | ], 36 | "aliases": { 37 | "Gocardless": "Nestednet\\Gocardless\\Facades\\Gocardless" 38 | } 39 | } 40 | }, 41 | "minimum-stability": "stable" 42 | } 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Nested.net 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/FacadeTest.php: -------------------------------------------------------------------------------- 1 | assertTrue($reflection->isSubclassOf($facade)); 34 | } 35 | 36 | /** @test */ 37 | public function it_can_test_it_is_a_facade_accessor() 38 | { 39 | $reflection = new ReflectionClass('Nestednet\Gocardless\Facades\Gocardless'); 40 | 41 | $method = $reflection->getMethod('getFacadeAccessor'); 42 | $method->setAccessible(true); 43 | 44 | $this->assertEquals('gocardless', $method->invoke(null)); 45 | } 46 | } -------------------------------------------------------------------------------- /src/Controllers/GocardlessWebhookController.php: -------------------------------------------------------------------------------- 1 | middleware(VerifySignature::class); 21 | } 22 | 23 | public function __invoke(Request $request) 24 | { 25 | $payload = $request->input(); 26 | $modelClass = config('gocardless.webhooks.model'); 27 | 28 | foreach ($payload['events'] as $event) { 29 | $gocardlessWebhookCall = $modelClass::create([ 30 | 'resource_type' => $event['resource_type'] ?? '', 31 | 'action' => $event['action'] ?? '', 32 | 'payload' => $event, 33 | ]); 34 | 35 | try { 36 | $gocardlessWebhookCall->process(); 37 | } catch (Exception $exception) { 38 | $gocardlessWebhookCall->saveException($exception); 39 | //Improve the way we handle the exceptions here, add the option to notify the exceptions. 40 | } 41 | } 42 | 43 | return response()->json(['message' => 'ok']); 44 | } 45 | } -------------------------------------------------------------------------------- /src/Middlewares/VerifySignature.php: -------------------------------------------------------------------------------- 1 | header('Webhook-Signature'); 21 | 22 | if (!$signature) { 23 | throw WebhookFailed::missingSignature(); 24 | } 25 | 26 | if (!$this->isValid($signature, $request->getContent(), $request->route('configKey'))) { 27 | throw WebhookFailed::invalidSignature($signature); 28 | } 29 | 30 | return $next($request); 31 | } 32 | 33 | protected function isValid(string $signature, string $payload, string $configKey = null) : bool 34 | { 35 | $secret = ($configKey) ? 36 | config('gocardless.webhooks.webhook_endpoint_secret_' . $configKey) : config('gocardless.webhooks.webhook_endpoint_secret'); 37 | 38 | if (empty($secret)) { 39 | throw WebhookFailed::noSecretKeyProvided(); 40 | } 41 | 42 | try { 43 | Webhook::parse($payload, $signature, $secret); 44 | } catch (Exception $e) { 45 | return false; 46 | } 47 | 48 | return true; 49 | } 50 | 51 | } -------------------------------------------------------------------------------- /src/config/gocardless.php: -------------------------------------------------------------------------------- 1 | env('GOCARDLESS_ENVIRONMENT', 'SANDBOX'), 26 | 27 | /** 28 | * Your Gocardless API token. 29 | */ 30 | 'token' => env('GOCARDLESS_TOKEN'), 31 | 32 | 'webhooks' => [ 33 | 34 | /** 35 | * Your Gocardless webhook secret endpoint. 36 | */ 37 | 'webhook_endpoint_secret' => env('GOCARDLESS_WEBHOOK_ENDPOINT_SECRET'), 38 | 39 | /** 40 | * Your Gocardless webhook model. The class should be or extend GocardlessWebhookCall. 41 | */ 42 | 'model' => Nestednet\Gocardless\GocardlessWebhookCall::class, 43 | 44 | /** 45 | * Define here the jobs that should run when a gocardless webhook hits your application. 46 | * The key is: {event_resource}_{event_action} 47 | */ 48 | 'jobs' => [ 49 | // 'payments_confirmed' => \App\Jobs\GocardlessWebhooks\HandleConfirmedPayment::class 50 | ], 51 | ], 52 | ]; -------------------------------------------------------------------------------- /src/Exceptions/WebhookFailed.php: -------------------------------------------------------------------------------- 1 | id}` of type `{$webhookCall->type} because the configured jobclass `$jobClass` does not exist."); 34 | } 35 | 36 | public static function missingResource(GocardlessWebhookCall $webhookCall) 37 | { 38 | return new static("Webhook call id `{$webhookCall->id}` did not contain a resource type. Valid Gocardless webhook calls should always contain a resource type."); 39 | } 40 | 41 | public static function missingAction(GocardlessWebhookCall $webhookCall) 42 | { 43 | return new static("Webhook call id `{$webhookCall->id}` did not contain an action. Valid Gocardless webhook calls should always contain an action."); 44 | } 45 | 46 | public function render($request) 47 | { 48 | return response(['error' => $this->getMessage()], 400); 49 | } 50 | 51 | } -------------------------------------------------------------------------------- /src/GocardlessWebhookCall.php: -------------------------------------------------------------------------------- 1 | 'array', 21 | 'exception' => 'array', 22 | ]; 23 | 24 | public function process() 25 | { 26 | $this->clearException(); 27 | 28 | if ($this->resource_type === '') { 29 | throw WebhookFailed::missingResource($this); 30 | } 31 | 32 | if ($this->action === '') { 33 | throw WebhookFailed::missingAction($this); 34 | } 35 | 36 | event("gocardless-webhooks::{$this->resource_type}_{$this->action}", $this); 37 | 38 | $jobClass = $this->determineJobClass($this->resource_type, $this->action); 39 | 40 | if ($jobClass === "") { 41 | return; 42 | } 43 | 44 | if (! class_exists($jobClass)) { 45 | throw WebhookFailed::jobClassDoesNotExist($jobClass, $this); 46 | } 47 | 48 | dispatch(new $jobClass($this)); 49 | } 50 | 51 | public function saveException(Exception $exception) 52 | { 53 | $this->exception = [ 54 | 'code' => $exception->getCode(), 55 | 'message' => $exception->getMessage(), 56 | 'trace' => $exception->getTraceAsString(), 57 | ]; 58 | 59 | $this->save(); 60 | 61 | return $this; 62 | } 63 | 64 | protected function determineJobClass(string $resourceType, string $action) : string 65 | { 66 | $formattedResourceType = str_replace('.', '_', $resourceType); 67 | $formattedAction = str_replace('.', '_', $action); 68 | 69 | $jobClassKey = "{$formattedResourceType}_{$formattedAction}"; 70 | 71 | return config("gocardless.webhooks.jobs.{$jobClassKey}", ""); 72 | } 73 | 74 | protected function clearException() 75 | { 76 | $this->exception = null; 77 | 78 | $this->save(); 79 | 80 | return $this; 81 | } 82 | 83 | } -------------------------------------------------------------------------------- /src/GocardlessServiceProvider.php: -------------------------------------------------------------------------------- 1 | publishes([ 34 | __DIR__.'/config/gocardless.php' => config_path('gocardless.php'), 35 | ], 'config'); 36 | 37 | if (! class_exists('CreateGocardlessWebhookClassTable')) { 38 | $timestamp = date('Y_m_d_His', time()); 39 | 40 | $this->publishes([ 41 | __DIR__.'/../database/migrations/create_gocardless_webhook_calls_table.php.stub' => database_path('migrations/'.$timestamp.'_create_gocardless_webhook_calls_table.php'), 42 | ], 'migrations'); 43 | } 44 | 45 | Route::macro('gocardlessWebhooks', function ($url) { 46 | return Route::post($url, '\Nestednet\Gocardless\Controllers\GocardlessWebhookController'); 47 | }); 48 | } 49 | 50 | /** 51 | * {@inheritDoc} d 52 | */ 53 | public function register() 54 | { 55 | $this->mergeConfigFrom(__DIR__ . '/config/gocardless.php', 'gocardless'); 56 | 57 | $this->registerGocardless(); 58 | } 59 | 60 | /** 61 | * {@inheritDoc} 62 | */ 63 | public function provides() 64 | { 65 | return [ 66 | 'gocardless', 67 | ]; 68 | } 69 | 70 | /** 71 | * Register the Gocardless API class. 72 | * 73 | * @return void 74 | */ 75 | protected function registerGocardless() 76 | { 77 | $this->app->singleton('gocardless', function ($app) { 78 | $config = $app['config']->get('gocardless'); 79 | $token = isset($config['token']) ? $config['token'] : null; 80 | $environment = isset($config['environment']) ? $config['environment'] : null; 81 | return new Client( array ( 82 | 'access_token' => $token, 83 | 'environment' => $environment 84 | )); 85 | }); 86 | $this->app->alias('gocardless', 'GoCardlessPro\Client'); 87 | } 88 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gocardless-laravel 2 | [![Build Status](https://travis-ci.com/Nestednet/gocardless-laravel.svg?branch=master)](https://travis-ci.com/Nestednet/gocardless-laravel) 3 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/Nestednet/gocardless-laravel/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/Nestednet/gocardless-laravel/?branch=master) 4 | [![Latest Stable Version](https://poser.pugx.org/nestednet/gocardless-laravel/v/stable)](https://packagist.org/packages/nestednet/gocardless-laravel) 5 | [![Latest Unstable Version](https://poser.pugx.org/nestednet/gocardless-laravel/v/unstable)](https://packagist.org/packages/nestednet/gocardless-laravel) 6 | [![License](https://poser.pugx.org/nestednet/gocardless-laravel/license)](https://packagist.org/packages/nestednet/gocardless-laravel) 7 | 8 | ##### GoCardless Pro PHP Client package integration for Laravel. 9 | 10 | This package tries to provide an easy, scalable and maintainable way to integrate Gocardless into your laravel project. 11 | 12 | It provides a Facade that wraps the Gocardless PHP client and also an easy way to handle the webhooks that Gocardless sends. This is done following the steps that [Spatie](https://spatie.be/opensource/php) uses for their [Stripe Laravel Webhooks package](https://github.com/spatie/laravel-stripe-webhooks). 13 | 14 | ##### Installation 15 | 16 | Get the package with composer: 17 | 18 | ```bash 19 | composer require nestednet/gocardless-laravel 20 | ``` 21 | 22 | **1.** If you are using Laravel >5.5 the package will be autodiscobered, for older versions add the service provider at your config/app.php file. 23 | 24 | **2.** Publish the configuration file 25 | ```bash 26 | $ php artisan vendor:publish --provider="Nestednet\Gocardless\GocardlessServiceProvider" 27 | ``` 28 | This will publish both the configuration file and the migration file. 29 | 30 | **3.** Review the configuration file 31 | ``` 32 | config/gocardless.php 33 | ``` 34 | and add your Gocardless API token and environment to the `.env` file. 35 | 36 | **4.** After publishing the migration you can run the migration and create the `gocardless_webhooks_table` 37 | 38 | **5.** The package provides a Macro route (`gocardlessWebhooks`). You can create a route at your routes file of your app. This route will be the endpoint where Gocardless will send the webhooks, you should register this webhook endpoint at your Gocardless dashboard. 39 | 40 | ```php 41 | Route::gocardlessWebhooks('gocardless-webhook-endpoint'); 42 | ``` 43 | 44 | This will register a `POST` route to a the controller provided by this package. You should add the route to the `except` array of the `VerifyCsrfToken` middleware. 45 | 46 | ```php 47 | protected $except = [ 48 | 'gocardless-webhook-endpoint', 49 | ]; 50 | ``` 51 | 52 | ##### Usage 53 | 54 | Once the package is properly installed you can use the `Gocardless` facade to access the methods of the Gocardless PHP client. The documentation of this methods can be found here: [Gocardless PHP cleint documentation](https://github.com/gocardless/gocardless-pro-php) 55 | 56 | If you use Gocardless at your project you provably will use webhooks to handle the asynchronous payment states. This package provides an easy way to handle the webhooks. 57 | 58 | Gocardless will send you webhooks with events. This events will contain the updates of your Gocardless resources. 59 | 60 | This package will verify the signature of the requests and if it's valid. Unless something goes terribly wrong, and even if one of the events inside the webhook fails the controller will reposnd with a `200` to Gocardless. This prevents Gocardless from spamming retries to the endpoint. 61 | 62 | If an event fails to be processed the exception will be saved to the database into the `gocardless_webhook_calls` table, you can find the failed events there. 63 | 64 | This package provides two ways to handle the webhook requests: 65 | 66 | * Using jobs 67 | * Using events 68 | 69 | ##### Using jobs 70 | You can find a jobs array inside the `config\gocardless.php`. 71 | 72 | You can register any job that you want to the gocardless events. An event from Gocardless references one resource `resource_type` and one `action`. In order to register a job to an action you should add it with the key `{resource_type}_{action}`. 73 | ```php 74 | 'jobs' => [ 75 | // '{resource_type}_{action} => path/to/job::class, 76 | 'payments_created' => App\Jobs\PaymentConfirmed::class, 77 | ] 78 | ``` 79 | 80 | In order to avoid timeouts it's highly recommended to use queued jobs. 81 | 82 | ##### Using events 83 | 84 | Every time an event is processed by the package it will trigger an event with this structure: 85 | 86 | `gocardless-webhooks::{resource_type}_{action}` 87 | 88 | The payload of the event will be the `GocardlessWebhookCall` (or an extended model) instance created with the request. 89 | 90 | You can register listeners to this events in the `EventServiceProvider`: 91 | 92 | ```php 93 | /** 94 | * The event listener mappings for the application. 95 | * 96 | * @var array 97 | */ 98 | protected $listen = [ 99 | 'gocardless-webhooks::payments_created' => [ 100 | App\Listeners\ListenerOfPaymentsCreated::class, 101 | ], 102 | ]; 103 | ``` 104 | 105 | --------------------------------------------------------------------------------