From 8dc0f5ba35788036ec10a440903aba3ce94a7f15 Mon Sep 17 00:00:00 2001 From: James Seconde Date: Tue, 2 May 2023 14:00:22 +0100 Subject: [PATCH 01/11] Fix the cedilla test, fix the engine to treat input string as UTF-8 because unicode in PHP is a miserable experience. (#403) --- phpunit.xml.dist | 3 +++ src/SMS/Message/SMS.php | 2 +- test/SMS/Message/SMSTest.php | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 81ebab32..2de4e2c6 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -23,6 +23,9 @@ test/Verify2 + + test/Messages + diff --git a/src/SMS/Message/SMS.php b/src/SMS/Message/SMS.php index 46fad168..8de89f7d 100644 --- a/src/SMS/Message/SMS.php +++ b/src/SMS/Message/SMS.php @@ -32,7 +32,7 @@ public function __construct(string $to, string $from, protected string $message, public static function isGsm7(string $message): bool { - $fullPattern = "/\A[" . preg_quote(self::GSM_7_CHARSET, '/') . "]*\z/"; + $fullPattern = "/\A[" . preg_quote(self::GSM_7_CHARSET, '/') . "]*\z/u"; return (bool)preg_match($fullPattern, $message); } diff --git a/test/SMS/Message/SMSTest.php b/test/SMS/Message/SMSTest.php index b5bce360..94846c4a 100644 --- a/test/SMS/Message/SMSTest.php +++ b/test/SMS/Message/SMSTest.php @@ -185,7 +185,7 @@ public function unicodeStringDataProvider(): array ['This is a text with some tasty characters: [test]', true], ['This is also a GSM7 text', true], ['This is a Çotcha', true], - ['This is also a çotcha', true], + ['This is also a çotcha', false], ['日本語でボナージュ', false], ]; } From c9b8ada33598fd2b648afde1756375d9054030bc Mon Sep 17 00:00:00 2001 From: James Seconde Date: Mon, 15 May 2023 10:41:13 +0100 Subject: [PATCH 02/11] Feature/voice machine detection (#404) * Outbound call PHP8 cleanup * Advanced machine detection in outbound call * Add advanced machine detection to NCCO --- phpunit.xml.dist | 3 + src/Voice/Client.php | 4 + src/Voice/NCCO/Action/Connect.php | 58 +++++----- src/Voice/OutboundCall.php | 71 +++++------- .../VoiceObjects/AdvancedMachineDetection.php | 104 ++++++++++++++++++ test/Voice/ClientTest.php | 47 +++++++- test/Voice/NCCO/Action/ConnectTest.php | 15 +++ test/Voice/OutboundCallTest.php | 39 ++++++- 8 files changed, 258 insertions(+), 83 deletions(-) create mode 100644 src/Voice/VoiceObjects/AdvancedMachineDetection.php diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 2de4e2c6..1e1536e9 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -23,6 +23,9 @@ test/Verify2 + + test/Voice + test/Messages diff --git a/src/Voice/Client.php b/src/Voice/Client.php index d131be8e..1d78b9a6 100644 --- a/src/Voice/Client.php +++ b/src/Voice/Client.php @@ -84,6 +84,10 @@ public function createOutboundCall(OutboundCall $call): Event $json['ringing_timer'] = (string)$call->getRingingTimer(); } + if (!is_null($call->getAdvancedMachineDetection())) { + $json['advanced_machine_detection'] = $call->getAdvancedMachineDetection()->toArray(); + } + $event = $this->api->create($json); $event['to'] = $call->getTo()->getId(); if ($call->getFrom()) { diff --git a/src/Voice/NCCO/Action/Connect.php b/src/Voice/NCCO/Action/Connect.php index 48bd99bd..52507e70 100644 --- a/src/Voice/NCCO/Action/Connect.php +++ b/src/Voice/NCCO/Action/Connect.php @@ -13,6 +13,7 @@ use InvalidArgumentException; use Vonage\Voice\Endpoint\EndpointInterface; +use Vonage\Voice\VoiceObjects\AdvancedMachineDetection; use Vonage\Voice\Webhook; class Connect implements ActionInterface @@ -21,40 +22,15 @@ class Connect implements ActionInterface public const MACHINE_CONTINUE = 'continue'; public const MACHINE_HANGUP = 'hangup'; - /** - * @var ?string - */ - protected $from; - - /** - * @var ?string - */ - protected $eventType; + protected ?string $from = ''; + protected ?string $eventType = ''; - /** - * @var int - */ - protected $timeout; - - /** - * @var int - */ - protected $limit; - - /** - * @var string - */ - protected $machineDetection; - - /** - * @var ?Webhook - */ - protected $eventWebhook; - - /** - * @var ?string - */ - protected $ringbackTone; + protected int $timeout = 0; + protected int $limit = 0; + protected $machineDetection = ''; + protected ?Webhook $eventWebhook = null; + protected ?string $ringbackTone = ''; + protected ?AdvancedMachineDetection $advancedMachineDetection = null; public function __construct(protected EndpointInterface $endpoint) { @@ -93,6 +69,10 @@ public function toNCCOArray(): array $data['machineDetection'] = $this->getMachineDetection(); } + if ($this->getAdvancedMachineDetection()) { + $data['advancedMachineDetection'] = $this->getAdvancedMachineDetection()->toArray(); + } + $from = $this->getFrom(); if ($from) { @@ -236,4 +216,16 @@ public function setRingbackTone(string $ringbackTone): self return $this; } + + public function getAdvancedMachineDetection(): ?AdvancedMachineDetection + { + return $this->advancedMachineDetection; + } + + public function setAdvancedMachineDetection(AdvancedMachineDetection $advancedMachineDetection): static + { + $this->advancedMachineDetection = $advancedMachineDetection; + + return $this; + } } diff --git a/src/Voice/OutboundCall.php b/src/Voice/OutboundCall.php index 019d6015..419837b6 100644 --- a/src/Voice/OutboundCall.php +++ b/src/Voice/OutboundCall.php @@ -15,61 +15,46 @@ use Vonage\Voice\Endpoint\EndpointInterface; use Vonage\Voice\Endpoint\Phone; use Vonage\Voice\NCCO\NCCO; +use Vonage\Voice\VoiceObjects\AdvancedMachineDetection; class OutboundCall { public const MACHINE_CONTINUE = 'continue'; public const MACHINE_HANGUP = 'hangup'; - - /** - * @var Webhook - */ - protected $answerWebhook; - - /** - * @var Webhook - */ - protected $eventWebhook; + protected ?Webhook $answerWebhook = null; + protected ?Webhook $eventWebhook = null; /** * Length of seconds before Vonage hangs up after going into `in_progress` status - * - * @var int */ - protected $lengthTimer; + protected int $lengthTimer = 7200; /** * What to do when Vonage detects an answering machine. - * - * @var ?string */ - protected $machineDetection; + protected ?string $machineDetection = ''; + /** - * @var NCCO + * Overrides machine detection if used for more configuration options */ - protected $ncco; + protected ?AdvancedMachineDetection $advancedMachineDetection = null; + + protected ?NCCO $ncco = null; /** - * Whether or not to use random numbers linked on the application - * - * @var bool + * Whether to use random numbers linked on the application */ - protected $randomFrom = false; + protected bool $randomFrom = false; /** * Length of time Vonage will allow a phone number to ring before hanging up - * - * @var int */ - protected $ringingTimer; + protected int $ringingTimer = 60; /** * Creates a new Outbound Call object * If no `$from` parameter is passed, the system will use a random number * that is linked to the application instead. - * - * - * @return void */ public function __construct(protected EndpointInterface $to, protected ?Phone $from = null) { @@ -118,9 +103,6 @@ public function getTo(): EndpointInterface return $this->to; } - /** - * @return $this - */ public function setAnswerWebhook(Webhook $webhook): self { $this->answerWebhook = $webhook; @@ -128,9 +110,6 @@ public function setAnswerWebhook(Webhook $webhook): self return $this; } - /** - * @return $this - */ public function setEventWebhook(Webhook $webhook): self { $this->eventWebhook = $webhook; @@ -138,9 +117,6 @@ public function setEventWebhook(Webhook $webhook): self return $this; } - /** - * @return $this - */ public function setLengthTimer(int $timer): self { $this->lengthTimer = $timer; @@ -148,9 +124,6 @@ public function setLengthTimer(int $timer): self return $this; } - /** - * @return $this - */ public function setMachineDetection(string $action): self { if ($action === self::MACHINE_CONTINUE || $action === self::MACHINE_HANGUP) { @@ -162,9 +135,6 @@ public function setMachineDetection(string $action): self throw new InvalidArgumentException('Unknown machine detection action'); } - /** - * @return $this - */ public function setNCCO(NCCO $ncco): self { $this->ncco = $ncco; @@ -172,9 +142,6 @@ public function setNCCO(NCCO $ncco): self return $this; } - /** - * @return $this - */ public function setRingingTimer(int $timer): self { $this->ringingTimer = $timer; @@ -186,4 +153,16 @@ public function getRandomFrom(): bool { return $this->randomFrom; } + + public function getAdvancedMachineDetection(): ?AdvancedMachineDetection + { + return $this->advancedMachineDetection; + } + + public function setAdvancedMachineDetection(?AdvancedMachineDetection $advancedMachineDetection): static + { + $this->advancedMachineDetection = $advancedMachineDetection; + + return $this; + } } diff --git a/src/Voice/VoiceObjects/AdvancedMachineDetection.php b/src/Voice/VoiceObjects/AdvancedMachineDetection.php new file mode 100644 index 00000000..626cce71 --- /dev/null +++ b/src/Voice/VoiceObjects/AdvancedMachineDetection.php @@ -0,0 +1,104 @@ +isValidBehaviour($behaviour)) { + throw new \InvalidArgumentException($behaviour . ' is not a valid behavior string'); + } + + if (!$this->isValidMode($mode)) { + throw new \InvalidArgumentException($mode . ' is not a valid mode string'); + } + + if (!$this->isValidTimeout($beepTimeout)) { + throw new \OutOfBoundsException('Timeout ' . $beepTimeout . ' is not valid'); + } + } + + protected function isValidBehaviour(string $behaviour): bool + { + if (in_array($behaviour, $this->permittedBehaviour, true)) { + return true; + } + + return false; + } + + protected function isValidMode(string $mode): bool + { + if (in_array($mode, $this->permittedModes, true)) { + return true; + } + + return false; + } + + protected function isValidTimeout(int $beepTimeout): bool + { + $range = [ + 'options' => [ + 'min_range' => self::BEEP_TIMEOUT_MIN, + 'max_range' => self::BEEP_TIMEOUT_MAX + ] + ]; + + if (filter_var($beepTimeout, FILTER_VALIDATE_INT, $range)) { + return true; + } + + return false; + } + + public function fromArray(array $data): static + { + $this->isArrayValid($data); + + $this->behaviour = $data['behaviour']; + $this->mode = $data['mode']; + $this->beepTimeout = $data['beep_timeout']; + + return $this; + } + + public function toArray(): array + { + return [ + 'behaviour' => $this->behaviour, + 'mode' => $this->mode, + 'beep_timeout' => $this->beepTimeout + ]; + } + + protected function isArrayValid(array $data): bool + { + if ( + !array_key_exists('behaviour', $data) + || !array_key_exists('mode', $data) + || !array_key_exists('beep_timeout', $data) + ) { + return false; + } + + return $this->isValidBehaviour($data['behaviour']) + || $this->isValidMode($data['mode']) + || $this->isValidTimeout($data['beep_timeout']); + } +} diff --git a/test/Voice/ClientTest.php b/test/Voice/ClientTest.php index 5899a306..896c8228 100644 --- a/test/Voice/ClientTest.php +++ b/test/Voice/ClientTest.php @@ -12,8 +12,7 @@ namespace VonageTest\Voice; use Laminas\Diactoros\Response; -use Psr\Http\Message\ResponseInterface; -use Psr\Http\Message\StreamInterface; +use Vonage\Voice\VoiceObjects\AdvancedMachineDetection; use VonageTest\VonageTestCase; use Prophecy\Argument; use Psr\Http\Client\ClientExceptionInterface; @@ -119,6 +118,50 @@ public function testCanCreateOutboundCall(): void $this->assertEquals('2541d01c-253e-48be-a8e0-da4bbe4c3722', $callData->getConversationUuid()); } + public function testAdvancedMachineDetectionRenders(): void + { + $advancedMachineDetection = new AdvancedMachineDetection( + AdvancedMachineDetection::MACHINE_BEHAVIOUR_CONTINUE, + 50, + AdvancedMachineDetection::MACHINE_MODE_DETECT_BEEP + ); + + $payload = [ + 'to' => [ + [ + 'type' => 'phone', + 'number' => '15555555555' + ] + ], + 'from' => [ + 'type' => 'phone', + 'number' => '16666666666' + ], + 'answer_url' => ['http://domain.test/answer'], + 'answer_method' => 'POST', + 'event_url' => ['http://domain.test/event'], + 'event_method' => 'POST', + 'machine_detection' => 'hangup', + 'length_timer' => '7200', + 'ringing_timer' => '60', + 'advanced_machine_detection' => $advancedMachineDetection + ]; + + $this->vonageClient->send(Argument::that(function (RequestInterface $request) use ($payload) { + $this->assertRequestUrl('api.nexmo.com', '/v1/calls', 'POST', $request); + $this->assertRequestJsonBodyContains('advanced_machine_detection', $payload['advanced_machine_detection']->toArray(), $request); + + return true; + }))->willReturn($this->getResponse('create-outbound-call-success', 201)); + + $outboundCall = (new OutboundCall(new Phone('15555555555'), new Phone('16666666666'))) + ->setEventWebhook(new Webhook('http://domain.test/event')) + ->setAnswerWebhook(new Webhook('http://domain.test/answer')) + ->setAdvancedMachineDetection($advancedMachineDetection); + + $callData = $this->voiceClient->createOutboundCall($outboundCall); + } + public function testCanCreateOutboundCallWithRandomFromNumber(): void { $payload = [ diff --git a/test/Voice/NCCO/Action/ConnectTest.php b/test/Voice/NCCO/Action/ConnectTest.php index 22cde882..064c05ed 100644 --- a/test/Voice/NCCO/Action/ConnectTest.php +++ b/test/Voice/NCCO/Action/ConnectTest.php @@ -12,6 +12,7 @@ namespace VonageTest\Voice\NCCO\Action; use InvalidArgumentException; +use Vonage\Voice\VoiceObjects\AdvancedMachineDetection; use VonageTest\VonageTestCase; use Vonage\Voice\Endpoint\EndpointInterface; use Vonage\Voice\Endpoint\Phone; @@ -45,10 +46,16 @@ public function testSimpleSetup(): void public function testCanSetAdditionalInformation(): void { + $advancedMachineDetection = new AdvancedMachineDetection( + AdvancedMachineDetection::MACHINE_BEHAVIOUR_CONTINUE, + 50 + ); + $webhook = new Webhook('https://test.domain/events'); $action = (new Connect($this->endpoint)) ->setFrom('15553216547') ->setMachineDetection(Connect::MACHINE_CONTINUE) + ->setAdvancedMachineDetection($advancedMachineDetection) ->setEventType(Connect::EVENT_TYPE_SYNCHRONOUS) ->setLimit(6000) ->setRingbackTone('https://test.domain/ringback.mp3') @@ -58,6 +65,7 @@ public function testCanSetAdditionalInformation(): void $this->assertSame('15553216547', $action->getFrom()); $this->assertSame(Connect::MACHINE_CONTINUE, $action->getMachineDetection()); $this->assertSame(Connect::EVENT_TYPE_SYNCHRONOUS, $action->getEventType()); + $this->assertSame($advancedMachineDetection, $action->getAdvancedMachineDetection()); $this->assertSame(6000, $action->getLimit()); $this->assertSame('https://test.domain/ringback.mp3', $action->getRingbackTone()); $this->assertSame(10, $action->getTimeout()); @@ -66,10 +74,16 @@ public function testCanSetAdditionalInformation(): void public function testGeneratesCorrectNCCOArray(): void { + $advancedMachineDetection = new AdvancedMachineDetection( + AdvancedMachineDetection::MACHINE_BEHAVIOUR_CONTINUE, + 50 + ); + $webhook = new Webhook('https://test.domain/events'); $ncco = (new Connect($this->endpoint)) ->setFrom('15553216547') ->setMachineDetection(Connect::MACHINE_CONTINUE) + ->setAdvancedMachineDetection($advancedMachineDetection) ->setEventType(Connect::EVENT_TYPE_SYNCHRONOUS) ->setLimit(6000) ->setRingbackTone('https://test.domain/ringback.mp3') @@ -79,6 +93,7 @@ public function testGeneratesCorrectNCCOArray(): void $this->assertSame('15553216547', $ncco['from']); $this->assertSame(Connect::MACHINE_CONTINUE, $ncco['machineDetection']); + $this->assertSame($advancedMachineDetection->toArray(), $ncco['advancedMachineDetection']); $this->assertSame(Connect::EVENT_TYPE_SYNCHRONOUS, $ncco['eventType']); $this->assertSame(6000, $ncco['limit']); $this->assertSame('https://test.domain/ringback.mp3', $ncco['ringbackTone']); diff --git a/test/Voice/OutboundCallTest.php b/test/Voice/OutboundCallTest.php index bac07be8..af8026f5 100644 --- a/test/Voice/OutboundCallTest.php +++ b/test/Voice/OutboundCallTest.php @@ -11,7 +11,7 @@ namespace VonageTest\Voice; -use InvalidArgumentException; +use Vonage\Voice\VoiceObjects\AdvancedMachineDetection; use VonageTest\VonageTestCase; use Vonage\Voice\Endpoint\Phone; use Vonage\Voice\OutboundCall; @@ -20,10 +20,45 @@ class OutboundCallTest extends VonageTestCase { public function testMachineDetectionThrowsExceptionOnBadValue(): void { - $this->expectException(InvalidArgumentException::class); + $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage('Unknown machine detection action'); (new OutboundCall(new Phone('15555555555'), new Phone('16666666666'))) ->setMachineDetection('bob'); } + + public function testAdvancedMachineDetectionThrowsExceptionOnBadBehaviour(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('forward is not a valid behavior string'); + + $advancedMachineDetection = new AdvancedMachineDetection( + 'forward', + 50, + ); + } + + public function testAdvancedMachineDetectionThrowsExceptionOnBadMode(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('beep-forward is not a valid mode string'); + + $advancedMachineDetection = new AdvancedMachineDetection( + AdvancedMachineDetection::MACHINE_BEHAVIOUR_CONTINUE, + 50, + 'beep-forward' + ); + } + + public function testAdvancedMachineDetectionThrowsExceptionOnBadTimeout(): void + { + $this->expectException(\OutOfBoundsException::class); + $this->expectExceptionMessage('Timeout 200 is not valid'); + + $advancedMachineDetection = new AdvancedMachineDetection( + AdvancedMachineDetection::MACHINE_BEHAVIOUR_CONTINUE, + 200, + AdvancedMachineDetection::MACHINE_MODE_DETECT + ); + } } From 15f008daa161000f17c6995b0f4c537666b98438 Mon Sep 17 00:00:00 2001 From: James Seconde Date: Mon, 15 May 2023 14:23:45 +0100 Subject: [PATCH 03/11] Added fraud check (#406) --- src/Verify2/Request/BaseVerifyRequest.php | 15 ++++++++++++++ test/Verify2/ClientTest.php | 24 +++++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/src/Verify2/Request/BaseVerifyRequest.php b/src/Verify2/Request/BaseVerifyRequest.php index a07eb584..2b8f6e54 100644 --- a/src/Verify2/Request/BaseVerifyRequest.php +++ b/src/Verify2/Request/BaseVerifyRequest.php @@ -16,6 +16,8 @@ abstract class BaseVerifyRequest implements RequestInterface protected int $timeout = 300; + protected bool $fraudCheck = true; + protected ?string $clientRef = null; protected int $length = 4; @@ -132,9 +134,22 @@ public function addWorkflow(VerificationWorkflow $verificationWorkflow): static return $this; } + public function getFraudCheck(): bool + { + return $this->fraudCheck; + } + + public function setFraudCheck(bool $fraudCheck): BaseVerifyRequest + { + $this->fraudCheck = $fraudCheck; + + return $this; + } + public function getBaseVerifyUniversalOutputArray(): array { $returnArray = [ + 'fraud_check' => $this->getFraudCheck(), 'locale' => $this->getLocale()->getCode(), 'channel_timeout' => $this->getTimeout(), 'code_length' => $this->getLength(), diff --git a/test/Verify2/ClientTest.php b/test/Verify2/ClientTest.php index 2b0b51f2..25a07e4e 100644 --- a/test/Verify2/ClientTest.php +++ b/test/Verify2/ClientTest.php @@ -98,6 +98,7 @@ public function testCanRequestSMS(): void ); $this->assertRequestJsonBodyContains('locale', 'en-us', $request); + $this->assertRequestJsonBodyContains('fraud_check', true, $request); $this->assertRequestJsonBodyContains('channel_timeout', 300, $request); $this->assertRequestJsonBodyContains('client_ref', $payload['client_ref'], $request); $this->assertRequestJsonBodyContains('code_length', 4, $request); @@ -115,6 +116,29 @@ public function testCanRequestSMS(): void $this->assertArrayHasKey('request_id', $result); } + public function testCanBypassFraudCheck(): void + { + $payload = [ + 'to' => '07785254785', + 'client_ref' => 'my-verification', + 'brand' => 'my-brand', + ]; + + $smsVerification = new SMSRequest($payload['to'], $payload['brand']); + $smsVerification->setFraudCheck(false); + + $this->vonageClient->send(Argument::that(function (Request $request) use ($payload) { + $this->assertRequestJsonBodyContains('fraud_check', false, $request); + + return true; + }))->willReturn($this->getResponse('verify-request-success', 202)); + + $result = $this->verify2Client->startVerification($smsVerification); + + $this->assertIsArray($result); + $this->assertArrayHasKey('request_id', $result); + } + /** * @dataProvider localeProvider */ From a8f6c33b6d5a25239524b4c176379cb844375640 Mon Sep 17 00:00:00 2001 From: James Seconde Date: Tue, 16 May 2023 11:34:31 +0100 Subject: [PATCH 04/11] Added ability to cancel request (#405) --- src/Verify2/Client.php | 10 +++++-- src/Verify2/ClientFactory.php | 2 +- test/Verify2/ClientTest.php | 26 +++++++++++++++++-- .../Responses/verify-cancel-success.json | 0 4 files changed, 33 insertions(+), 5 deletions(-) create mode 100644 test/Verify2/Fixtures/Responses/verify-cancel-success.json diff --git a/src/Verify2/Client.php b/src/Verify2/Client.php index e70b5901..e8e9c1cc 100644 --- a/src/Verify2/Client.php +++ b/src/Verify2/Client.php @@ -2,7 +2,6 @@ namespace Vonage\Verify2; -use Laminas\Diactoros\Request; use Vonage\Client\APIClient; use Vonage\Client\APIResource; use Vonage\Client\Exception\Exception; @@ -27,7 +26,7 @@ public function startVerification(BaseVerifyRequest $request): ?array public function check(string $requestId, $code): bool { try { - $response = $this->getAPIResource()->create(['code' => $code], $requestId); + $response = $this->getAPIResource()->create(['code' => $code], '/' . $requestId); } catch (Exception $e) { // For horrible reasons in the API Error Handler, throw the error unless it's a 409. if ($e->getCode() === 409) { @@ -39,4 +38,11 @@ public function check(string $requestId, $code): bool return true; } + + public function cancelRequest(string $requestId): bool + { + $this->api->delete($requestId); + + return true; + } } \ No newline at end of file diff --git a/src/Verify2/ClientFactory.php b/src/Verify2/ClientFactory.php index 00daa750..fd3fe5d0 100644 --- a/src/Verify2/ClientFactory.php +++ b/src/Verify2/ClientFactory.php @@ -15,7 +15,7 @@ public function __invoke(ContainerInterface $container): Client $api->setIsHAL(false) ->setErrorsOn200(false) ->setAuthHandler([new KeypairHandler(), new BasicHandler()]) - ->setBaseUrl('https://api.nexmo.com/v2/verify/'); + ->setBaseUrl('https://api.nexmo.com/v2/verify'); return new Client($api); } diff --git a/test/Verify2/ClientTest.php b/test/Verify2/ClientTest.php index 25a07e4e..e9c8fa19 100644 --- a/test/Verify2/ClientTest.php +++ b/test/Verify2/ClientTest.php @@ -46,7 +46,7 @@ public function setUp(): void ->setErrorsOn200(false) ->setClient($this->vonageClient->reveal()) ->setAuthHandler([new Client\Credentials\Handler\BasicHandler(), new Client\Credentials\Handler\KeypairHandler()]) - ->setBaseUrl('https://api.nexmo.com/v2/verify/'); + ->setBaseUrl('https://api.nexmo.com/v2/verify'); $this->verify2Client = new Verify2Client($this->api); } @@ -93,7 +93,7 @@ public function testCanRequestSMS(): void $uri = $request->getUri(); $uriString = $uri->__toString(); $this->assertEquals( - 'https://api.nexmo.com/v2/verify/', + 'https://api.nexmo.com/v2/verify', $uriString ); @@ -615,6 +615,28 @@ public function testCheckHandlesThrottle(): void $result = $this->verify2Client->check('c11236f4-00bf-4b89-84ba-88b25df97315', '24525'); } + public function testWillCancelVerification(): void + { + $requestId = 'c11236f4-00bf-4b89-84ba-88b25df97315'; + + $this->vonageClient->send(Argument::that(function (Request $request) { + $uri = $request->getUri(); + $uriString = $uri->__toString(); + $this->assertEquals( + 'https://api.nexmo.com/v2/verify/c11236f4-00bf-4b89-84ba-88b25df97315', + $uriString + ); + + $this->assertEquals('DELETE', $request->getMethod()); + + return true; + }))->willReturn($this->getResponse('verify-cancel-success', 204)); + + $result = $this->verify2Client->cancelRequest($requestId); + + $this->assertTrue($result); + } + /** * This method gets the fixtures and wraps them in a Response object to mock the API */ diff --git a/test/Verify2/Fixtures/Responses/verify-cancel-success.json b/test/Verify2/Fixtures/Responses/verify-cancel-success.json new file mode 100644 index 00000000..e69de29b From 0dc217b0f2c90bcf3854ee6c122a6588a317d42a Mon Sep 17 00:00:00 2001 From: James Seconde Date: Tue, 16 May 2023 15:05:54 +0100 Subject: [PATCH 05/11] Commit cancel to docs, yes straight to main as trivial --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index bf91caa2..22d5eac9 100644 --- a/README.md +++ b/README.md @@ -383,6 +383,14 @@ $verificationEvent = \Vonage\Verify2\Webhook\Factory::createFromArray($payload); var_dump($verificationEvent->getStatus()); ``` +### Cancelling a request in-flight + +You can cancel a request should you need to, before the end user has taken any action. + +```php +$requestId = 'c11236f4-00bf-4b89-84ba-88b25df97315'; +$client->verify2()->cancel($requestId); +``` ### Making a Call From 9cc0d27da5a2b13347f984ade499327e52510d65 Mon Sep 17 00:00:00 2001 From: James Seconde Date: Wed, 17 May 2023 11:13:36 +0100 Subject: [PATCH 06/11] Only render fraud check if it's been set to false. True does not need to be rendered as it's default (#407) --- src/Verify2/Request/BaseVerifyRequest.php | 11 +++++++---- test/Verify2/ClientTest.php | 5 ++--- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/Verify2/Request/BaseVerifyRequest.php b/src/Verify2/Request/BaseVerifyRequest.php index 2b8f6e54..c74aaa8d 100644 --- a/src/Verify2/Request/BaseVerifyRequest.php +++ b/src/Verify2/Request/BaseVerifyRequest.php @@ -16,7 +16,7 @@ abstract class BaseVerifyRequest implements RequestInterface protected int $timeout = 300; - protected bool $fraudCheck = true; + protected ?bool $fraudCheck = null; protected ?string $clientRef = null; @@ -134,9 +134,9 @@ public function addWorkflow(VerificationWorkflow $verificationWorkflow): static return $this; } - public function getFraudCheck(): bool + public function getFraudCheck(): ?bool { - return $this->fraudCheck; + return $this->fraudCheck ?? null; } public function setFraudCheck(bool $fraudCheck): BaseVerifyRequest @@ -149,7 +149,6 @@ public function setFraudCheck(bool $fraudCheck): BaseVerifyRequest public function getBaseVerifyUniversalOutputArray(): array { $returnArray = [ - 'fraud_check' => $this->getFraudCheck(), 'locale' => $this->getLocale()->getCode(), 'channel_timeout' => $this->getTimeout(), 'code_length' => $this->getLength(), @@ -157,6 +156,10 @@ public function getBaseVerifyUniversalOutputArray(): array 'workflow' => $this->getWorkflows() ]; + if ($this->getFraudCheck() === false) { + $returnArray['fraud_check'] = $this->getFraudCheck(); + } + if ($this->getClientRef()) { $returnArray['client_ref'] = $this->getClientRef(); } diff --git a/test/Verify2/ClientTest.php b/test/Verify2/ClientTest.php index e9c8fa19..71f286f1 100644 --- a/test/Verify2/ClientTest.php +++ b/test/Verify2/ClientTest.php @@ -98,7 +98,7 @@ public function testCanRequestSMS(): void ); $this->assertRequestJsonBodyContains('locale', 'en-us', $request); - $this->assertRequestJsonBodyContains('fraud_check', true, $request); + $this->assertRequestJsonBodyMissing('fraud_check', $request); $this->assertRequestJsonBodyContains('channel_timeout', 300, $request); $this->assertRequestJsonBodyContains('client_ref', $payload['client_ref'], $request); $this->assertRequestJsonBodyContains('code_length', 4, $request); @@ -120,14 +120,13 @@ public function testCanBypassFraudCheck(): void { $payload = [ 'to' => '07785254785', - 'client_ref' => 'my-verification', 'brand' => 'my-brand', ]; $smsVerification = new SMSRequest($payload['to'], $payload['brand']); $smsVerification->setFraudCheck(false); - $this->vonageClient->send(Argument::that(function (Request $request) use ($payload) { + $this->vonageClient->send(Argument::that(function (Request $request) { $this->assertRequestJsonBodyContains('fraud_check', false, $request); return true; From bb29ae2b9001581a077e09114394f9ce88973828 Mon Sep 17 00:00:00 2001 From: James Seconde Date: Mon, 22 May 2023 16:31:17 +0100 Subject: [PATCH 07/11] Add PHPUnit test suite. Tweak test to make SURE every character in gsm7 is covered. Add docs in readme (#409) --- README.md | 15 +++++++++++++++ phpunit.xml.dist | 3 +++ test/SMS/Message/SMSTest.php | 34 +++++++++++++++++++++++++--------- 3 files changed, 43 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 22d5eac9..a0ea3642 100644 --- a/README.md +++ b/README.md @@ -112,6 +112,21 @@ foreach($response as $index => $data){ The [send example][send_example] also has full working examples. +### Detecting Encoding Type + +You can use a static `isGsm7()` method within the SMS Client code to determine whether to send the message using +GSM-7 encoding or Unicode. Here is an example: + +```php +$sms = new \Vonage\SMS\Message\SMS('123', '456', 'is this gsm7?'); + +if (Vonage\SMS\Message\SMS::isGsm7($text)) { + $sms->setType('text'); +} else { + $sms->setType('unicode'); +} +``` + ### Receiving a Message Inbound messages are [sent to your application as a webhook][doc_inbound]. The Client library provides a way to diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 1e1536e9..05cad2e7 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -29,6 +29,9 @@ test/Messages + + test/SMS + diff --git a/test/SMS/Message/SMSTest.php b/test/SMS/Message/SMSTest.php index 94846c4a..a03caaab 100644 --- a/test/SMS/Message/SMSTest.php +++ b/test/SMS/Message/SMSTest.php @@ -170,7 +170,7 @@ public function testDLTInfoDoesNotAppearsWhenNotSet(): void } /** - * @dataProvider unicodeStringDataProvider + * @dataProvider entireGsm7CharSetProvider * @return void */ public function testGsm7Identification(string $message, bool $expectedGsm7): void @@ -178,15 +178,31 @@ public function testGsm7Identification(string $message, bool $expectedGsm7): voi $this->assertEquals($expectedGsm7, SMS::isGsm7($message)); } - public function unicodeStringDataProvider(): array + public function entireGsm7CharSetProvider(): array { - return [ - ['this is a text', true], - ['This is a text with some tasty characters: [test]', true], - ['This is also a GSM7 text', true], - ['This is a Çotcha', true], - ['This is also a çotcha', false], - ['日本語でボナージュ', false], + $gsm7Characters = [ + "@", "£", "$", "¥", "è", "é", "ù", "ì", "ò", "Ç", "\n", "Ø", "ø", "\r", "Å", + "å", "\u0394", "_", "\u03a6", "\u0393", "\u039b", "\u03a9", "\u03a0", "\u03a8", + "\u03a3", "\u0398", "\u039e", "\u00a0", "Æ", "æ", "ß", "É", " ", "!", "\"", "#", + "¤", "%", "&", "'", "(", ")", "*", "+", ",", "-", ".", "/", "0", "1", "2", "3", + "4", "5", "6", "7", "8", "9", ":", ";", "<", "=", ">", "?", "¡", "A", "B", "C", + "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", + "T", "U", "V", "W", "X", "Y", "Z", "Ä", "Ö", "Ñ", "Ü", "§", "¿", "a", "b", "c", + "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", + "t", "u", "v", "w", "x", "y", "z", "ä", "ö", "ñ", "ü", "à", ]; + + $return = []; + + foreach ($gsm7Characters as $character) { + $return[] = [$character, true]; + } + + $return[] = ['This is a text with some tasty characters: [test]', true]; + $return[] = ['This is a Çotcha', true]; + $return[] = ['This is also a çotcha', false]; + $return[] = ['日本語でボナージュ', false]; + + return $return; } } From 5d59b3f0078e82508bc33131ff2c1f84783eed78 Mon Sep 17 00:00:00 2001 From: Marcin Lewandowski Date: Tue, 30 May 2023 12:47:36 +0200 Subject: [PATCH 08/11] Upgrade the laminas/laminas-diactoros package (#410) --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index d668110f..133a6f9a 100644 --- a/composer.json +++ b/composer.json @@ -14,7 +14,7 @@ "require": { "php": "~8.0 || ~8.1 || ~8.2", "ext-mbstring": "*", - "laminas/laminas-diactoros": "^2.21", + "laminas/laminas-diactoros": "^3.0", "lcobucci/jwt": "^3.4|^4.0", "psr/container": "^1.0 | ^2.0", "psr/http-client-implementation": "^1.0", From fd72359b4bf34bfd3a8a36cc3169fbc321b500c6 Mon Sep 17 00:00:00 2001 From: James Seconde Date: Thu, 1 Jun 2023 11:38:45 +0100 Subject: [PATCH 09/11] Get recording was broken, introduce a total override for this edge case where region URL is different from REST base URL (#413) --- src/Client/APIResource.php | 7 ++++++- src/Voice/Client.php | 2 +- test/Voice/ClientTest.php | 4 ++-- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/Client/APIResource.php b/src/Client/APIResource.php index aa2b563d..4b63e683 100644 --- a/src/Client/APIResource.php +++ b/src/Client/APIResource.php @@ -188,10 +188,15 @@ public function delete(string $id, array $headers = []): ?array * @throws ClientExceptionInterface * @throws Exception\Exception */ - public function get($id, array $query = [], array $headers = [], bool $jsonResponse = true) + public function get($id, array $query = [], array $headers = [], bool $jsonResponse = true, bool $uriOverride = false) { $uri = $this->getBaseUrl() . $this->baseUri . '/' . $id; + // This is a necessary hack if you want to fetch a totally different URL but use Vonage Auth + if ($uriOverride) { + $uri = $id; + } + if (!empty($query)) { $uri .= '?' . http_build_query($query); } diff --git a/src/Voice/Client.php b/src/Voice/Client.php index 1d78b9a6..050443d3 100644 --- a/src/Voice/Client.php +++ b/src/Voice/Client.php @@ -274,6 +274,6 @@ public function unmuteCall(string $callId): void public function getRecording(string $url): StreamInterface { - return $this->getAPIResource()->get($url, [], [], false); + return $this->getAPIResource()->get($url, [], [], false, true); } } diff --git a/test/Voice/ClientTest.php b/test/Voice/ClientTest.php index 896c8228..e481fdcc 100644 --- a/test/Voice/ClientTest.php +++ b/test/Voice/ClientTest.php @@ -646,7 +646,7 @@ public function testCanSearchCalls(): void public function testCanDownloadRecording(): void { $fixturePath = __DIR__ . '/Fixtures/mp3fixture.mp3'; - $url = 'recordings/mp3fixture.mp3'; + $url = 'https://api-us.nexmo.com/v1/files/999f999-526d-4013-87fc-c824f7a443b3'; $this->vonageClient->send(Argument::that(function (RequestInterface $request) { $this->assertEquals( @@ -657,7 +657,7 @@ public function testCanDownloadRecording(): void $uri = $request->getUri(); $uriString = $uri->__toString(); $this->assertEquals( - 'https://api.nexmo.com/v1/calls/recordings/mp3fixture.mp3', + 'https://api-us.nexmo.com/v1/files/999f999-526d-4013-87fc-c824f7a443b3', $uriString ); return true; From 30eda93730921691be42ce8223cd0780eba8b88d Mon Sep 17 00:00:00 2001 From: James Seconde Date: Wed, 14 Jun 2023 15:56:27 +0100 Subject: [PATCH 10/11] Output typo for backend when using AMD (#415) --- src/Voice/VoiceObjects/AdvancedMachineDetection.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Voice/VoiceObjects/AdvancedMachineDetection.php b/src/Voice/VoiceObjects/AdvancedMachineDetection.php index 626cce71..75ac6d7f 100644 --- a/src/Voice/VoiceObjects/AdvancedMachineDetection.php +++ b/src/Voice/VoiceObjects/AdvancedMachineDetection.php @@ -81,7 +81,7 @@ public function fromArray(array $data): static public function toArray(): array { return [ - 'behaviour' => $this->behaviour, + 'behavior' => $this->behaviour, 'mode' => $this->mode, 'beep_timeout' => $this->beepTimeout ]; From 8e9f239264eea5aab77e4268b3fc5204929e2f1d Mon Sep 17 00:00:00 2001 From: James Seconde Date: Wed, 28 Jun 2023 12:37:52 +0100 Subject: [PATCH 11/11] Subaccounts API (#414) * Hacked IterableAPICollection for subaccounts. I really need the class to handle this, it's violating DRY * TDD, a lot of failing tests here * Big chunk of TDD and implementation, almost done. * Docs, factory implementation * Update src/Subaccount/Filter/Subaccount.php Co-authored-by: Chris Tankersley * Update test/Subaccount/ClientTest.php Co-authored-by: Chris Tankersley * Refactor hydrators to use the stock one * Typo in client factory * Namespace change * more explicit class name * DTO object passed into update * Add secret to DTO * refactor out array payload * split out transfer requests --------- Co-authored-by: Chris Tankersley --- README.md | 202 ++++++++-- phpunit.xml.dist | 3 + src/Client.php | 5 +- src/Entity/IterableAPICollection.php | 25 ++ src/Subaccount/Client.php | 123 ++++++ src/Subaccount/ClientFactory.php | 22 ++ src/Subaccount/Filter/SubaccountFilter.php | 97 +++++ .../Request/NumberTransferRequest.php | 99 +++++ .../Request/TransferBalanceRequest.php | 96 +++++ .../Request/TransferCreditRequest.php | 96 +++++ src/Subaccount/SubaccountObjects/Account.php | 210 ++++++++++ .../SubaccountObjects/BalanceTransfer.php | 130 ++++++ .../SubaccountObjects/CreditTransfer.php | 124 ++++++ test/Subaccount/ClientTest.php | 369 ++++++++++++++++++ test/Subaccount/Filter/SubaccountTest.php | 125 ++++++ .../Fixtures/Responses/create-success.json | 5 + .../get-balance-transfers-success.json | 22 ++ .../get-credit-transfers-success.json | 22 ++ .../Responses/get-individual-success.json | 10 + .../Responses/get-success-subaccounts.json | 36 ++ .../Fixtures/Responses/get-success.json | 26 ++ .../make-balance-transfer-success.json | 8 + .../make-credit-transfer-success.json | 8 + .../Responses/number-transfer-success.json | 6 + .../Fixtures/Responses/patch-success.json | 5 + .../Request/NumberTransferRequestTest.php | 63 +++ .../SubaccountObjects/AccountTest.php | 64 +++ 27 files changed, 1968 insertions(+), 33 deletions(-) create mode 100644 src/Subaccount/Client.php create mode 100644 src/Subaccount/ClientFactory.php create mode 100644 src/Subaccount/Filter/SubaccountFilter.php create mode 100644 src/Subaccount/Request/NumberTransferRequest.php create mode 100644 src/Subaccount/Request/TransferBalanceRequest.php create mode 100644 src/Subaccount/Request/TransferCreditRequest.php create mode 100644 src/Subaccount/SubaccountObjects/Account.php create mode 100644 src/Subaccount/SubaccountObjects/BalanceTransfer.php create mode 100644 src/Subaccount/SubaccountObjects/CreditTransfer.php create mode 100644 test/Subaccount/ClientTest.php create mode 100644 test/Subaccount/Filter/SubaccountTest.php create mode 100644 test/Subaccount/Fixtures/Responses/create-success.json create mode 100644 test/Subaccount/Fixtures/Responses/get-balance-transfers-success.json create mode 100644 test/Subaccount/Fixtures/Responses/get-credit-transfers-success.json create mode 100644 test/Subaccount/Fixtures/Responses/get-individual-success.json create mode 100644 test/Subaccount/Fixtures/Responses/get-success-subaccounts.json create mode 100644 test/Subaccount/Fixtures/Responses/get-success.json create mode 100644 test/Subaccount/Fixtures/Responses/make-balance-transfer-success.json create mode 100644 test/Subaccount/Fixtures/Responses/make-credit-transfer-success.json create mode 100644 test/Subaccount/Fixtures/Responses/number-transfer-success.json create mode 100644 test/Subaccount/Fixtures/Responses/patch-success.json create mode 100644 test/Subaccount/Request/NumberTransferRequestTest.php create mode 100644 test/Subaccount/SubaccountObjects/AccountTest.php diff --git a/README.md b/README.md index a0ea3642..1ccd3409 100644 --- a/README.md +++ b/README.md @@ -243,9 +243,9 @@ $viberImage = new Vonage\Messages\Channel\Viber\ViberImage( $client->messages()->send($viberImage); ``` -## Verify Examples (v1) +### Verify Examples (v1) -### Starting a Verification +#### Starting a Verification Vonage's [Verify API][doc_verify] makes it easy to prove that a user has provided their own phone number during signup, or implement second factor authentication during sign in. @@ -260,7 +260,7 @@ echo "Started verification with an id of: " . $response->getRequestId(); Once the user inputs the pin code they received, call the `check()` method (see below) with the request ID and the PIN to confirm the PIN is correct. -### Controlling a Verification +#### Controlling a Verification To cancel an in-progress verification, or to trigger the next attempt to send the confirmation code, you can pass either an existing verification object to the client library, or simply use a request ID: @@ -270,7 +270,7 @@ $client->verify()->trigger('00e6c3377e5348cdaf567e1417c707a5'); $client->verify()->cancel('00e6c3377e5348cdaf567e1417c707a5'); ``` -### Checking a Verification +#### Checking a Verification In the same way, checking a verification requires the PIN the user provided, and the request ID: @@ -284,7 +284,7 @@ try { } ``` -### Searching For a Verification +#### Searching For a Verification You can check the status of a verification, or access the results of past verifications using a request ID. The verification object will then provide a rich interface: @@ -298,7 +298,7 @@ foreach($verification->getChecks() as $check){ } ``` -### Payment Verification +#### Payment Verification Vonage's [Verify API][doc_verify] has SCA (Secure Customer Authentication) support, required by the PSD2 (Payment Services Directive) and used by applications that need to get confirmation from customers for payments. It includes the payee and the amount in the message. @@ -312,9 +312,9 @@ echo "Started verification with an id of: " . $response['request_id']; Once the user inputs the pin code they received, call the `/check` endpoint with the request ID and the pin to confirm the pin is correct. -## Verify Examples (v2) +### Verify Examples (v2) -### Starting a Verification +#### Starting a Verification Vonage's Verify v2 relies more on asynchronous workflows via. webhooks, and more customisable Verification workflows to the developer. To start a verification, you'll need the API client, which is under the namespace @@ -363,7 +363,7 @@ The base request types are as follows: For adding workflows, you can see the available valid workflows as constants within the `VerificationWorkflow` object. For a better developer experience, you can't create an invalid workflow due to the validation that happens on the object. -### Check a submitted code +#### Check a submitted code To submit a code, you'll need to surround the method in a try/catch due to the nature of the API. If the code is correct, the method will return a `true` boolean. If it fails, it will throw the relevant Exception from the API that will need to @@ -378,7 +378,7 @@ try { } ``` -### Webhooks +#### Webhooks As events happen during a verification workflow, events and updates will fired as webhooks. Incoming server requests that conform to PSR-7 standards can be hydrated into a webhook value object for nicer interactions. You can also hydrate @@ -398,7 +398,7 @@ $verificationEvent = \Vonage\Verify2\Webhook\Factory::createFromArray($payload); var_dump($verificationEvent->getStatus()); ``` -### Cancelling a request in-flight +#### Cancelling a request in-flight You can cancel a request should you need to, before the end user has taken any action. @@ -407,7 +407,7 @@ $requestId = 'c11236f4-00bf-4b89-84ba-88b25df97315'; $client->verify2()->cancel($requestId); ``` -### Making a Call +#### Making a Call All `$client->voice()` methods require the client to be constructed with a `Vonage\Client\Credentials\Keypair`, or a `Vonage\Client\Credentials\Container` that includes the `Keypair` credentials: @@ -907,28 +907,166 @@ try { Check out the [documentation](https://developer.nexmo.com/number-insight/code-snippets/number-insight-advanced-async-callback) for what to expect in the incoming webhook containing the data you requested. +### Subaccount Examples + +This API is used to create and configure subaccounts related to your primary account and transfer credit, balances and bought numbers between accounts. +The subaccounts API is disabled by default. If you want to use subaccounts, [contact support](https://api.support.vonage.com) to have the API enabled on your account. + +#### Get a list of Subaccounts + +```php +$client = new \Vonage\Client(new \Vonage\Client\Credentials\Basic(API_KEY, API_SECRET)); +$apiKey = '34kokdf'; +$subaccounts = $client->subaccount()->getSubaccounts($apiKey); +var_dump($subaccounts); +``` + +#### Create a Subaccount + +```php +$client = new \Vonage\Client(new \Vonage\Client\Credentials\Basic(API_KEY, API_SECRET)); + +$apiKey = 'acc6111f'; + +$payload = [ + 'name' => 'sub name', + 'secret' => 's5r3fds', + 'use_primary_account_balance' => false +]; + +$account = new Account(); +$account->fromArray($payload); + +$response = $client->subaccount()->createSubaccount($apiKey, $account); +var_dump($response); +``` + +#### Get a Subaccount + +```php +$client = new \Vonage\Client(new \Vonage\Client\Credentials\Basic(API_KEY, API_SECRET)); + +$apiKey = 'acc6111f'; +$subaccountKey = 'bbe6222f'; + +$response = $client->subaccount()->getSubaccount($apiKey, $subaccountKey); +var_dump($response); +``` + +#### Update a Subaccount + +```php +$client = new \Vonage\Client(new \Vonage\Client\Credentials\Basic(API_KEY, API_SECRET)); + +$apiKey = 'acc6111f'; +$subaccountKey = 'bbe6222f'; + +$payload = [ + 'suspended' => true, + 'use_primary_account_balance' => false, + 'name' => 'Subaccount department B' +]; + +$account = new Account(); +$account->fromArray($payload); + +$response = $client->subaccount()->updateSubaccount($apiKey, $subaccountKey, $account) +var_dump($response); +``` + +#### Get a list of Credit Transfers + +```php +$client = new \Vonage\Client(new \Vonage\Client\Credentials\Basic(API_KEY, API_SECRET)); + +$apiKey = 'acc6111f'; +$filter = new Vonage\Subaccount\Filter\Subaccount(['subaccount' => '35wsf5']) +$transfers = $client->subaccount()->getCreditTransfers($apiKey); +var_dump($transfers); +``` + +#### Transfer Credit between accounts + +```php +$client = new \Vonage\Client(new \Vonage\Client\Credentials\Basic(API_KEY, API_SECRET)); + +$apiKey = 'acc6111f'; + +$transferRequest = (new TransferCreditRequest($apiKey)) + ->setFrom('acc6111f') + ->setTo('s5r3fds') + ->setAmount('123.45') + ->setReference('this is a credit transfer'); + +$response = $this->subaccountClient->makeCreditTransfer($transferRequest); +``` + +#### Get a list of Balance Transfers + +```php +$client = new \Vonage\Client(new \Vonage\Client\Credentials\Basic(API_KEY, API_SECRET)); +$apiKey = 'acc6111f'; + +$filter = new \Vonage\Subaccount\Filter\Subaccount(['end_date' => '2022-10-02']); +$transfers = $client->subaccount()->getBalanceTransfers($apiKey, $filter); +``` + +#### Transfer Balance between accounts + +```php +$client = new \Vonage\Client(new \Vonage\Client\Credentials\Basic(API_KEY, API_SECRET)); + +$apiKey = 'acc6111f'; + +$transferRequest = (new TransferBalanceRequest($apiKey)) + ->setFrom('acc6111f') + ->setTo('s5r3fds') + ->setAmount('123.45') + ->setReference('this is a credit transfer'); + +$response = $client->subaccount()->makeBalanceTransfer($transferRequest); +var_dump($response); +``` + +#### Transfer a Phone Number between accounts + +```php +$client = new \Vonage\Client(new \Vonage\Client\Credentials\Basic(API_KEY, API_SECRET)); +$apiKey = 'acc6111f'; + +$numberTransferRequest = (new NumberTransferRequest($apiKey)) + ->setFrom('acc6111f') + ->setTo('s5r3fds') + ->setNumber('4477705478484') + ->setCountry('GB'); + +$response = $client->subaccount()->makeNumberTransfer($numberTransferRequest); +var_dump($response); +``` + ## Supported APIs -| API | API Release Status | Supported? -|------------------------|:--------------------:|:-------------:| -| Account API | General Availability |✅| -| Alerts API | General Availability |✅| -| Application API | General Availability |✅| -| Audit API | Beta |❌| -| Conversation API | Beta |❌| -| Dispatch API | Beta |❌| -| External Accounts API | Beta |❌| -| Media API | Beta | ❌| -| Messages API | General Availability |✅| -| Number Insight API | General Availability |✅| -| Number Management API | General Availability |✅| -| Pricing API | General Availability |✅| -| Redact API | General Availability |✅| -| Reports API | Beta |❌| -| SMS API | General Availability |✅| -| Verify API | General Availability |✅| -| Verify API (Version 2) | Beta |❌| -| Voice API | General Availability |✅| +| API | API Release Status | Supported? +|------------------------|:--------------------:|:----------:| +| Account API | General Availability | ✅ | +| Alerts API | General Availability | ✅ | +| Application API | General Availability | ✅ | +| Audit API | Beta | ❌ | +| Conversation API | Beta | ❌ | +| Dispatch API | Beta | ❌ | +| External Accounts API | Beta | ❌ | +| Media API | Beta | ❌ | +| Messages API | General Availability | ✅ | +| Number Insight API | General Availability | ✅ | +| Number Management API | General Availability | ✅ | +| Pricing API | General Availability | ✅ | +| Redact API | General Availability | ✅ | +| Reports API | Beta | ❌ | +| SMS API | General Availability | ✅ | +| Subaccounts API | General Availability | ✅ | +| Verify API | General Availability | ✅ | +| Verify API (Version 2) | Beta | ❌ | +| Voice API | General Availability | ✅ | ## Troubleshooting diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 05cad2e7..70e06a5a 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -32,6 +32,9 @@ test/SMS + + test/Subaccount + diff --git a/src/Client.php b/src/Client.php index 3ce630bc..244d7ec4 100644 --- a/src/Client.php +++ b/src/Client.php @@ -50,6 +50,7 @@ use Vonage\Redact\ClientFactory as RedactClientFactory; use Vonage\Secrets\ClientFactory as SecretsClientFactory; use Vonage\SMS\ClientFactory as SMSClientFactory; +use Vonage\Subaccount\ClientFactory as SubaccountClientFactory; use Vonage\Messages\ClientFactory as MessagesClientFactory; use Vonage\Verify\ClientFactory as VerifyClientFactory; use Vonage\Verify2\ClientFactory as Verify2ClientFactory; @@ -81,6 +82,7 @@ * @method Redact\Client redact() * @method Secrets\Client secrets() * @method SMS\Client sms() + * @method Subaccount\Client subaccount() * @method Verify\Client verify() * @method Verify2\Client verify2() * @method Voice\Client voice() @@ -218,6 +220,7 @@ public function __construct( 'redact' => RedactClientFactory::class, 'secrets' => SecretsClientFactory::class, 'sms' => SMSClientFactory::class, + 'subaccount' => SubaccountClientFactory::class, 'verify' => VerifyClientFactory::class, 'verify2' => Verify2ClientFactory::class, 'voice' => VoiceClientFactory::class, @@ -475,7 +478,7 @@ public function send(RequestInterface $request): ResponseInterface $response = $this->client->sendRequest($request); if ($this->debug) { - $id = uniqid(); + $id = uniqid('', true); $request->getBody()->rewind(); $response->getBody()->rewind(); $this->log( diff --git a/src/Entity/IterableAPICollection.php b/src/Entity/IterableAPICollection.php index 28a5ce62..e32b6cf3 100644 --- a/src/Entity/IterableAPICollection.php +++ b/src/Entity/IterableAPICollection.php @@ -89,6 +89,12 @@ class IterableAPICollection implements ClientAwareInterface, Iterator, Countable protected bool $isHAL = true; + /** + * Used if there are HAL elements, but entities are all in one request + * Specifically removes automatic pagination that adds request parameters + */ + protected bool $noQueryParameters = false; + /** * User set pgge sixe. */ @@ -443,6 +449,8 @@ protected function fetchPage($absoluteUri): void { //use filter if no query provided if (false === strpos($absoluteUri, '?')) { + $originalUri = $absoluteUri; + $query = []; if (isset($this->size)) { @@ -458,6 +466,11 @@ protected function fetchPage($absoluteUri): void } $absoluteUri .= '?' . http_build_query($query); + + // This is an override to completely remove request parameters for single requests + if ($this->getNoQueryParameters()) { + $absoluteUri = $originalUri; + } } $requestUri = $absoluteUri; @@ -554,4 +567,16 @@ public function setNaiveCount(bool $naiveCount): self return $this; } + + public function setNoQueryParameters(bool $noQueryParameters): self + { + $this->noQueryParameters = $noQueryParameters; + + return $this; + } + + public function getNoQueryParameters(): bool + { + return $this->noQueryParameters; + } } diff --git a/src/Subaccount/Client.php b/src/Subaccount/Client.php new file mode 100644 index 00000000..89c0e849 --- /dev/null +++ b/src/Subaccount/Client.php @@ -0,0 +1,123 @@ +api; + } + + public function getPrimaryAccount(string $apiKey): Account + { + $response = $this->api->get($apiKey . '/subaccounts'); + + return (new Account())->fromArray($response['_embedded'][self::PRIMARY_ACCOUNT_ARRAY_KEY]); + } + + public function getSubaccount(string $apiKey, string $subaccountApiKey): Account + { + $response = $this->api->get($apiKey . '/subaccounts/' . $subaccountApiKey); + return (new Account())->fromArray($response); + } + + public function getSubaccounts(string $apiKey): array + { + $api = clone $this->api; + $api->setCollectionName('subaccounts'); + + $collection = $this->api->search(null, '/' . $apiKey . '/subaccounts'); + $collection->setNoQueryParameters(true); + + $hydrator = new ArrayHydrator(); + $hydrator->setPrototype(new Account()); + $subaccounts = $collection->getPageData()['_embedded'][$api->getCollectionName()]; + + return array_map(function ($item) use ($hydrator) { + return $hydrator->hydrate($item); + }, $subaccounts); + } + + public function createSubaccount(string $apiKey, Account $account): ?array + { + return $this->api->create($account->toArray(), '/' . $apiKey . '/subaccounts'); + } + + public function makeBalanceTransfer(TransferBalanceRequest $transferRequest): BalanceTransfer + { + $response = $this->api->create($transferRequest->toArray(), '/' . $transferRequest->getApiKey() . '/balance-transfers'); + + return (new BalanceTransfer())->fromArray($response); + } + + public function makeCreditTransfer(TransferCreditRequest $transferRequest): CreditTransfer + { + $response = $this->api->create($transferRequest->toArray(), '/' . $transferRequest->getApiKey() . '/credit-transfers'); + return (new CreditTransfer())->fromArray($response); + } + + public function updateSubaccount(string $apiKey, string $subaccountApiKey, Account $account): ?array + { + return $this->api->partiallyUpdate($apiKey . '/subaccounts/' . $subaccountApiKey, $account->toArray()); + } + + public function getCreditTransfers(string $apiKey, FilterInterface $filter = null): mixed + { + if (!$filter) { + $filter = new EmptyFilter(); + } + + $response = $this->api->get($apiKey . '/credit-transfers', $filter->getQuery()); + + $hydrator = new ArrayHydrator(); + $hydrator->setPrototype(new CreditTransfer()); + $transfers = $response['_embedded']['credit_transfers']; + + return array_map(function ($item) use ($hydrator) { + return $hydrator->hydrate($item); + }, $transfers); + } + + public function getBalanceTransfers(string $apiKey, FilterInterface $filter = null): mixed + { + if (!$filter) { + $filter = new EmptyFilter(); + } + + $response = $this->api->get($apiKey . '/balance-transfers', $filter->getQuery()); + + $hydrator = new ArrayHydrator(); + $hydrator->setPrototype(new BalanceTransfer()); + $transfers = $response['_embedded']['balance_transfers']; + + return array_map(function ($item) use ($hydrator) { + return $hydrator->hydrate($item); + }, $transfers); + } + + public function makeNumberTransfer(NumberTransferRequest $request): ?array + { + return $this->api->create($request->toArray(), '/' . $request->getApiKey() . '/transfer-number'); + } +} diff --git a/src/Subaccount/ClientFactory.php b/src/Subaccount/ClientFactory.php new file mode 100644 index 00000000..ba399079 --- /dev/null +++ b/src/Subaccount/ClientFactory.php @@ -0,0 +1,22 @@ +make(APIResource::class); + $api->setIsHAL(true) + ->setErrorsOn200(false) + ->setBaseUrl('https://api.nexmo.com/accounts'); + + return new Client($api); + } +} \ No newline at end of file diff --git a/src/Subaccount/Filter/SubaccountFilter.php b/src/Subaccount/Filter/SubaccountFilter.php new file mode 100644 index 00000000..2103a8f1 --- /dev/null +++ b/src/Subaccount/Filter/SubaccountFilter.php @@ -0,0 +1,97 @@ + $value) { + if (! in_array($key, self::$possibleParameters, true)) { + throw new Request($value . ' is not a valid value'); + } + + if (!is_string($value)) { + throw new Request($value . ' is not a string'); + } + } + + if (array_key_exists('start_date', $filterValues)) { + $this->setStartDate($filterValues['start_date']); + } + + if ($this->startDate === '') { + $this->startDate = date('Y-m-d'); + } + + if (array_key_exists('end_date', $filterValues)) { + $this->setEndDate($filterValues['end_date']); + } + + if (array_key_exists('subaccount', $filterValues)) { + $this->setSubaccount($filterValues['subaccount']); + } + } + + public function getQuery() + { + $data = []; + + if ($this->getStartDate()) { + $data['start_date'] = $this->getStartDate(); + } + + if ($this->getEndDate()) { + $data['end_date'] = $this->getEndDate(); + } + + if ($this->getSubaccount()) { + $data['subaccount'] = $this->getSubaccount(); + } + + return $data; + } + + public function getEndDate(): ?string + { + return $this->endDate; + } + + public function setEndDate(?string $endDate): void + { + $this->endDate = $endDate; + } + + public function getStartDate(): ?string + { + return $this->startDate; + } + + public function setStartDate(?string $startDate): void + { + $this->startDate = $startDate; + } + + public function getSubaccount(): ?string + { + return $this->subaccount; + } + + public function setSubaccount(?string $subaccount): void + { + $this->subaccount = $subaccount; + } +} \ No newline at end of file diff --git a/src/Subaccount/Request/NumberTransferRequest.php b/src/Subaccount/Request/NumberTransferRequest.php new file mode 100644 index 00000000..e0ca9e7c --- /dev/null +++ b/src/Subaccount/Request/NumberTransferRequest.php @@ -0,0 +1,99 @@ +from = $from; + + return $this; + } + + public function getFrom(): string + { + return $this->from; + } + + public function setTo(string $to): self + { + $this->to = $to; + + return $this; + } + + public function getTo(): string + { + return $this->to; + } + + public function setNumber(string $number): self + { + $this->number = $number; + + return $this; + } + + public function getNumber(): string + { + return $this->number; + } + + public function setCountry(string $country): self + { + $this->country = $country; + + return $this; + } + + public function getCountry(): string + { + return $this->country; + } + + public function fromArray(array $data): self + { + $this->from = $data['from'] ?? ''; + $this->to = $data['to'] ?? ''; + $this->number = $data['number'] ?? ''; + $this->country = $data['country'] ?? ''; + + return $this; + } + + public function toArray(): array + { + return [ + 'from' => $this->getFrom(), + 'to' => $this->getTo(), + 'number' => $this->getNumber(), + 'country' => $this->getCountry(), + ]; + } + + /** + * @return string + */ + public function getApiKey(): string + { + return $this->apiKey; + } + + public function setApiKey(string $apiKey): self + { + $this->apiKey = $apiKey; + + return $this; + } +} diff --git a/src/Subaccount/Request/TransferBalanceRequest.php b/src/Subaccount/Request/TransferBalanceRequest.php new file mode 100644 index 00000000..1ceda4ae --- /dev/null +++ b/src/Subaccount/Request/TransferBalanceRequest.php @@ -0,0 +1,96 @@ +from; + } + + public function setFrom(string $from): self + { + $this->from = $from; + + return $this; + } + + public function getTo(): string + { + return $this->to; + } + + public function setTo(string $to): self + { + $this->to = $to; + + return $this; + } + + public function getAmount(): string + { + return $this->amount; + } + + public function setAmount(string $amount): self + { + $this->amount = $amount; + + return $this; + } + + public function getReference(): string + { + return $this->reference; + } + + public function setReference(string $reference): self + { + $this->reference = $reference; + + return $this; + } + + public function getApiKey(): string + { + return $this->apiKey; + } + + public function setApiKey(string $apiKey): self + { + $this->apiKey = $apiKey; + + return $this; + } + + public function fromArray(array $data): static + { + $this->from = $data['from']; + $this->to = $data['to']; + $this->amount = $data['amount']; + $this->reference = $data['reference']; + + return $this; + } + + public function toArray(): array + { + return [ + 'from' => $this->getFrom(), + 'to' => $this->getTo(), + 'amount' => (float)$this->getAmount(), + 'reference' => $this->getReference() + ]; + } +} diff --git a/src/Subaccount/Request/TransferCreditRequest.php b/src/Subaccount/Request/TransferCreditRequest.php new file mode 100644 index 00000000..59d6695f --- /dev/null +++ b/src/Subaccount/Request/TransferCreditRequest.php @@ -0,0 +1,96 @@ +from; + } + + public function setFrom(string $from): self + { + $this->from = $from; + + return $this; + } + + public function getTo(): string + { + return $this->to; + } + + public function setTo(string $to): self + { + $this->to = $to; + + return $this; + } + + public function getAmount(): string + { + return $this->amount; + } + + public function setAmount(string $amount): self + { + $this->amount = $amount; + + return $this; + } + + public function getReference(): string + { + return $this->reference; + } + + public function setReference(string $reference): self + { + $this->reference = $reference; + + return $this; + } + + public function getApiKey(): string + { + return $this->apiKey; + } + + public function setApiKey(string $apiKey): self + { + $this->apiKey = $apiKey; + + return $this; + } + + public function fromArray(array $data): static + { + $this->from = $data['from']; + $this->to = $data['to']; + $this->amount = $data['amount']; + $this->reference = $data['reference']; + + return $this; + } + + public function toArray(): array + { + return [ + 'from' => $this->getFrom(), + 'to' => $this->getTo(), + 'amount' => (float)$this->getAmount(), + 'reference' => $this->getReference() + ]; + } +} diff --git a/src/Subaccount/SubaccountObjects/Account.php b/src/Subaccount/SubaccountObjects/Account.php new file mode 100644 index 00000000..e5a4e4fa --- /dev/null +++ b/src/Subaccount/SubaccountObjects/Account.php @@ -0,0 +1,210 @@ +apiKey; + } + + public function setApiKey(?string $apiKey): static + { + $this->apiKey = $apiKey; + + return $this; + } + + public function getName(): ?string + { + return $this->name; + } + + public function setName(?string $name): static + { + $this->name = $name; + + return $this; + } + + public function getPrimaryAccountApiKey(): ?string + { + return $this->primaryAccountApiKey; + } + + public function setPrimaryAccountApiKey(?string $primaryAccountApiKey): static + { + $this->primaryAccountApiKey = $primaryAccountApiKey; + + return $this; + } + + public function getUsePrimaryAccountBalance(): ?bool + { + return $this->usePrimaryAccountBalance; + } + + public function setUsePrimaryAccountBalance(?bool $usePrimaryAccountBalance): static + { + $this->usePrimaryAccountBalance = $usePrimaryAccountBalance; + + return $this; + } + + public function getCreatedAt(): ?string + { + return $this->createdAt; + } + + public function setCreatedAt(?string $createdAt): static + { + $this->createdAt = $createdAt; + + return $this; + } + + public function getSuspended(): ?bool + { + return $this->suspended; + } + + public function setSuspended(?bool $suspended): static + { + $this->suspended = $suspended; + + return $this; + } + + public function getBalance(): ?float + { + return $this->balance; + } + + public function setBalance(?float $balance): static + { + $this->balance = $balance; + + return $this; + } + + public function getCreditLimit(): ?float + { + return $this->creditLimit; + } + + public function setCreditLimit(?float $creditLimit): static + { + $this->creditLimit = $creditLimit; + + return $this; + } + + public function toArray(): array + { + $data = []; + + if ($this->apiKey !== null) { + $data['api_key'] = $this->getApiKey(); + } + + if ($this->name !== null) { + $data['name'] = $this->getName(); + } + + if ($this->primaryAccountApiKey !== null) { + $data['primary_account_api_key'] = $this->getPrimaryAccountApiKey(); + } + + if ($this->usePrimaryAccountBalance !== null) { + $data['use_primary_account_balance'] = $this->getUsePrimaryAccountBalance(); + } + + if ($this->createdAt !== null) { + $data['created_at'] = $this->getCreatedAt(); + } + + if ($this->suspended !== null) { + $data['suspended'] = $this->getSuspended(); + } + + if ($this->balance !== null) { + $data['balance'] = $this->getBalance(); + } + + if ($this->creditLimit !== null) { + $data['credit_limit'] = $this->getCreditLimit(); + } + + if ($this->secret !== null) { + $data['secret'] = $this->getSecret(); + } + + return $data; + } + + public function fromArray(array $data): static + { + if (isset($data['api_key'])) { + $this->apiKey = $data['api_key']; + } + + if (isset($data['name'])) { + $this->name = $data['name']; + } + + if (isset($data['primary_account_api_key'])) { + $this->primaryAccountApiKey = $data['primary_account_api_key']; + } + + if (isset($data['use_primary_account_balance'])) { + $this->usePrimaryAccountBalance = $data['use_primary_account_balance']; + } + + if (isset($data['created_at'])) { + $this->createdAt = $data['created_at']; + } + + if (isset($data['suspended'])) { + $this->suspended = $data['suspended']; + } + + if (isset($data['balance'])) { + $this->balance = $data['balance']; + } + + if (isset($data['credit_limit'])) { + $this->creditLimit = $data['credit_limit']; + } + + if (isset($data['secret'])) { + $this->secret = $data['secret']; + } + + return $this; + } + + public function getSecret(): ?string + { + return $this->secret; + } + + public function setSecret(?string $secret): void + { + $this->secret = $secret; + } +} diff --git a/src/Subaccount/SubaccountObjects/BalanceTransfer.php b/src/Subaccount/SubaccountObjects/BalanceTransfer.php new file mode 100644 index 00000000..bf9653cd --- /dev/null +++ b/src/Subaccount/SubaccountObjects/BalanceTransfer.php @@ -0,0 +1,130 @@ +balanceTransferId; + } + + public function setCreditTransferId(string $balanceTransferId): self + { + $this->balanceTransferId = $balanceTransferId; + + return $this; + } + + public function getAmount(): float + { + return $this->amount; + } + + public function setAmount(float $amount): self + { + $this->amount = $amount; + + return $this; + } + + public function getFrom(): string + { + return $this->from; + } + + public function setFrom(string $from): self + { + $this->from = $from; + + return $this; + } + + public function getTo(): string + { + return $this->to; + } + + public function setTo(string $to): self + { + $this->to = $to; + + return $this; + } + + public function getReference(): string + { + return $this->reference; + } + + public function setReference(string $reference): self + { + $this->reference = $reference; + + return $this; + } + + public function getCreatedAt(): string + { + return $this->createdAt; + } + + public function setCreatedAt(string $createdAt): self + { + $this->createdAt = $createdAt; + + return $this; + } + + public function fromArray(array $data): static + { + if (isset($data['balance_transfer_id'])) { + $this->setCreditTransferId($data['balance_transfer_id']); + } + + if (isset($data['amount'])) { + $this->setAmount($data['amount']); + } + + if (isset($data['from'])) { + $this->setFrom($data['from']); + } + + if (isset($data['to'])) { + $this->setTo($data['to']); + } + + if (isset($data['reference'])) { + $this->setReference($data['reference']); + } + + if (isset($data['created_at'])) { + $this->setCreatedAt($data['created_at']); + } + + return $this; + } + + public function toArray(): array + { + return [ + 'id' => $this->getBalanceTransferId(), + 'amount' => $this->getAmount(), + 'from' => $this->getFrom(), + 'to' => $this->getTo(), + 'reference' => $this->getReference(), + 'created_at' => $this->getCreatedAt(), + ]; + } +} diff --git a/src/Subaccount/SubaccountObjects/CreditTransfer.php b/src/Subaccount/SubaccountObjects/CreditTransfer.php new file mode 100644 index 00000000..46fb0a41 --- /dev/null +++ b/src/Subaccount/SubaccountObjects/CreditTransfer.php @@ -0,0 +1,124 @@ +creditTransferId; + } + + public function setCreditTransferId(string $creditTransferId): self + { + $this->creditTransferId = $creditTransferId; + return $this; + } + + public function getAmount(): float + { + return $this->amount; + } + + public function setAmount(float $amount): self + { + $this->amount = $amount; + return $this; + } + + public function getFrom(): string + { + return $this->from; + } + + public function setFrom(string $from): self + { + $this->from = $from; + return $this; + } + + public function getTo(): string + { + return $this->to; + } + + public function setTo(string $to): self + { + $this->to = $to; + return $this; + } + + public function getReference(): string + { + return $this->reference; + } + + public function setReference(string $reference): self + { + $this->reference = $reference; + return $this; + } + + public function getCreatedAt(): string + { + return $this->createdAt; + } + + public function setCreatedAt(string $createdAt): self + { + $this->createdAt = $createdAt; + return $this; + } + + public function fromArray(array $data): static + { + if (isset($data['credit_transfer_id'])) { + $this->setCreditTransferId($data['credit_transfer_id']); + } + + if (isset($data['amount'])) { + $this->setAmount($data['amount']); + } + + if (isset($data['from'])) { + $this->setFrom($data['from']); + } + + if (isset($data['to'])) { + $this->setTo($data['to']); + } + + if (isset($data['reference'])) { + $this->setReference($data['reference']); + } + + if (isset($data['created_at'])) { + $this->setCreatedAt($data['created_at']); + } + + return $this; + } + + public function toArray(): array + { + return [ + 'id' => $this->getCreditTransferId(), + 'amount' => $this->getAmount(), + 'from' => $this->getFrom(), + 'to' => $this->getTo(), + 'reference' => $this->getReference(), + 'created_at' => $this->getCreatedAt(), + ]; + } +} diff --git a/test/Subaccount/ClientTest.php b/test/Subaccount/ClientTest.php new file mode 100644 index 00000000..9c0f4fa9 --- /dev/null +++ b/test/Subaccount/ClientTest.php @@ -0,0 +1,369 @@ +vonageClient = $this->prophesize(Client::class); + $this->vonageClient->getCredentials()->willReturn( + new Client\Credentials\Basic('abc', 'def'), + ); + + /** @noinspection PhpParamsInspection */ + $this->api = (new APIResource()) + ->setIsHAL(true) + ->setErrorsOn200(false) + ->setClient($this->vonageClient->reveal()) + ->setBaseUrl('https://api.nexmo.com/accounts'); + + $this->subaccountClient = new SubaccountClient($this->api); + } + + public function testClientInitialises(): void + { + $this->assertInstanceOf(SubaccountClient::class, $this->subaccountClient); + } + + public function testUsesCorrectAuth(): void + { + $this->vonageClient->send(Argument::that(function (RequestInterface $request) { + $this->assertEquals( + 'Basic ', + mb_substr($request->getHeaders()['Authorization'][0], 0, 6) + ); + return true; + }))->willReturn($this->getResponse('get-success')); + + $apiKey = 'something'; + $response = $this->subaccountClient->getPrimaryAccount($apiKey); + } + + public function testWillUpdateSubaccount(): void + { + $apiKey = 'acc6111f'; + $subaccountKey = 'bbe6222f'; + + $payload = [ + 'suspended' => true, + 'use_primary_account_balance' => false, + 'name' => 'Subaccount department B' + ]; + + $account = (new Account())->fromArray($payload); + + $this->vonageClient->send(Argument::that(function (RequestInterface $request) { + $uri = $request->getUri(); + $uriString = $uri->__toString(); + $this->assertEquals( + 'https://api.nexmo.com/accounts/acc6111f/subaccounts/bbe6222f', + $uriString + ); + $this->assertRequestMethod('PATCH', $request); + + return true; + }))->willReturn($this->getResponse('patch-success')); + + $response = $this->subaccountClient->updateSubaccount($apiKey, $subaccountKey, $account); + $this->assertIsArray($response); + } + + public function testCanGetPrimaryAccount(): void + { + $apiKey = 'acc6111f'; + + $this->vonageClient->send(Argument::that(function (RequestInterface $request) { + $uri = $request->getUri(); + $uriString = $uri->__toString(); + $this->assertEquals( + 'https://api.nexmo.com/accounts/acc6111f/subaccounts', + $uriString + ); + $this->assertRequestMethod('GET', $request); + + + return true; + }))->willReturn($this->getResponse('get-success')); + + $response = $this->subaccountClient->getPrimaryAccount($apiKey); + $this->assertInstanceOf(Account::class, $response); + } + + public function testWillCreateSubaccount(): void + { + $apiKey = 'acc6111f'; + + $payload = [ + 'name' => 'sub name', + 'secret' => 's5r3fds', + 'use_primary_account_balance' => false + ]; + + $account = (new Account())->fromArray($payload); + + $this->vonageClient->send(Argument::that(function (RequestInterface $request) { + $uri = $request->getUri(); + $uriString = $uri->__toString(); + $this->assertEquals( + 'https://api.nexmo.com/accounts/acc6111f/subaccounts', + $uriString + ); + $this->assertRequestMethod('POST', $request); + $this->assertRequestJsonBodyContains('name', 'sub name', $request); + $this->assertRequestJsonBodyContains('secret', 's5r3fds', $request); + $this->assertRequestJsonBodyContains('use_primary_account_balance', false, $request); + + return true; + }))->willReturn($this->getResponse('create-success')); + + $response = $this->subaccountClient->createSubaccount($apiKey, $account); + $this->assertEquals('sub name', $response['name']); + $this->assertEquals('s5r3fds', $response['secret']); + $this->assertEquals(false, $response['use_primary_account_balance']); + } + + public function testWillGetAccount(): void + { + $apiKey = 'acc6111f'; + $subaccountKey = 'bbe6222f'; + + $this->vonageClient->send(Argument::that(function (RequestInterface $request) { + $uri = $request->getUri(); + $uriString = $uri->__toString(); + $this->assertEquals( + 'https://api.nexmo.com/accounts/acc6111f/subaccounts/bbe6222f', + $uriString + ); + $this->assertRequestMethod('GET', $request); + + + return true; + }))->willReturn($this->getResponse('get-individual-success')); + + $response = $this->subaccountClient->getSubaccount($apiKey, $subaccountKey); + $this->assertInstanceOf(Account::class, $response); + $this->assertEquals('Get Subaccount', $response->getName()); + } + + public function testCanGetSubaccounts(): void + { + $apiKey = 'acc6111f'; + + $this->vonageClient->send(Argument::that(function (RequestInterface $request) { + $uri = $request->getUri(); + $uriString = $uri->__toString(); + $this->assertEquals( + 'https://api.nexmo.com/accounts/acc6111f/subaccounts', + $uriString + ); + $this->assertRequestMethod('GET', $request); + + return true; + }))->willReturn($this->getResponse('get-success-subaccounts')); + + $response = $this->subaccountClient->getSubaccounts($apiKey); + + foreach ($response as $item) { + $this->assertInstanceOf(Account::class, $item); + } + } + + public function testWillTransferCredit(): void + { + $apiKey = 'acc6111f'; + + $this->vonageClient->send(Argument::that(function (RequestInterface $request) { + $uri = $request->getUri(); + $uriString = $uri->__toString(); + $this->assertEquals( + 'https://api.nexmo.com/accounts/acc6111f/credit-transfers', + $uriString + ); + $this->assertRequestMethod('POST', $request); + $this->assertRequestJsonBodyContains('from', 'acc6111f', $request); + $this->assertRequestJsonBodyContains('to', 's5r3fds', $request); + $this->assertRequestJsonBodyContains('amount', 123.45, $request); + $this->assertRequestJsonBodyContains('reference', 'this is a credit transfer', $request); + + return true; + }))->willReturn($this->getResponse('make-credit-transfer-success')); + + $transferRequest = (new TransferCreditRequest($apiKey)) + ->setFrom('acc6111f') + ->setTo('s5r3fds') + ->setAmount('123.45') + ->setReference('this is a credit transfer'); + + $response = $this->subaccountClient->makeCreditTransfer($transferRequest); + $this->assertInstanceOf(CreditTransfer::class, $response); + } + + public function testWillListCreditTransfers(): void + { + $apiKey = 'acc6111f'; + + $this->vonageClient->send(Argument::that(function (RequestInterface $request) { + $uri = $request->getUri(); + $uriString = $uri->__toString(); + $this->assertEquals( + 'https://api.nexmo.com/accounts/acc6111f/credit-transfers?start_date=2022-01-01&end_date=2022-01-05&subaccount=s5r3fds', + $uriString + ); + $this->assertRequestMethod('GET', $request); + $this->assertRequestQueryContains('start_date', '2022-01-01', $request); + $this->assertRequestQueryContains('end_date', '2022-01-05', $request); + $this->assertRequestQueryContains('subaccount', 's5r3fds', $request); + + return true; + }))->willReturn($this->getResponse('get-credit-transfers-success')); + + $filter = new SubaccountFilter([ + 'start_date' => '2022-01-01', + 'end_date'=> '2022-01-05', + 'subaccount' => 's5r3fds' + ]); + + $response = $this->subaccountClient->getCreditTransfers($apiKey, $filter); + + foreach ($response as $item) { + $this->assertInstanceOf(CreditTransfer::class, $item); + } + + $this->assertCount(2, $response); + } + + public function testWillListBalanceTransfers(): void + { + $apiKey = 'acc6111f'; + + $this->vonageClient->send(Argument::that(function (RequestInterface $request) { + $uri = $request->getUri(); + $uriString = $uri->__toString(); + $this->assertEquals( + 'https://api.nexmo.com/accounts/acc6111f/balance-transfers?start_date=2022-01-01&end_date=2022-01-05&subaccount=s5r3fds', + $uriString + ); + $this->assertRequestMethod('GET', $request); + $this->assertRequestQueryContains('start_date', '2022-01-01', $request); + $this->assertRequestQueryContains('end_date', '2022-01-05', $request); + $this->assertRequestQueryContains('subaccount', 's5r3fds', $request); + + return true; + }))->willReturn($this->getResponse('get-balance-transfers-success')); + + $filter = new SubaccountFilter([ + 'start_date' => '2022-01-01', + 'end_date'=> '2022-01-05', + 'subaccount' => 's5r3fds' + ]); + + $response = $this->subaccountClient->getBalanceTransfers($apiKey, $filter); + + foreach ($response as $item) { + $this->assertInstanceOf(BalanceTransfer::class, $item); + } + + $this->assertCount(2, $response); + } + + public function testCanTransferBalance(): void + { + $apiKey = 'acc6111f'; + + $this->vonageClient->send(Argument::that(function (RequestInterface $request) { + $uri = $request->getUri(); + $uriString = $uri->__toString(); + $this->assertEquals( + 'https://api.nexmo.com/accounts/acc6111f/balance-transfers', + $uriString + ); + $this->assertRequestMethod('POST', $request); + + $this->assertRequestJsonBodyContains('from', 'acc6111f', $request); + $this->assertRequestJsonBodyContains('to', 's5r3fds', $request); + $this->assertRequestJsonBodyContains('amount', 123.45, $request); + $this->assertRequestJsonBodyContains('reference', 'this is a balance transfer', $request); + + return true; + }))->willReturn($this->getResponse('make-balance-transfer-success')); + + $balanceTransferRequest = (new TransferBalanceRequest($apiKey)) + ->setTo('s5r3fds') + ->setFrom('acc6111f') + ->setAmount('123.45') + ->setReference('this is a balance transfer'); + + $response = $this->subaccountClient->makeBalanceTransfer($balanceTransferRequest); + + $this->assertInstanceOf(BalanceTransfer::class, $response); + } + + public function testWillTransferNumber(): void + { + $apiKey = 'acc6111f'; + + $this->vonageClient->send(Argument::that(function (RequestInterface $request) { + $uri = $request->getUri(); + $uriString = $uri->__toString(); + $this->assertEquals( + 'https://api.nexmo.com/accounts/acc6111f/transfer-number', + $uriString + ); + $this->assertRequestMethod('POST', $request); + + $this->assertRequestJsonBodyContains('from', 'acc6111f', $request); + $this->assertRequestJsonBodyContains('to', 's5r3fds', $request); + $this->assertRequestJsonBodyContains('number', '4477705478484', $request); + $this->assertRequestJsonBodyContains('country', 'GB', $request); + + return true; + }))->willReturn($this->getResponse('number-transfer-success')); + + $numberTransferRequest = (new NumberTransferRequest( + $apiKey, + 'acc6111f', + 's5r3fds', + '4477705478484', + 'GB' + )); + + $response = $this->subaccountClient->makeNumberTransfer($numberTransferRequest); + $this->assertIsArray($response); + $this->assertEquals('acc6111f', $response['from']); + } + + /** + * This method gets the fixtures and wraps them in a Response object to mock the API + */ + protected function getResponse(string $identifier, int $status = 200): Response + { + return new Response(fopen(__DIR__ . '/Fixtures/Responses/' . $identifier . '.json', 'rb'), $status); + } +} diff --git a/test/Subaccount/Filter/SubaccountTest.php b/test/Subaccount/Filter/SubaccountTest.php new file mode 100644 index 00000000..a074e76e --- /dev/null +++ b/test/Subaccount/Filter/SubaccountTest.php @@ -0,0 +1,125 @@ + '2023-01-01', + 'end_date' => '2023-06-30', + 'subaccount' => 'my_subaccount' + ]; + + $subaccountFilter = new SubaccountFilter($filterValues); + $expectedQuery = [ + 'start_date' => '2023-01-01', + 'end_date' => '2023-06-30', + 'subaccount' => 'my_subaccount' + ]; + + $this->assertEquals($expectedQuery, $subaccountFilter->getQuery()); + } + + public function testWillDefaultStartDate(): void + { + $subaccountFilter = new SubaccountFilter([]); + $today = date('Y-m-d'); + $this->assertEquals($today, $subaccountFilter->getStartDate()); + } + + public function testGetStartDate(): void + { + $filterValues = [ + 'start_date' => '2023-01-01' + ]; + + $subaccountFilter = new SubaccountFilter($filterValues); + $this->assertEquals('2023-01-01', $subaccountFilter->getStartDate()); + } + + public function testSetStartDate(): void + { + $subaccountFilter = new SubaccountFilter([]); + $subaccountFilter->setStartDate('2023-01-01'); + $this->assertEquals('2023-01-01', $subaccountFilter->getStartDate()); + } + + public function testGetEndDate(): void + { + $filterValues = [ + 'end_date' => '2023-06-30' + ]; + + $subaccountFilter = new SubaccountFilter($filterValues); + $this->assertEquals('2023-06-30', $subaccountFilter->getEndDate()); + } + + public function testSetEndDate(): void + { + $subaccountFilter = new SubaccountFilter([]); + $subaccountFilter->setEndDate('2023-06-30'); + $this->assertEquals('2023-06-30', $subaccountFilter->getEndDate()); + } + + public function testGetSubaccount(): void + { + $filterValues = [ + 'subaccount' => 'my_subaccount' + ]; + + $subaccountFilter = new SubaccountFilter($filterValues); + $this->assertEquals('my_subaccount', $subaccountFilter->getSubaccount()); + } + + public function testSetSubaccount(): void + { + $subaccountFilter = new SubaccountFilter([]); + $subaccountFilter->setSubaccount('my_subaccount'); + $this->assertEquals('my_subaccount', $subaccountFilter->getSubaccount()); + } + + public function testConstructionWithValidValues(): void + { + $filterValues = [ + 'start_date' => '2023-01-01', + 'end_date' => '2023-06-30', + 'subaccount' => 'my_subaccount' + ]; + + $subaccountFilter = new SubaccountFilter($filterValues); + + $this->assertInstanceOf(SubaccountFilter::class, $subaccountFilter); + } + + public function testConstructionWithInvalidKeyThrowsException(): void + { + $this->expectException(Request::class); + + $filterValues = [ + 'start_date' => '2023-01-01', + 'end_date' => '2023-06-30', + 'invalid_key' => 'value' + ]; + + new SubaccountFilter($filterValues); + } + + public function testConstructionWithNonStringValueThrowsException(): void + { + $this->expectException(Request::class); + + $filterValues = [ + 'start_date' => '2023-01-01', + 'end_date' => '2023-06-30', + 'subaccount' => 12345 + ]; + + new SubaccountFilter($filterValues); + } +} diff --git a/test/Subaccount/Fixtures/Responses/create-success.json b/test/Subaccount/Fixtures/Responses/create-success.json new file mode 100644 index 00000000..431c01c0 --- /dev/null +++ b/test/Subaccount/Fixtures/Responses/create-success.json @@ -0,0 +1,5 @@ +{ + "name": "sub name", + "secret": "s5r3fds", + "use_primary_account_balance": false +} \ No newline at end of file diff --git a/test/Subaccount/Fixtures/Responses/get-balance-transfers-success.json b/test/Subaccount/Fixtures/Responses/get-balance-transfers-success.json new file mode 100644 index 00000000..599d532d --- /dev/null +++ b/test/Subaccount/Fixtures/Responses/get-balance-transfers-success.json @@ -0,0 +1,22 @@ +{ + "_embedded": { + "balance_transfers": [ + { + "balance_transfer_id": "07b5-46e1-a527-85530e625800", + "amount": 123.45, + "from": "7c9738e6", + "to": "ad6dc56f", + "reference": "This gets added to the audit log", + "created_at": "2019-03-02T16:34:49Z" + }, + { + "balance_transfer_id": "b9a8-43d2-c627-90f4c24a75e1", + "amount": 987.65, + "from": "2e7f8a4d", + "to": "e3b59f71", + "reference": "Another balance transfer", + "created_at": "2023-06-12T09:15:30Z" + } + ] + } +} diff --git a/test/Subaccount/Fixtures/Responses/get-credit-transfers-success.json b/test/Subaccount/Fixtures/Responses/get-credit-transfers-success.json new file mode 100644 index 00000000..b144a2bd --- /dev/null +++ b/test/Subaccount/Fixtures/Responses/get-credit-transfers-success.json @@ -0,0 +1,22 @@ +{ + "_embedded": { + "credit_transfers": [ + { + "credit_transfer_id": "07b5-46e1-a527-85530e625800", + "amount": 123.45, + "from": "7c9738e6", + "to": "ad6dc56f", + "reference": "This gets added to the audit log", + "created_at": "2019-03-02T16:34:49Z" + }, + { + "credit_transfer_id": "b9a8-43d2-c627-90f4c24a75e1", + "amount": 987.65, + "from": "2e7f8a4d", + "to": "e3b59f71", + "reference": "Another credit transfer", + "created_at": "2023-06-12T09:15:30Z" + } + ] + } +} diff --git a/test/Subaccount/Fixtures/Responses/get-individual-success.json b/test/Subaccount/Fixtures/Responses/get-individual-success.json new file mode 100644 index 00000000..b7427d7b --- /dev/null +++ b/test/Subaccount/Fixtures/Responses/get-individual-success.json @@ -0,0 +1,10 @@ +{ + "api_key": "bbe6222f", + "name": "Get Subaccount", + "primary_account_api_key": "acc6111f", + "use_primary_account_balance": true, + "created_at": "2018-03-02T16:34:49Z", + "suspended": false, + "balance": 100.25, + "credit_limit": -100.25 +} \ No newline at end of file diff --git a/test/Subaccount/Fixtures/Responses/get-success-subaccounts.json b/test/Subaccount/Fixtures/Responses/get-success-subaccounts.json new file mode 100644 index 00000000..029938f6 --- /dev/null +++ b/test/Subaccount/Fixtures/Responses/get-success-subaccounts.json @@ -0,0 +1,36 @@ +{ + "_embedded": { + "primary_account": { + "api_key": "bbe6222f", + "name": "Subaccount department A", + "primary_account_api_key": "acc6111f", + "use_primary_account_balance": true, + "created_at": "2018-03-02T16:34:49Z", + "suspended": false, + "balance": 100.25, + "credit_limit": -100.25 + }, + "subaccounts": [ + { + "api_key": "df4rtyr8", + "name": "Subaccount department B", + "primary_account_api_key": "acc6111f", + "use_primary_account_balance": true, + "created_at": "2018-03-02T16:34:49Z", + "suspended": false, + "balance": 100.25, + "credit_limit": -100.25 + }, + { + "api_key": "887de4r", + "name": "Subaccount department C", + "primary_account_api_key": "acc6111f", + "use_primary_account_balance": true, + "created_at": "2018-03-02T16:34:49Z", + "suspended": false, + "balance": 100.25, + "credit_limit": -100.25 + } + ] + } +} \ No newline at end of file diff --git a/test/Subaccount/Fixtures/Responses/get-success.json b/test/Subaccount/Fixtures/Responses/get-success.json new file mode 100644 index 00000000..06e84596 --- /dev/null +++ b/test/Subaccount/Fixtures/Responses/get-success.json @@ -0,0 +1,26 @@ +{ + "_embedded": { + "primary_account": { + "api_key": "bbe6222f", + "name": "Subaccount department A", + "primary_account_api_key": "acc6111f", + "use_primary_account_balance": true, + "created_at": "2018-03-02T16:34:49Z", + "suspended": false, + "balance": 100.25, + "credit_limit": -100.25 + }, + "subaccounts": [ + { + "api_key": "dfert466", + "name": "Subaccount department B", + "primary_account_api_key": "acc6111f", + "use_primary_account_balance": true, + "created_at": "2018-03-02T16:34:49Z", + "suspended": false, + "balance": 100.25, + "credit_limit": -100.25 + } + ] + } +} \ No newline at end of file diff --git a/test/Subaccount/Fixtures/Responses/make-balance-transfer-success.json b/test/Subaccount/Fixtures/Responses/make-balance-transfer-success.json new file mode 100644 index 00000000..0e5d4631 --- /dev/null +++ b/test/Subaccount/Fixtures/Responses/make-balance-transfer-success.json @@ -0,0 +1,8 @@ +{ + "balance_transfer_id": "07b5-46e1-a527-85530e625800", + "amount": 123.45, + "from": "acc6111f", + "to": "s5r3fds", + "reference": "this is a balance transfer", + "created_at": "2019-03-02T16:34:49Z" +} \ No newline at end of file diff --git a/test/Subaccount/Fixtures/Responses/make-credit-transfer-success.json b/test/Subaccount/Fixtures/Responses/make-credit-transfer-success.json new file mode 100644 index 00000000..0116aeb1 --- /dev/null +++ b/test/Subaccount/Fixtures/Responses/make-credit-transfer-success.json @@ -0,0 +1,8 @@ +{ + "credit_transfer_id": "07b5-46e1-a527-85530e625800", + "amount": 123.45, + "from": "7c9738e6", + "to": "ad6dc56f", + "reference": "This gets added to the audit log", + "created_at": "2019-03-02T16:34:49Z" +} \ No newline at end of file diff --git a/test/Subaccount/Fixtures/Responses/number-transfer-success.json b/test/Subaccount/Fixtures/Responses/number-transfer-success.json new file mode 100644 index 00000000..23aebdc1 --- /dev/null +++ b/test/Subaccount/Fixtures/Responses/number-transfer-success.json @@ -0,0 +1,6 @@ +{ + "from": "acc6111f", + "to": "s5r3fds", + "number": "4477705478484", + "country": "GB" +} \ No newline at end of file diff --git a/test/Subaccount/Fixtures/Responses/patch-success.json b/test/Subaccount/Fixtures/Responses/patch-success.json new file mode 100644 index 00000000..91bde30c --- /dev/null +++ b/test/Subaccount/Fixtures/Responses/patch-success.json @@ -0,0 +1,5 @@ +{ + "suspended": true, + "use_primary_account_balance": false, + "name": "Subaccount department B" +} \ No newline at end of file diff --git a/test/Subaccount/Request/NumberTransferRequestTest.php b/test/Subaccount/Request/NumberTransferRequestTest.php new file mode 100644 index 00000000..077741cb --- /dev/null +++ b/test/Subaccount/Request/NumberTransferRequestTest.php @@ -0,0 +1,63 @@ +assertEquals($apiKey, $request->getApiKey()); + $this->assertEquals($from, $request->getFrom()); + $this->assertEquals($to, $request->getTo()); + $this->assertEquals($number, $request->getNumber()); + $this->assertEquals($country, $request->getCountry()); + + $newFrom = '0987654321'; + $newTo = '1234567890'; + $newNumber = '1234567890'; + $newCountry = 'GB'; + + $request->setFrom($newFrom); + $request->setTo($newTo); + $request->setNumber($newNumber); + $request->setCountry($newCountry); + + $this->assertEquals($newFrom, $request->getFrom()); + $this->assertEquals($newTo, $request->getTo()); + $this->assertEquals($newNumber, $request->getNumber()); + $this->assertEquals($newCountry, $request->getCountry()); + } + + public function testArrayHydration(): void + { + $data = [ + 'from' => '1234567890', + 'to' => '0987654321', + 'number' => '9876543210', + 'country' => 'US', + ]; + + $request = new NumberTransferRequest('', '', '', '', ''); + $request->fromArray($data); + + $this->assertEquals($data['from'], $request->getFrom()); + $this->assertEquals($data['to'], $request->getTo()); + $this->assertEquals($data['number'], $request->getNumber()); + $this->assertEquals($data['country'], $request->getCountry()); + + $arrayData = $request->toArray(); + + $this->assertEquals($data, $arrayData); + } +} diff --git a/test/Subaccount/SubaccountObjects/AccountTest.php b/test/Subaccount/SubaccountObjects/AccountTest.php new file mode 100644 index 00000000..79a00022 --- /dev/null +++ b/test/Subaccount/SubaccountObjects/AccountTest.php @@ -0,0 +1,64 @@ +setApiKey('abc123') + ->setName('John Doe') + ->setPrimaryAccountApiKey('def456') + ->setUsePrimaryAccountBalance(true) + ->setCreatedAt('2023-06-26T10:23:59Z') + ->setSuspended(false) + ->setBalance(100.0) + ->setCreditLimit(500.0); + + $expectedArray = [ + 'api_key' => 'abc123', + 'name' => 'John Doe', + 'primary_account_api_key' => 'def456', + 'use_primary_account_balance' => true, + 'created_at' => '2023-06-26T10:23:59Z', + 'suspended' => false, + 'balance' => 100.0, + 'credit_limit' => 500.0 + ]; + + $this->assertEquals($expectedArray, $instance->toArray()); + } + + public function testGettersAndSetters(): void + { + $instance = new Account(); + + $instance->setApiKey('abc123'); + $this->assertEquals('abc123', $instance->getApiKey()); + + $instance->setName('John Doe'); + $this->assertEquals('John Doe', $instance->getName()); + + $instance->setPrimaryAccountApiKey('def456'); + $this->assertEquals('def456', $instance->getPrimaryAccountApiKey()); + + $instance->setUsePrimaryAccountBalance(true); + $this->assertTrue($instance->getUsePrimaryAccountBalance()); + + $instance->setCreatedAt('2023-06-26T10:23:59Z'); + $this->assertEquals('2023-06-26T10:23:59Z', $instance->getCreatedAt()); + + $instance->setSuspended(false); + $this->assertFalse($instance->getSuspended()); + + $instance->setBalance(100.0); + $this->assertEquals(100.0, $instance->getBalance()); + + $instance->setCreditLimit(500.0); + $this->assertEquals(500.0, $instance->getCreditLimit()); + } +}