Skip to content

Commit 3cd8ddc

Browse files
Added support for the PSD2 endpoint (#240)
* Added support for the PSD2 endpoint * Move to Vonage namespaces for new PSD2 feature * Add PSD2 feature to README docs * Update tests to use renamed client Co-authored-by: Lorna Jane Mitchell <[email protected]>
1 parent 6a0c524 commit 3cd8ddc

File tree

4 files changed

+307
-2
lines changed

4 files changed

+307
-2
lines changed

README.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,14 +164,16 @@ Using your signature secret and the other supplied parameters, the signature can
164164
Vonage's [Verify API][doc_verify] makes it easy to prove that a user has provided their own phone number during signup,
165165
or implement second factor authentication during signin.
166166

167-
You can start a verification process using a simple array:
167+
You can start a verification process using code like this:
168168

169169
```php
170170
$request = new \Vonage\Verify\Request('14845551212', 'My App');
171171
$response = $client->verify()->start($request);
172172
echo "Started verification with an id of: " . $response->getRequestId();
173173
```
174174

175+
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.
176+
175177
### Controlling a Verification
176178

177179
To cancel an in-progress verification, or to trigger the next attempt to send the confirmation code, you can pass
@@ -210,6 +212,20 @@ foreach($verification->getChecks() as $check){
210212
}
211213
```
212214

215+
### Payment Verification
216+
217+
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.
218+
219+
Start the verification for a payment like this:
220+
221+
```php
222+
$request = new \Vonage\Verify\RequestPSD2('14845551212', 'My App');
223+
$response = $client->verify()->requestPSD2($request);
224+
echo "Started verification with an id of: " . $response['request_id'];
225+
```
226+
227+
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.
228+
213229
### Making a Call
214230

215231
All `$client->voice()` methods require the client to be constructed with a `Vonage\Client\Credentials\Keypair`, or a

src/Verify/Client.php

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,19 @@ public function start($verification) : Verification
7878
return $this->checkError($verification, $response);
7979
}
8080

81+
/**
82+
* @return array{request_id: string, status: string}
83+
*/
84+
public function requestPSD2(RequestPSD2 $request) : array
85+
{
86+
$api = $this->getApiResource();
87+
$response = $api->create($request->toArray(), '/psd2/json');
88+
89+
$this->checkError($request, $response);
90+
91+
return $response;
92+
}
93+
8194
/**
8295
* @param string|Verification $verification
8396
*/
@@ -224,7 +237,7 @@ protected function control($verification, string $cmd) : Verification
224237
return $this->checkError($verification, $data);
225238
}
226239

227-
protected function checkError(Verification $verification, $data)
240+
protected function checkError($verification, $data)
228241
{
229242
if (!isset($data['status'])) {
230243
$e = new Exception\Request('unexpected response from API');

src/Verify/RequestPSD2.php

Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Vonage\Verify;
5+
6+
use Vonage\Entity\Hydrator\ArrayHydrateInterface;
7+
8+
class RequestPSD2 implements ArrayHydrateInterface
9+
{
10+
const PIN_LENGTH_4 = 4;
11+
const PIN_LENGTH_6 = 6;
12+
13+
const WORKFLOW_SMS_TTS_TSS = 1;
14+
const WORKFLOW_SMS_SMS_TSS = 2;
15+
const WORKFLOW_TTS_TSS = 3;
16+
const WORKFLOW_SMS_SMS = 4;
17+
const WORKFLOW_SMS_TTS = 5;
18+
const WORKFLOW_SMS = 6;
19+
const WORKFLOW_TTS = 7;
20+
21+
/**
22+
* @var string
23+
*/
24+
protected $number;
25+
26+
/**
27+
* @var string
28+
*/
29+
protected $country;
30+
31+
/**
32+
* @var string
33+
*/
34+
protected $payee;
35+
36+
/**
37+
* @var string
38+
*/
39+
protected $amount;
40+
41+
/**
42+
* @var int
43+
*/
44+
protected $codeLength;
45+
46+
/**
47+
* @var string
48+
*/
49+
protected $locale;
50+
51+
/**
52+
* @var int
53+
*/
54+
protected $pinExpiry;
55+
56+
/**
57+
* @var int
58+
*/
59+
protected $nextEventWait;
60+
61+
/**
62+
* @var int
63+
*/
64+
protected $workflowId;
65+
66+
public function __construct(string $number, string $payee, string $amount, int $workflowId = null)
67+
{
68+
$this->number = $number;
69+
$this->payee = $payee;
70+
$this->amount = $amount;
71+
72+
if ($workflowId) {
73+
$this->setWorkflowId($workflowId);
74+
}
75+
}
76+
77+
public function getCountry() : ?string
78+
{
79+
return $this->country;
80+
}
81+
82+
public function setCountry(string $country) : self
83+
{
84+
if (strlen($country) !== 2) {
85+
throw new \InvalidArgumentException('Country must be in two character format');
86+
}
87+
$this->country = $country;
88+
return $this;
89+
}
90+
91+
public function getCodeLength() : ?int
92+
{
93+
return $this->codeLength;
94+
}
95+
96+
public function setCodeLength(int $codeLength) : self
97+
{
98+
if ($codeLength !== 4 || $codeLength !== 6) {
99+
throw new \InvalidArgumentException('Pin length must be either 4 or 6 digits');
100+
}
101+
102+
$this->codeLength = $codeLength;
103+
return $this;
104+
}
105+
106+
public function getLocale() : ?string
107+
{
108+
return $this->locale;
109+
}
110+
111+
public function setLocale(string $locale) : self
112+
{
113+
$this->locale = $locale;
114+
return $this;
115+
}
116+
117+
public function getPinExpiry() : ?int
118+
{
119+
return $this->pinExpiry;
120+
}
121+
122+
public function setPinExpiry(int $pinExpiry) : self
123+
{
124+
if ($pinExpiry < 60 || $pinExpiry > 3600) {
125+
throw new \InvalidArgumentException('Pin expiration must be between 60 and 3600 seconds');
126+
}
127+
128+
$this->pinExpiry = $pinExpiry;
129+
return $this;
130+
}
131+
132+
public function getNextEventWait() : ?int
133+
{
134+
return $this->nextEventWait;
135+
}
136+
137+
public function setNextEventWait(int $nextEventWait) : self
138+
{
139+
if ($nextEventWait < 60 || $nextEventWait > 3600) {
140+
throw new \InvalidArgumentException('Next Event time must be between 60 and 900 seconds');
141+
}
142+
143+
$this->nextEventWait = $nextEventWait;
144+
return $this;
145+
}
146+
147+
public function getWorkflowId() : ?int
148+
{
149+
return $this->workflowId;
150+
}
151+
152+
public function setWorkflowId(int $workflowId) : self
153+
{
154+
if ($workflowId < 1 || $workflowId > 7) {
155+
throw new \InvalidArgumentException('Workflow ID must be from 1 to 7');
156+
}
157+
158+
$this->workflowId = $workflowId;
159+
return $this;
160+
}
161+
162+
public function getNumber() : string
163+
{
164+
return $this->number;
165+
}
166+
167+
public function getPayee() : string
168+
{
169+
return $this->payee;
170+
}
171+
172+
public function getAmount() : string
173+
{
174+
return $this->amount;
175+
}
176+
177+
public function fromArray(array $data)
178+
{
179+
if (array_key_exists('code_length', $data)) {
180+
$this->setCodeLength($data['code_length']);
181+
}
182+
183+
if (array_key_exists('pin_expiry', $data)) {
184+
$this->setPinExpiry($data['pin_expiry']);
185+
}
186+
187+
if (array_key_exists('next_event_wait', $data)) {
188+
$this->setNextEventWait($data['next_event_wait']);
189+
}
190+
191+
if (array_key_exists('workflow_id', $data)) {
192+
$this->setWorkflowId($data['workflow_id']);
193+
}
194+
195+
if (array_key_exists('country', $data)) {
196+
$this->setCountry($data['country']);
197+
}
198+
199+
if (array_key_exists('lg', $data)) {
200+
$this->setLocale($data['lg']);
201+
}
202+
}
203+
204+
public function toArray(): array
205+
{
206+
$data = [
207+
'number' => $this->getNumber(),
208+
'amount' => $this->getAmount(),
209+
'payee' => $this->getPayee(),
210+
];
211+
212+
if ($this->getCodeLength()) {
213+
$data['code_length'] = $this->getCodeLength();
214+
}
215+
216+
if ($this->getPinExpiry()) {
217+
$data['pin_expiry'] = $this->getPinExpiry();
218+
}
219+
220+
if ($this->getNextEventWait()) {
221+
$data['next_event_wait'] = $this->getNextEventWait();
222+
}
223+
224+
if ($this->getWorkflowId()) {
225+
$data['workflow_id'] = $this->getWorkflowId();
226+
}
227+
228+
if ($this->getCountry()) {
229+
$data['country'] = $this->getCountry();
230+
}
231+
232+
if ($this->getLocale()) {
233+
$data['lg'] = $this->getLocale();
234+
}
235+
236+
return $data;
237+
}
238+
}

test/Verify/ClientTest.php

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
use Vonage\Verify\Client;
1313
use Vonage\Verify\Request;
14+
use Vonage\Verify\RequestPSD2;
1415
use Vonage\Verify\Verification;
1516
use VonageTest\Psr7AssertionTrait;
1617
use Prophecy\Argument;
@@ -130,6 +131,43 @@ public function testCanStartVerification()
130131
$this->assertSame($success, @$verification->getResponse());
131132
}
132133

134+
public function testCanStartPSD2Verification()
135+
{
136+
$this->vonageClient->send(Argument::that(function (RequestInterface $request) {
137+
$this->assertRequestJsonBodyContains('number', '14845551212', $request);
138+
$this->assertRequestJsonBodyContains('payee', 'Test Verify', $request);
139+
$this->assertRequestJsonBodyContains('amount', '5.25', $request);
140+
$this->assertRequestMatchesUrl('https://api.nexmo.com/verify/psd2/json', $request);
141+
return true;
142+
}))->willReturn($this->getResponse('start'))
143+
->shouldBeCalledTimes(1);
144+
145+
$request = new RequestPSD2('14845551212', 'Test Verify', '5.25');
146+
$response = @$this->client->requestPSD2($request);
147+
148+
$this->assertSame('0', $response['status']);
149+
$this->assertSame('44a5279b27dd4a638d614d265ad57a77', $response['request_id']);
150+
}
151+
152+
public function testCanStartPSD2VerificationWithWorkflowID()
153+
{
154+
$this->vonageClient->send(Argument::that(function (RequestInterface $request) {
155+
$this->assertRequestJsonBodyContains('number', '14845551212', $request);
156+
$this->assertRequestJsonBodyContains('payee', 'Test Verify', $request);
157+
$this->assertRequestJsonBodyContains('amount', '5.25', $request);
158+
$this->assertRequestJsonBodyContains('workflow_id', 5, $request);
159+
$this->assertRequestMatchesUrl('https://api.nexmo.com/verify/psd2/json', $request);
160+
return true;
161+
}))->willReturn($this->getResponse('start'))
162+
->shouldBeCalledTimes(1);
163+
164+
$request = new RequestPSD2('14845551212', 'Test Verify', '5.25', 5);
165+
$response = @$this->client->requestPSD2($request);
166+
167+
$this->assertSame('0', $response['status']);
168+
$this->assertSame('44a5279b27dd4a638d614d265ad57a77', $response['request_id']);
169+
}
170+
133171
public function testCanStartArray()
134172
{
135173
$response = $this->setupClientForStart('start');

0 commit comments

Comments
 (0)