-
-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add payments resource and allow getting default payment options as well as payment options on an invoice and creating payment options for an invoice. Closes #60
- Loading branch information
Showing
6 changed files
with
504 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace amcintosh\FreshBooks\Model; | ||
|
||
use Spatie\DataTransferObject\Attributes\MapFrom; | ||
use Spatie\DataTransferObject\Attributes\MapTo; | ||
use Spatie\DataTransferObject\Caster; | ||
use Spatie\DataTransferObject\DataTransferObject; | ||
use amcintosh\FreshBooks\Model\DataModel; | ||
|
||
/** | ||
* In FreshBooks, invoices can be paid online via a variety of payment gateways | ||
* setup on the sender’s account. In order for this to be available on an invoice, | ||
* the online payments must be set up through a separate call after the invoice has | ||
* been created. | ||
* | ||
* While default payment options exist, they are not automatically applied to new | ||
* invoices and must be retrieved and added manually. | ||
* | ||
* @package amcintosh\FreshBooks\Model | ||
* @link https://www.freshbooks.com/api/online-payments | ||
*/ | ||
class InvoicePaymentOptions extends DataTransferObject implements DataModel | ||
{ | ||
public const RESPONSE_FIELD = 'payment_options'; | ||
|
||
/** | ||
* @var string invoice_id of the connected invoice. | ||
* | ||
* _Note_: The API returns this as `entity_id`. | ||
*/ | ||
#[MapFrom('entity_id')] | ||
#[MapTo('entity_id')] | ||
public ?string $entityId; | ||
|
||
/** | ||
* @var string Eg. “invoices”. | ||
*/ | ||
#[MapFrom('entity_type')] | ||
#[MapTo('entity_type')] | ||
public ?string $entityType; | ||
|
||
/** | ||
* @var string Payment gateway name. | ||
*/ | ||
#[MapFrom('gateway_name')] | ||
#[MapTo('gateway_name')] | ||
public ?string $gatewayName; | ||
|
||
/** | ||
* @var bool If the invoice can accept credit cards. | ||
*/ | ||
#[MapTo('has_credit_card')] | ||
#[MapFrom('has_credit_card')] | ||
public ?bool $hasCreditCard; | ||
|
||
/** | ||
* @var bool If the invoice can accept ACH bank transfers. | ||
*/ | ||
#[MapTo('has_ach_transfer')] | ||
#[MapFrom('has_ach_transfer')] | ||
public ?bool $hasAchTransfer; | ||
|
||
#[MapFrom('has_acss_debit')] | ||
public ?bool $hasAcssDebit; | ||
|
||
#[MapFrom('has_bacs_debit')] | ||
public ?bool $hasBacsDebit; | ||
|
||
#[MapFrom('has_sepa_debit')] | ||
public ?bool $hasSepaDebit; | ||
|
||
#[MapFrom('has_paypal_smart_checkout')] | ||
public ?bool $hasPaypalSmartCheckout; | ||
|
||
/** | ||
* @var bool If the client can use the gateway to pay part | ||
* of the invoice or only the full amount. | ||
*/ | ||
#[MapTo('allow_partial_payments')] | ||
#[MapFrom('allow_partial_payments')] | ||
public ?bool $allowPartialPayments; | ||
|
||
/** | ||
* 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('id') | ||
->except('hasAcssDebit') | ||
->except('hasBacsDebit') | ||
->except('hasSepaDebit') | ||
->except('hasPaypalSmartCheckout') | ||
->toArray(); | ||
foreach ($data as $key => $value) { | ||
if (is_null($value)) { | ||
unset($data[$key]); | ||
} | ||
} | ||
return $data; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,167 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace amcintosh\FreshBooks\Resource; | ||
|
||
use Http\Client\HttpClient; | ||
use Spatie\DataTransferObject\DataTransferObject; | ||
use amcintosh\FreshBooks\Exception\FreshBooksException; | ||
use amcintosh\FreshBooks\Model\DataModel; | ||
|
||
class PaymentResource extends BaseResource | ||
{ | ||
private HttpClient $httpClient; | ||
private string $resourcePath; | ||
private string $subResourcePath; | ||
private string $defaultsPath; | ||
private string $staticPathParams; | ||
private string $model; | ||
|
||
public function __construct( | ||
HttpClient $httpClient, | ||
string $resourcePath, | ||
string $model, | ||
string $subResourcePath = null, | ||
string $defaultsPath = null, | ||
string $staticPathParams = null, | ||
) { | ||
$this->httpClient = $httpClient; | ||
$this->resourcePath = $resourcePath; | ||
$this->subResourcePath = $subResourcePath; | ||
$this->defaultsPath = $defaultsPath; | ||
$this->staticPathParams = $staticPathParams; | ||
$this->model = $model; | ||
} | ||
|
||
/** | ||
* The the url to the payment resource. | ||
* | ||
* @param int $accountId | ||
* @param int $resourceId | ||
* @param bool $isList | ||
* @return string | ||
*/ | ||
private function getUrl(string $accountId, int $resourceId = null): string | ||
{ | ||
if (!is_null($resourceId) && !is_null($this->subResourcePath)) { | ||
return "/payments/account/{$accountId}/{$this->resourcePath}/{$resourceId}/{$this->subResourcePath}"; | ||
} else { | ||
$url = "/payments/account/{$accountId}/{$this->defaultsPath}"; | ||
if (!is_null($this->staticPathParams) && !is_null($this->subResourcePath)) { | ||
$url .= "?{$this->staticPathParams}"; | ||
} | ||
return $url; | ||
} | ||
} | ||
|
||
/** | ||
* Parse the json response for payments endpoint errors and create a FreshBooksException from it. | ||
* | ||
* @param int $statusCode HTTP status code | ||
* @param array $responseData The json-parsed response | ||
* @param string $rawRespone The raw response body | ||
* @return void | ||
*/ | ||
private function createResponseError(int $statusCode, array $responseData, string $rawRespone): void | ||
{ | ||
$message = $responseData['message'] ?? 'Unknown error'; | ||
$errorDetails = null; | ||
|
||
if (array_key_exists('errors', $responseData) && is_array($responseData['errors'])) { | ||
$errorDetails = []; | ||
foreach ($responseData['errors'] as $errorKey => $errorDetail) { | ||
$errorDetails[] = [$errorKey => $errorDetail]; | ||
$message = 'Error: ' . $errorKey . ' ' . $errorDetail; | ||
} | ||
} elseif (array_key_exists('error', $responseData)) { | ||
$message = $responseData['error']; | ||
} | ||
throw new FreshBooksException($message, $statusCode, null, $rawRespone, null, $errorDetails); | ||
} | ||
|
||
/** | ||
* Make a request against the payments resource and return an array of the json response. | ||
* Throws a FreshBooksException if the response is not a 200 or if the response cannot be parsed. | ||
* | ||
* @param string $method | ||
* @param string $url | ||
* @param array $data | ||
* @return array | ||
*/ | ||
private function makeRequest(string $method, string $url, array $data = null): array | ||
{ | ||
if (!is_null($data)) { | ||
$data = json_encode($data); | ||
} | ||
$response = $this->httpClient->send($method, $url, [], $data); | ||
|
||
$statusCode = $response->getStatusCode(); | ||
if ($statusCode == 204 && $method == self::DELETE) { | ||
return []; | ||
} | ||
try { | ||
$contents = $response->getBody()->getContents(); | ||
$responseData = json_decode($contents, true); | ||
} catch (JSONDecodeError $e) { | ||
throw new FreshBooksException('Failed to parse response', $statusCode, $e, $contents); | ||
} | ||
|
||
if ($statusCode >= 400) { | ||
$this->createResponseError($statusCode, $responseData, $contents); | ||
} | ||
if (!array_key_exists($this->model::RESPONSE_FIELD, $responseData)) { | ||
throw new FreshBooksException('Returned an unexpected response', $statusCode, null, $contents); | ||
} | ||
return $responseData; | ||
} | ||
|
||
/** | ||
* Get the default settings for an account resource. | ||
* | ||
* @param string $accountId The alpha-numeric account id | ||
* @return DataTransferObject The result model with the default data | ||
*/ | ||
public function defaults(string $accountId): DataTransferObject | ||
{ | ||
$url = $this->getUrl($accountId); | ||
$result = $this->makeRequest(self::GET, $url); | ||
return new $this->model($result[$this->model::RESPONSE_FIELD]); | ||
} | ||
|
||
/** | ||
* Get a single resource with the corresponding id. | ||
* | ||
* @param string $accountId The alpha-numeric account id | ||
* @param int $resourceId Id of the resource to return | ||
* @return DataTransferObject The result model | ||
*/ | ||
public function get(string $accountId, int $resourceId): DataTransferObject | ||
{ | ||
$url = $this->getUrl($accountId, $resourceId); | ||
$result = $this->makeRequest(self::GET, $url); | ||
return new $this->model($result[$this->model::RESPONSE_FIELD]); | ||
} | ||
|
||
/** | ||
* Create a resource from either an array or a DataModel object. | ||
* | ||
* @param string $accountId The alpha-numeric account id | ||
* @param DataModel $model (Optional) The model to create | ||
* @param array $data (Optional) The data to create the model with | ||
* @return DataTransferObject Model of the new resource's response data. | ||
*/ | ||
public function create( | ||
string $accountId, | ||
int $resourceId, | ||
DataModel $model = null, | ||
array $data = null | ||
): DataTransferObject { | ||
if (!is_null($model)) { | ||
$data = $model->getContent(); | ||
} | ||
$url = $this->getUrl($accountId, $resourceId); | ||
$result = $this->makeRequest(self::POST, $url, $data); | ||
return new $this->model($result[$this->model::RESPONSE_FIELD]); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace amcintosh\FreshBooks\Tests\Model; | ||
|
||
use PHPUnit\Framework\TestCase; | ||
use amcintosh\FreshBooks\Model\InvoicePaymentOptions; | ||
|
||
final class InvoicePaymentOptionsTest extends TestCase | ||
{ | ||
private $samplePaymentOptionsData = '{"payment_options":{ | ||
"gateway_name": "Stripe", | ||
"has_credit_card": true, | ||
"has_ach_transfer": true, | ||
"has_bacs_debit": false, | ||
"has_sepa_debit": false, | ||
"has_acss_debit": false, | ||
"stripe_acss_payment_options": null, | ||
"has_paypal_smart_checkout": false, | ||
"allow_partial_payments": false, | ||
"entity_type": "invoice", | ||
"entity_id": "12345", | ||
"gateway_info": { | ||
"id": "abcdef", | ||
"account_id": "210000012", | ||
"country": "CA", | ||
"user_publishable_key": null, | ||
"currencies": [ | ||
"CAD" | ||
], | ||
"bank_transfer_enabled": false, | ||
"gateway_name": "fbpay", | ||
"can_process_payments": true | ||
} | ||
}}'; | ||
|
||
public function testInvoicePaymentOptionsFromResponse(): void | ||
{ | ||
$paymentOptionsData = json_decode($this->samplePaymentOptionsData, true); | ||
$paymentOptions = new InvoicePaymentOptions($paymentOptionsData[InvoicePaymentOptions::RESPONSE_FIELD]); | ||
|
||
$this->assertSame('12345', $paymentOptions->entityId); | ||
$this->assertSame('invoice', $paymentOptions->entityType); | ||
$this->assertSame('Stripe', $paymentOptions->gatewayName); | ||
$this->assertTrue($paymentOptions->hasCreditCard); | ||
$this->assertTrue($paymentOptions->hasAchTransfer); | ||
$this->assertFalse($paymentOptions->hasAcssDebit); | ||
$this->assertFalse($paymentOptions->hasBacsDebit); | ||
$this->assertFalse($paymentOptions->hasSepaDebit); | ||
$this->assertFalse($paymentOptions->hasPaypalSmartCheckout); | ||
$this->assertFalse($paymentOptions->allowPartialPayments); | ||
} | ||
|
||
public function testInvoicePaymentOptionsGetContent(): void | ||
{ | ||
$paymentOptionsData = json_decode($this->samplePaymentOptionsData, true); | ||
$paymentOptions = new InvoicePaymentOptions($paymentOptionsData['payment_options']); | ||
$this->assertSame([ | ||
'entity_id' => '12345', | ||
'entity_type' => 'invoice', | ||
'gateway_name' => 'Stripe', | ||
'has_credit_card' => true, | ||
'has_ach_transfer' => true, | ||
'allow_partial_payments' => false | ||
], $paymentOptions->getContent()); | ||
} | ||
} |
Oops, something went wrong.