diff --git a/README.md b/README.md index bf91caa2..1ccd3409 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 @@ -228,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. @@ -245,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: @@ -255,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: @@ -269,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: @@ -283,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. @@ -297,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 @@ -348,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 @@ -363,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 @@ -383,8 +398,16 @@ $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 +#### 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: @@ -884,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/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", diff --git a/phpunit.xml.dist b/phpunit.xml.dist index d75d1610..aa73cabd 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -25,6 +25,17 @@ test/ProactiveConnect + + test/Voice + + + test/Messages + + + 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/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/Entity/IterableAPICollection.php b/src/Entity/IterableAPICollection.php index 5d390050..016ad4ed 100644 --- a/src/Entity/IterableAPICollection.php +++ b/src/Entity/IterableAPICollection.php @@ -99,6 +99,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. */ @@ -477,6 +483,8 @@ protected function fetchPage($absoluteUri): void { //use filter if no query provided if (false === strpos($absoluteUri, '?')) { + $originalUri = $absoluteUri; + $query = []; if (isset($this->size)) { @@ -492,6 +500,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; @@ -588,4 +601,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/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/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/src/Verify2/Client.php b/src/Verify2/Client.php index c73467d0..e8e9c1cc 100644 --- a/src/Verify2/Client.php +++ b/src/Verify2/Client.php @@ -26,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) { @@ -38,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/src/Verify2/Request/BaseVerifyRequest.php b/src/Verify2/Request/BaseVerifyRequest.php index a07eb584..c74aaa8d 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 = null; + protected ?string $clientRef = null; protected int $length = 4; @@ -132,6 +134,18 @@ public function addWorkflow(VerificationWorkflow $verificationWorkflow): static return $this; } + public function getFraudCheck(): ?bool + { + return $this->fraudCheck ?? null; + } + + public function setFraudCheck(bool $fraudCheck): BaseVerifyRequest + { + $this->fraudCheck = $fraudCheck; + + return $this; + } + public function getBaseVerifyUniversalOutputArray(): array { $returnArray = [ @@ -142,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/src/Voice/Client.php b/src/Voice/Client.php index d131be8e..050443d3 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()) { @@ -270,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/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..75ac6d7f --- /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 [ + 'behavior' => $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/SMS/Message/SMSTest.php b/test/SMS/Message/SMSTest.php index b5bce360..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', true], - ['日本語でボナージュ', 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; } } 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()); + } +} diff --git a/test/Verify2/ClientTest.php b/test/Verify2/ClientTest.php index 2b0b51f2..71f286f1 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,11 +93,12 @@ 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 ); $this->assertRequestJsonBodyContains('locale', 'en-us', $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); @@ -115,6 +116,28 @@ public function testCanRequestSMS(): void $this->assertArrayHasKey('request_id', $result); } + public function testCanBypassFraudCheck(): void + { + $payload = [ + 'to' => '07785254785', + 'brand' => 'my-brand', + ]; + + $smsVerification = new SMSRequest($payload['to'], $payload['brand']); + $smsVerification->setFraudCheck(false); + + $this->vonageClient->send(Argument::that(function (Request $request) { + $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 */ @@ -591,6 +614,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 diff --git a/test/Voice/ClientTest.php b/test/Voice/ClientTest.php index 5899a306..e481fdcc 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 = [ @@ -603,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( @@ -614,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; 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 + ); + } }