Skip to content

Commit

Permalink
Add webhook support
Browse files Browse the repository at this point in the history
  • Loading branch information
hailwood committed Jan 30, 2020
1 parent 134545e commit 8397552
Show file tree
Hide file tree
Showing 5 changed files with 254 additions and 26 deletions.
70 changes: 70 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,76 @@ class XeroController extends Controller
Route::get('/manage/xero', [\App\Http\Controllers\XeroController::class, 'index'])->name('xero.auth.success');
```

## Using Webhooks
On your application in the Xero developer portal create a webhook to get your webhook key.

You can then add this to your .env file as

```
XERO_WEBHOOK_KEY=...
```

You can then setup a controller to handle your webhook and inject `\Webfox\Xero\Webhook` e.g.

```php
<?php

namespace App\Http\Controllers;

use Webfox\Xero\Webhook;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Webfox\Xero\WebhookEvent;
use XeroApi\XeroPHP\Models\Accounting\Contact;
use XeroApi\XeroPHP\Models\Accounting\Invoice;

class XeroWebhookController extends Controller
{
public function __invoke(Request $request, Webhook $webhook)
{

// The following lines are required for Xero's 'itent to receive' validation
if (!$webhook->validate($request->header('x-xero-signature'))) {
// We can't use abort here, since Xero expects no response body
return response('', Response::HTTP_UNAUTHORIZED);
}

// A single webhook trigger can contain multiple events, so we must loop over them
foreach ($webhook->getEvents() as $event) {
if ($event->getEventType() === 'CREATE' && $event->getEventCategory() === 'INVOICE') {
$this->invoiceCreated($request, $event->getResource());
} elseif ($event->getEventType() === 'CREATE' && $event->getEventCategory() === 'CONTACT') {
$this->contactCreated($request, $event->getResource());
} elseif ($event->getEventType() === 'UPDATE' && $event->getEventCategory() === 'INVOICE') {
$this->invoiceUpdated($request, $event->getResource());
} elseif ($event->getEventType() === 'UPDATE' && $event->getEventCategory() === 'CONTACT') {
$this->contactUpdated($request, $event->getResource());
}
}

return response('', Response::HTTP_OK);
}

protected function invoiceCreated(Request $request, Invoice $invoice)
{
}

protected function contactCreated(Request $request, Contact $contact)
{
}

protected function invoiceUpdated(Request $request, Invoice $invoice)
{
}

protected function contactUpdated(Request $request, Contact $contact)
{
}

}
```


## License

The MIT License (MIT). Please see [License File](LICENSE.md) for more information.
79 changes: 79 additions & 0 deletions src/Webhook.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?php

namespace Webfox\Xero;

use Illuminate\Support\Collection;
use XeroAPI\XeroPHP\Api\AccountingApi;

class Webhook
{

protected $signingKey;

protected $payload;

protected $properties;

protected $events;

protected $accountingApi;

protected OauthCredentialManager $credentialManager;

public function __construct(OauthCredentialManager $credentialManager, AccountingApi $accountingApi, string $payload, string $signingKey)
{
$this->accountingApi = $accountingApi;
$this->payload = $payload;
$this->signingKey = $signingKey;
$this->properties = new Collection(json_decode($payload, true));

// bail if json_decode fails
if ($this->properties->isEmpty()) {
throw new \Exception('The webhook payload could not be decoded: ' . json_last_error_msg());
}

// bail if we don't have all the fields we are expecting
if (!$this->properties->has(['events', 'firstEventSequence', 'lastEventSequence'])) {
throw new \Exception('The webhook payload was malformed');
}

$this->events = new Collection(array_map(function($event) {
return new WebhookEvent($this->credentialManager, $this->accountingApi, $event);
}, $this->properties->get('events')));
$this->credentialManager = $credentialManager;
}

public function getSignature()
{
return base64_encode(hash_hmac('sha256', $this->payload, $this->signingKey, true));
}

public function validate($signature)
{
return hash_equals($this->getSignature(), $signature);
}

/**
* @return int
*/
public function getFirstEventSequence()
{
return $this->properties->get('firstEventSequence');
}

/**
* @return int
*/
public function getLastEventSequence()
{
return $this->properties->get('lastEventSequence');
}

/**
* @return \Webfox\Xero\WebhookEvent[]|\Illuminate\Support\Collection
*/
public function getEvents()
{
return $this->events;
}
}
94 changes: 94 additions & 0 deletions src/WebhookEvent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
<?php


namespace Webfox\Xero;


use Illuminate\Support\Collection;
use XeroAPI\XeroPHP\Api\AccountingApi;

class WebhookEvent
{

protected $properties;

protected AccountingApi $accountingApi;

protected OauthCredentialManager $credentialManager;

public function __construct(OauthCredentialManager $credentialManager, AccountingApi $accountingApi, $event)
{
$this->accountingApi = $accountingApi;
$this->properties = new Collection($event);

if (!$this->properties->has(['resourceUrl', 'resourceId', 'eventDateUtc', 'eventType', 'eventCategory', 'tenantId', 'tenantType',])) {
throw new \Exception("The event payload was malformed; missing required field");
}
$this->credentialManager = $credentialManager;
}

public function getResourceUrl()
{
return $this->properties->get('resourceUrl');
}

public function getResourceId()
{
return $this->properties->get('resourceId');
}

public function getEventDateUtc()
{
return $this->properties->get('eventDateUtc');
}

public function getEventDate()
{
return new \DateTime($this->getEventDateUtc());
}

public function getEventType()
{
return $this->properties->get('eventType');
}

public function getEventCategory()
{
return $this->properties->get('eventCategory');
}

public function getEventClass()
{
if ($this->getEventCategory() === 'INVOICE') {
return \XeroApi\XeroPHP\Models\Accounting\Invoice::class;
}
if ($this->getEventCategory() === 'CONTACT') {
return \XeroApi\XeroPHP\Models\Accounting\Contact::class;
}

}

public function getTenantId()
{
return $this->properties->get('tenantId');
}

public function getTenantType()
{
return $this->properties->get('tenantType');
}

public function getResource()
{
if ($this->getEventCategory() === 'INVOICE') {
return $this->accountingApi
->getInvoice($this->credentialManager->getTenantId(), $this->getResourceId())
->getInvoices()[0];
}
if ($this->getEventCategory() === 'CONTACT') {
return $this->accountingApi
->getContact($this->credentialManager->getTenantId(), $this->getResourceId())
->getContacts()[0];
}
}
}
25 changes: 0 additions & 25 deletions src/XeroClass.php

This file was deleted.

12 changes: 11 additions & 1 deletion src/XeroServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Webfox\Xero;

use Illuminate\Http\Request;
use XeroAPI\XeroPHP\Configuration;
use XeroAPI\XeroPHP\Api\IdentityApi;
use GuzzleHttp\Client as GuzzleClient;
Expand All @@ -16,7 +17,7 @@ class XeroServiceProvider extends ServiceProvider
*/
public function boot()
{
$this->loadRoutesFrom(__DIR__.'/../routes/routes.php');
$this->loadRoutesFrom(__DIR__ . '/../routes/routes.php');

if ($this->app->runningInConsole()) {
$this->publishes([
Expand Down Expand Up @@ -73,5 +74,14 @@ public function register()
$this->app->singleton(AccountingApi::class, function (Application $app) {
return new AccountingApi(new GuzzleClient(), $app->make(Configuration::class));
});

$this->app->bind(Webhook::class, function(Application $app) {
return new Webhook(
$app->make(OauthCredentialManager::class),
$app->make(AccountingApi::class),
$this->app->make(Request::class)->getContent(),
config('xero.oauth.webhook_signing_key')
);
});
}
}

0 comments on commit 8397552

Please sign in to comment.