Skip to content

Commit

Permalink
Merge pull request #57 from amcintosh/issue_44_webhook_callbacks
Browse files Browse the repository at this point in the history
✨ Webhook Callbacks endpoint CRUD methods
  • Loading branch information
amcintosh committed Oct 2, 2023
2 parents 0602c9b + 0eb7ec8 commit 9dbb830
Show file tree
Hide file tree
Showing 8 changed files with 350 additions and 8 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## Unreleased

- Added Webhook Callback resource
- Remove warnings in PHP 8.2
- Handle new API version accounting errors

Expand Down
14 changes: 14 additions & 0 deletions src/FreshBooksClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
use Psr\Http\Message\StreamFactoryInterface;
use amcintosh\FreshBooks\Exception\FreshBooksClientConfigException;
use amcintosh\FreshBooks\Model\AuthorizationToken;
use amcintosh\FreshBooks\Model\Callback;
use amcintosh\FreshBooks\Model\CallbackList;
use amcintosh\FreshBooks\Model\Client;
use amcintosh\FreshBooks\Model\ClientList;
use amcintosh\FreshBooks\Model\Expense;
Expand All @@ -37,6 +39,7 @@
use amcintosh\FreshBooks\Model\TaxList;
use amcintosh\FreshBooks\Resource\AccountingResource;
use amcintosh\FreshBooks\Resource\AuthResource;
use amcintosh\FreshBooks\Resource\EventsResource;
use amcintosh\FreshBooks\Resource\ProjectResource;

class FreshBooksClient
Expand Down Expand Up @@ -302,6 +305,17 @@ public function taxes(): AccountingResource
return new AccountingResource($this->httpClient, 'taxes/taxes', Tax::class, TaxList::class);
}

/**
* FreshBooks callbacks (webhook callbacks) resource with calls to
* get, list, create, update, delete, resend_verification, verify
*
* @return EventsResource
*/
public function callbacks(): EventsResource
{
return new EventsResource($this->httpClient, 'events/callbacks', Callback::class, CallbackList::class);
}

/**
* FreshBooks projects resource with calls to get, list, create, update, delete.
*
Expand Down
73 changes: 73 additions & 0 deletions src/Model/Callback.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<?php

declare(strict_types=1);

namespace amcintosh\FreshBooks\Model;

use DateTimeImmutable;
use Spatie\DataTransferObject\Attributes\CastWith;
use Spatie\DataTransferObject\Attributes\MapFrom;
use Spatie\DataTransferObject\Attributes\MapTo;
use Spatie\DataTransferObject\Caster;
use Spatie\DataTransferObject\DataTransferObject;
use amcintosh\FreshBooks\Model\Caster\ISODateTimeImmutableCaster;
use amcintosh\FreshBooks\Model\DataModel;

/**
* Webhook callback subscription model.
*
* @package amcintosh\FreshBooks\Model
* @link https://www.freshbooks.com/api/webhooks
*/
class Callback extends DataTransferObject implements DataModel
{
public const RESPONSE_FIELD = 'callback';

/**
* @var int Get the unique identifier of this callback within this business.
*/
#[MapFrom('callbackid')]
public ?int $callbackId;

/**
* @var string The event to register the webhook callback for (eg. `invoice.create`).
*/
public ?string $event;

/**
* @var DateTimeImmutable The time of last modification.
*/
#[MapFrom('updated_at')]
#[CastWith(ISODateTimeImmutableCaster::class)]
public ?DateTimeImmutable $updatedAt;

/**
* @var string The URI to send the webhook callback to.
*/
public ?string $uri;

/**
* @var bool Whether the callback has been verified against the URI.
*/
public ?bool $verified;

/**
* Get the data as an array to POST or PUT to FreshBooks, removing any read-only fields.
*
* @return array
*/
public function getContent(): array
{
$data = $this
->except('callbackId')
->except('updatedAt')
->except('verified')
->toArray();
foreach ($data as $key => $value) {
if (is_null($value)) {
unset($data[$key]);
}
}
return $data;
}
}
24 changes: 24 additions & 0 deletions src/Model/CallbackList.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

declare(strict_types=1);

namespace amcintosh\FreshBooks\Model;

use Spatie\DataTransferObject\Attributes\CastWith;
use Spatie\DataTransferObject\Casters\ArrayCaster;
use amcintosh\FreshBooks\Model\AccountingList;
use amcintosh\FreshBooks\Model\Callback;

/**
* Results of callbacks list call containing list of callbacks and pagination data.
*
* @package amcintosh\FreshBooks\Model
* @link https://www.freshbooks.com/api/webhooks
*/
class CallbackList extends AccountingList
{
public const RESPONSE_FIELD = 'callbacks';

#[CastWith(ArrayCaster::class, itemType: Callback::class)]
public array $callbacks;
}
17 changes: 9 additions & 8 deletions src/Resource/AccountingResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@

class AccountingResource extends BaseResource
{
private HttpClient $httpClient;
private string $accountingPath;
private string $singleModel;
private string $listModel;
private bool $deleteViaUpdate;
private ?array $missingEndpoints;
protected HttpClient $httpClient;
protected string $accountingPath;
protected string $singleModel;
protected string $listModel;
protected bool $deleteViaUpdate;
protected ?array $missingEndpoints;

public function __construct(
HttpClient $httpClient,
Expand All @@ -45,7 +45,7 @@ public function __construct(
* @param int $resourceId
* @return string
*/
private function getUrl(string $accountId, int $resourceId = null): string
protected function getUrl(string $accountId, int $resourceId = null): string
{
if (!is_null($resourceId)) {
return "/accounting/account/{$accountId}/{$this->accountingPath}/{$resourceId}";
Expand Down Expand Up @@ -127,6 +127,7 @@ private function createOldResponseError(int $statusCode, array $responseData, st
private function createNewResponseError(int $statusCode, array $responseData, string $rawRespone): void
{
$message = $responseData['message'];
$errorCode = null;
$details = [];

foreach ($responseData['details'] as $detail) {
Expand All @@ -151,7 +152,7 @@ private function createNewResponseError(int $statusCode, array $responseData, st
* @param string $rawRespone The raw response body
* @return void
*/
private function handleError(int $statusCode, array $responseData, string $rawRespone): void
protected function handleError(int $statusCode, array $responseData, string $rawRespone): void
{
if (array_key_exists('response', $responseData) && array_key_exists('errors', $responseData['response'])) {
$this->createOldResponseError($statusCode, $responseData, $rawRespone);
Expand Down
67 changes: 67 additions & 0 deletions src/Resource/EventsResource.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php

declare(strict_types=1);

namespace amcintosh\FreshBooks\Resource;

use Http\Client\HttpClient;
use Spatie\DataTransferObject\DataTransferObject;
use amcintosh\FreshBooks\Builder\IncludesBuilder;
use amcintosh\FreshBooks\Exception\FreshBooksException;
use amcintosh\FreshBooks\Exception\FreshBooksNotImplementedException;
use amcintosh\FreshBooks\Model\DataModel;
use amcintosh\FreshBooks\Model\ListModel;
use amcintosh\FreshBooks\Model\VisState;

/**
* Resource for calls to /events endpoints.
*
* @package amcintosh\FreshBooks\Resource
*/
class EventsResource extends AccountingResource
{
/**
* The the url to the events resource.
*
* @param string $accountId
* @param int $resourceId
* @return string
*/
protected function getUrl(string $accountId, int $resourceId = null): string
{
if (!is_null($resourceId)) {
return "/events/account/{$accountId}/{$this->accountingPath}/{$resourceId}";
}
return "/events/account/{$accountId}/{$this->accountingPath}";
}

/**
* Create a FreshBooksException from the json response from the events endpoint.
*
* @param int $statusCode HTTP status code
* @param array $responseData The json-parsed response
* @param string $rawRespone The raw response body
* @return void
*/
protected function handleError(int $statusCode, array $responseData, string $rawRespone): void
{
if (!array_key_exists('message', $responseData) || !array_key_exists('code', $responseData)) {
throw new FreshBooksException('Unknown error', $statusCode, null, $rawRespone);
}

$message = $responseData['message'];
$errorCode = null;
$details = [];

foreach ($responseData['details'] as $detail) {
if (
in_array('type.googleapis.com/google.rpc.BadRequest', $detail)
&& array_key_exists('fieldViolations', $detail)
) {
$details = $detail['fieldViolations'];
}
}
var_dump($details);
throw new FreshBooksException($message, $statusCode, null, $rawRespone, $errorCode, $details);
}
}
42 changes: 42 additions & 0 deletions tests/Model/CallbackTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

declare(strict_types=1);

namespace amcintosh\FreshBooks\Tests\Model;

use DateTime;
use PHPUnit\Framework\TestCase;
use amcintosh\FreshBooks\Model\Callback;

final class CallbackTest extends TestCase
{
private $sampleCallbackData = '{"callback":{
"callbackid": 123,
"verified": true,
"uri": "http://freshbooks.com/hook/123",
"event": "invoice.create",
"updated_at": "2017-08-23T11:45:09Z"}}';

public function testCallbackFromResponse(): void
{
$callbackData = json_decode($this->sampleCallbackData, true);

$callback = new Callback($callbackData[Callback::RESPONSE_FIELD]);

$this->assertSame(123, $callback->callbackId);
$this->assertSame('invoice.create', $callback->event);
$this->assertEquals(new DateTime('2017-08-23T11:45:09Z'), $callback->updatedAt);
$this->assertSame('http://freshbooks.com/hook/123', $callback->uri);
$this->assertTrue($callback->verified);
}

public function testCallbackGetContent(): void
{
$callbackData = json_decode($this->sampleCallbackData, true);
$callback = new Callback($callbackData['callback']);
$this->assertSame([
'event' => 'invoice.create',
'uri' => 'http://freshbooks.com/hook/123'
], $callback->getContent());
}
}
Loading

0 comments on commit 9dbb830

Please sign in to comment.