diff --git a/README.md b/README.md index 40966e1..55abd69 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,62 @@ if ($webhook->status === \VerifyMyContent\SDK\IdentityVerification\IdentityVerif } ``` +### Start a Re-Identification + +Use the `createReIdentification` of the `VerifyMyContent\SDK\ReIdentification\ReIdentificationClient` abstraction inside `VerifyMyContent\VerifyMyContent` passing a `VerifyMyContent\SDK\ReIdentification\Entity\Requests\CreateReIdentificationRequest` and receiving a `VerifyMyContent\SDK\ReIdentification\Entity\Responses\CreateReIdentificationResponse`. + +```php +useSandbox(); + +$response = $vmc->reIdentification()->createReIdentification( + new \VerifyMyContent\SDK\ReIdentification\Entity\Requests\CreateReIdentificationRequest([ + "customer" => [ + "id" => "YOUR-CUSTOMER-UNIQUE-ID", + "email" => "person@example.com", + ], + "redirect_uri" => "https://example.com/callback", + "webhook" => "https://example.com/webhook", + ]) +); + +// save $response->id if you want to track this re-identification + +// redirect user to re-identify +header("Location: {$response->redirect_uri}"); +``` + +### Retrieve Re-Identification by ID + +```php +useSandbox(); + +$response = $vmc->reIdentification()->getReIdentification("YOUR-RE-IDENTIFICATION-ID"); + +// Printing current status +echo "Status: {$response->status}"; +``` + +### Receive a Re-Identification Webhook + +```php +status} received from re-identification {$webhook->id}"; +``` + ### Create a Static Content Moderation Use the `createStaticContentModeration` of the `VerifyMyContent\SDK\ContentModeration\ContentModerationClient` abstraction inside `VerifyMyContent\VerifyMyContent` passing an `VerifyMyContent\SDK\ContentModeration\Entity\Requests\CreateStaticContentModerationRequest` and receiving an `VerifyMyContent\SDK\ContentModeration\Entity\Responses\CreateStaticContentModerationResponse`. diff --git a/examples/re-identification/create-re-identification.php b/examples/re-identification/create-re-identification.php new file mode 100644 index 0000000..28211a6 --- /dev/null +++ b/examples/re-identification/create-re-identification.php @@ -0,0 +1,41 @@ +useSandbox(); + +// Create Re-Identification +$request = new CreateReIdentificationRequest([ + "customer" => [ + "id" => "example-php", + "email" => "example-php@verifymycontent.com", + ], + "redirect_uri" => "https://example.com/callback", + "webhook" => "https://example.com/webhook", +]); +$response = $vmc->reIdentification()->createReIdentification($request); + +echo "To re-identify, please access: {$response->redirect_uri}\n"; + +// Wait until verification is approved +while (true) { + echo "Checking status..."; + $response = $vmc->reIdentification()->getReIdentification($response->id); + if ($response->status == ReIdentificationStatus::APPROVED) { + echo "\n\nVerification {$response->id} for user {$response->customer->id} is approved!\n"; + break; + } + + echo " " . $response->status . "\n"; + sleep(5); +} diff --git a/examples/re-identification/receiving-webhook.php b/examples/re-identification/receiving-webhook.php new file mode 100644 index 0000000..e58d9e8 --- /dev/null +++ b/examples/re-identification/receiving-webhook.php @@ -0,0 +1,36 @@ +toArray()]))); + http_response_code(204); + exit; +} + +catch (InvalidArgumentException $e) { + http_response_code(400); + echo "Invalid request: " . $e->getMessage(); + exit; +} +catch (Exception $e) { + http_response_code(400); + echo "Invalid JSON"; + exit; +} diff --git a/src/ContentModeration/ContentModerationClient.php b/src/ContentModeration/ContentModerationClient.php index 48025a7..76a01f9 100644 --- a/src/ContentModeration/ContentModerationClient.php +++ b/src/ContentModeration/ContentModerationClient.php @@ -46,7 +46,7 @@ public function getStaticContentModeration(string $id): GetStaticContentModerati * @return void * @throws InvalidStatusCodeException */ - public function startLiveContentModeration(string $id, StartLiveContentModerationRequest $request = null): void; + public function startLiveContentModeration(string $id, ?StartLiveContentModerationRequest $request = null): void; /** * @param CreateLiveContentModerationRequest $request diff --git a/src/ContentModeration/ContentModerationClientV1.php b/src/ContentModeration/ContentModerationClientV1.php index 43038f7..a06f748 100644 --- a/src/ContentModeration/ContentModerationClientV1.php +++ b/src/ContentModeration/ContentModerationClientV1.php @@ -108,7 +108,7 @@ public function getStaticContentModeration(string $id): GetStaticContentModerati * @return void * @throws InvalidStatusCodeException */ - public function startLiveContentModeration(string $id, StartLiveContentModerationRequest $request = null): void + public function startLiveContentModeration(string $id, ?StartLiveContentModerationRequest $request = null): void { $uri = sprintf(self::ENDPOINT_START_LIVE_CONTENT_MODERATION, $id); $req = null; diff --git a/src/ReIdentification/Entity/Customer.php b/src/ReIdentification/Entity/Customer.php new file mode 100644 index 0000000..adf64a5 --- /dev/null +++ b/src/ReIdentification/Entity/Customer.php @@ -0,0 +1,28 @@ + [ + RequiredValidator::class, + StringValidator::class, + ], + 'email' => [ + StringValidator::class, + ], + ]; +} diff --git a/src/ReIdentification/Entity/Requests/CreateReIdentificationRequest.php b/src/ReIdentification/Entity/Requests/CreateReIdentificationRequest.php new file mode 100644 index 0000000..ded1572 --- /dev/null +++ b/src/ReIdentification/Entity/Requests/CreateReIdentificationRequest.php @@ -0,0 +1,34 @@ + [ + RequiredValidator::class, + ArrayValidator::class, + ], + 'redirect_uri' => UrlValidator::class, + 'webhook' => UrlValidator::class, + ]; + + protected $casts = [ + 'customer' => Customer::class + ]; +} diff --git a/src/ReIdentification/Entity/Requests/WebhookReIdentificationRequest.php b/src/ReIdentification/Entity/Requests/WebhookReIdentificationRequest.php new file mode 100644 index 0000000..29d9fc2 --- /dev/null +++ b/src/ReIdentification/Entity/Requests/WebhookReIdentificationRequest.php @@ -0,0 +1,34 @@ + [ + RequiredValidator::class, + StringValidator::class, + ], + 'customer_id' => [ + RequiredValidator::class, + StringValidator::class, + ], + 'status' => [ + RequiredValidator::class, + StringValidator::class, + ], + ]; +} diff --git a/src/ReIdentification/Entity/Responses/CreateReIdentificationResponse.php b/src/ReIdentification/Entity/Responses/CreateReIdentificationResponse.php new file mode 100644 index 0000000..35ea6a3 --- /dev/null +++ b/src/ReIdentification/Entity/Responses/CreateReIdentificationResponse.php @@ -0,0 +1,40 @@ + [ + RequiredValidator::class, + StringValidator::class, + ], + 'customer' => [ + RequiredValidator::class, + ArrayValidator::class, + ], + 'redirect_uri' => UrlValidator::class, + 'webhook' => UrlValidator::class, + ]; + + protected $casts = [ + 'customer' => Customer::class + ]; +} diff --git a/src/ReIdentification/Entity/Responses/GetReIdentificationResponse.php b/src/ReIdentification/Entity/Responses/GetReIdentificationResponse.php new file mode 100644 index 0000000..3b8afef --- /dev/null +++ b/src/ReIdentification/Entity/Responses/GetReIdentificationResponse.php @@ -0,0 +1,45 @@ + [ + RequiredValidator::class, + StringValidator::class, + ], + 'customer' => [ + RequiredValidator::class, + ArrayValidator::class, + ], + 'redirect_uri' => UrlValidator::class, + 'webhook' => UrlValidator::class, + 'status' => [ + RequiredValidator::class, + StringValidator::class, + ], + ]; + + protected $casts = [ + 'customer' => Customer::class, + ]; +} diff --git a/src/ReIdentification/Exception/FeatureNotEnabledException.php b/src/ReIdentification/Exception/FeatureNotEnabledException.php new file mode 100644 index 0000000..c6935f1 --- /dev/null +++ b/src/ReIdentification/Exception/FeatureNotEnabledException.php @@ -0,0 +1,13 @@ + ReIdentificationClientV1::class, + ]; + + const PRODUCTION_URL = 'https://oauth.verifymycontent.com'; + const SANDBOX_URL = 'https://oauth.sandbox.verifymycontent.com'; + + public function createReIdentification(CreateReIdentificationRequest $request): CreateReIdentificationResponse; + + public function getReIdentification(string $id): GetReIdentificationResponse; + + public function __construct(HMAC $hmac); +} diff --git a/src/ReIdentification/ReIdentificationClientV1.php b/src/ReIdentification/ReIdentificationClientV1.php new file mode 100644 index 0000000..0e135b9 --- /dev/null +++ b/src/ReIdentification/ReIdentificationClientV1.php @@ -0,0 +1,103 @@ +hmac = $hmac; + $this->transport = new HTTP(ReIdentificationClient::PRODUCTION_URL); + } + + /** + * @throws InvalidStatusCodeException + * @throws FeatureNotEnabledException + * @throws NoApprovedVerificationFoundException + */ + public function createReIdentification(CreateReIdentificationRequest $request): CreateReIdentificationResponse + { + $response = $this->transport->post( + self::ENDPOINT_CREATE_RE_IDENTIFICATION, + $request->toArray(), + [ + 'Authorization' => $this->sign($request->toArray()), + ], + [200, 201, 403, 404] + ); + + if ($response->getStatusCode() === 403) { + throw new FeatureNotEnabledException(); + } + + if ($response->getStatusCode() === 404) { + throw new NoApprovedVerificationFoundException(); + } + + $data = json_decode($response->getBody()->getContents(), true); + return new CreateReIdentificationResponse($data); + } + + /** + * @throws InvalidStatusCodeException + */ + public function getReIdentification(string $id): GetReIdentificationResponse + { + $uri = sprintf(self::ENDPOINT_GET_RE_IDENTIFICATION, $id); + $response = $this->transport->get( + $uri, + [ + 'Authorization' => $this->sign($uri), + ], + [200] + ); + + $data = json_decode($response->getBody()->getContents(), true); + return new GetReIdentificationResponse($data); + } + + public function useSandbox(): void + { + $this->setBaseURL(ReIdentificationClient::SANDBOX_URL); + } + + public function setBaseURL(string $baseURL): void + { + $this->transport->setBaseUrl($baseURL); + } + + private function sign($input): string + { + return sprintf("hmac %s", $this->hmac->generate($input)); + } + + /** + * @param HTTP $transport + */ + public function setTransport(HTTP $transport): void + { + $this->transport = $transport; + } +} diff --git a/src/ReIdentification/ReIdentificationStatus.php b/src/ReIdentification/ReIdentificationStatus.php new file mode 100644 index 0000000..5fb7b4a --- /dev/null +++ b/src/ReIdentification/ReIdentificationStatus.php @@ -0,0 +1,12 @@ +complaintClient = new $consentComplaintClientClassName($this->hmac); + $reIdentificationClientClassName = ReIdentificationClient::API_VERSIONS[ReIdentificationClient::API_VERSION_V1]; + $this->reIdentificationClient = new $reIdentificationClientClassName($this->hmac); + $this->verifyMy = new VerifyMy(IdentityVerificationClient::PRODUCTION_URL, $apiKey, $apiSecret); } @@ -105,6 +114,23 @@ public function complaint(): ComplaintClient return $this->complaintClient; } + /** + * @return ReIdentificationClient + */ + public function reIdentification(): ReIdentificationClient + { + return $this->reIdentificationClient; + } + + /** + * @param string|ReIdentificationClient $client + * @return void + */ + public function setReIdentificationClient($client): void + { + $this->setClient($client, 'reIdentificationClient', ReIdentificationClient::class, ReIdentificationClient::API_VERSIONS); + } + private function setClient($client, $clientAttribute, $clientClass, $clientVersions): void { @@ -178,6 +204,7 @@ public function useSandbox(): void $this->identityVerificationClient->useSandbox(); $this->contentModerationClient->useSandbox(); $this->complaintClient->useSandbox(); + $this->reIdentificationClient->useSandbox(); $this->verifyMy = new VerifyMy(IdentityVerificationClient::SANDBOX_URL, $this->apiKey, $this->apiSecret); } } diff --git a/src/VerifyMyContentInterface.php b/src/VerifyMyContentInterface.php index e1350eb..946a3fe 100644 --- a/src/VerifyMyContentInterface.php +++ b/src/VerifyMyContentInterface.php @@ -6,6 +6,7 @@ use VerifyMyContent\SDK\ContentModeration\ContentModerationClient; use VerifyMyContent\SDK\Core\ExportableClient; use VerifyMyContent\SDK\IdentityVerification\IdentityVerificationClient; +use VerifyMyContent\SDK\ReIdentification\ReIdentificationClient; interface VerifyMyContentInterface extends ExportableClient { @@ -29,6 +30,11 @@ public function contentModeration(): ContentModerationClient; public function complaint(): ComplaintClient; + /** + * @return ReIdentificationClient + */ + public function reIdentification(): ReIdentificationClient; + /** * @param string|IdentityVerificationClient $client * @return void @@ -46,4 +52,10 @@ public function setContentModerationClient($client): void; * @return void */ public function setComplaintClient($client): void; + + /** + * @param string|ReIdentificationClient $client + * @return void + */ + public function setReIdentificationClient($client): void; } diff --git a/tests/ReIdentification/ReIdentificationClientV1Test.php b/tests/ReIdentification/ReIdentificationClientV1Test.php new file mode 100644 index 0000000..b44db88 --- /dev/null +++ b/tests/ReIdentification/ReIdentificationClientV1Test.php @@ -0,0 +1,330 @@ +createReIdentificationInput(); + $transportMock = $this->createMock(HTTP::class); + $transportMock->expects($this->once()) + ->method('post') + ->with( + $this->equalTo(ReIdentificationClientV1::ENDPOINT_CREATE_RE_IDENTIFICATION), + $this->equalTo($input), + $this->equalTo($this->authorizationHeaders($input)), + $this->equalTo([200, 201, 403, 404]) + ) + ->willReturn($this->createConfiguredMock(ResponseInterface::class, [ + 'getBody' => $this->createConfiguredMock( + StreamInterface::class, [ + 'getContents' => json_encode(array_merge($input, [ + 'id' => 're-identification-id', + ])), + ]), + 'getStatusCode' => 201, + ])); + + $client = new ReIdentificationClientV1($this->hmac); + $client->setTransport($transportMock); + + $response = $client->createReIdentification( + new CreateReIdentificationRequest($input) + ); + + $this->assertEquals($response->toArray(), array_merge($input, [ + 'id' => 're-identification-id', + ])); + } + + public function testCreateReIdentificationIfTransportThrowsException() + { + $this->expectException(InvalidStatusCodeException::class); + $this->expectExceptionMessage('Invalid status code: 500'); + $input = $this->createReIdentificationInput(); + $transportMock = $this->createMock(HTTP::class); + $transportMock->expects($this->once()) + ->method('post') + ->with( + $this->equalTo(ReIdentificationClientV1::ENDPOINT_CREATE_RE_IDENTIFICATION), + $this->equalTo($input), + $this->equalTo($this->authorizationHeaders($input)), + $this->equalTo([200, 201, 403, 404]) + ) + ->willThrowException(new InvalidStatusCodeException(500)); + + $client = new ReIdentificationClientV1($this->hmac); + $client->setTransport($transportMock); + + $client->createReIdentification( + new CreateReIdentificationRequest($input) + ); + } + + public function testCreateReIdentificationIfDtoParserOfCreateReIdentificationResponseThrows() + { + $input = $this->createReIdentificationInput(); + $this->expectException(ValidationException::class); + $this->expectExceptionMessage("id is required"); + $transportMock = $this->createMock(HTTP::class); + $transportMock->expects($this->once()) + ->method('post') + ->with( + $this->equalTo(ReIdentificationClientV1::ENDPOINT_CREATE_RE_IDENTIFICATION), + $this->equalTo($input), + $this->equalTo($this->authorizationHeaders($input)), + $this->equalTo([200, 201, 403, 404]) + ) + ->willReturn($this->createConfiguredMock(ResponseInterface::class, [ + 'getBody' => $this->createConfiguredMock( + StreamInterface::class, [ + 'getContents' => json_encode(array_merge($input, [ + 'not-id' => 're-identification-id', + ])), + ]), + 'getStatusCode' => 201, + ])); + + $client = new ReIdentificationClientV1($this->hmac); + $client->setTransport($transportMock); + + $client->createReIdentification( + new CreateReIdentificationRequest($input) + ); + } + + public function testCreateReIdentificationThrowsFeatureNotEnabledException() + { + $this->expectException(FeatureNotEnabledException::class); + $this->expectExceptionMessage('re-identification feature not enabled'); + $input = $this->createReIdentificationInput(); + $transportMock = $this->createMock(HTTP::class); + $transportMock->expects($this->once()) + ->method('post') + ->with( + $this->equalTo(ReIdentificationClientV1::ENDPOINT_CREATE_RE_IDENTIFICATION), + $this->equalTo($input), + $this->equalTo($this->authorizationHeaders($input)), + $this->equalTo([200, 201, 403, 404]) + ) + ->willReturn($this->createConfiguredMock(ResponseInterface::class, [ + 'getBody' => $this->createConfiguredMock(StreamInterface::class, [ + 'getContents' => json_encode([ + 'message' => 're-identification feature not enabled', + 'status_code' => 403, + ]), + ]), + 'getStatusCode' => 403, + ])); + + $client = new ReIdentificationClientV1($this->hmac); + $client->setTransport($transportMock); + + $client->createReIdentification(new CreateReIdentificationRequest($input)); + } + + public function testCreateReIdentificationThrowsNoApprovedVerificationFoundException() + { + $this->expectException(NoApprovedVerificationFoundException::class); + $this->expectExceptionMessage('no approved verification found for customer'); + $input = $this->createReIdentificationInput(); + $transportMock = $this->createMock(HTTP::class); + $transportMock->expects($this->once()) + ->method('post') + ->with( + $this->equalTo(ReIdentificationClientV1::ENDPOINT_CREATE_RE_IDENTIFICATION), + $this->equalTo($input), + $this->equalTo($this->authorizationHeaders($input)), + $this->equalTo([200, 201, 403, 404]) + ) + ->willReturn($this->createConfiguredMock(ResponseInterface::class, [ + 'getBody' => $this->createConfiguredMock(StreamInterface::class, [ + 'getContents' => json_encode([ + 'message' => 'no approved verification found for customer', + 'status_code' => 404, + ]), + ]), + 'getStatusCode' => 404, + ])); + + $client = new ReIdentificationClientV1($this->hmac); + $client->setTransport($transportMock); + + $client->createReIdentification(new CreateReIdentificationRequest($input)); + } + + public function testGetReIdentification() + { + $output = $this->getReIdentificationOutput(); + $uri = sprintf( + ReIdentificationClientV1::ENDPOINT_GET_RE_IDENTIFICATION, + $output["id"] + ); + + $transportMock = $this->createMock(HTTP::class); + $transportMock->expects($this->once()) + ->method('get') + ->with( + $this->equalTo($uri), + $this->equalTo($this->authorizationHeaders($uri)), + $this->equalTo([200]) + ) + ->willReturn($this->createConfiguredMock(ResponseInterface::class, [ + 'getBody' => $this->createConfiguredMock(StreamInterface::class, [ + 'getContents' => json_encode($output), + ]), + 'getStatusCode' => 200, + ])); + + $client = new ReIdentificationClientV1($this->hmac); + $client->setTransport($transportMock); + + $response = $client->getReIdentification($output["id"]); + + $this->assertEquals($response->id, $output["id"]); + $this->assertEquals($response->status, $output["status"]); + $this->assertEquals($response->customer->id, $output["customer"]["id"]); + $this->assertEquals($response->customer->email, $output["customer"]["email"]); + $this->assertEquals($response->redirect_uri, $output["redirect_uri"]); + $this->assertEquals($response->webhook, $output["webhook"]); + } + + public function testGetReIdentificationIfTransportThrowsException() + { + $this->expectException(InvalidStatusCodeException::class); + $this->expectExceptionMessage('Invalid status code: 500'); + $output = $this->getReIdentificationOutput(); + $uri = sprintf( + ReIdentificationClientV1::ENDPOINT_GET_RE_IDENTIFICATION, + $output["id"] + ); + + $transportMock = $this->createMock(HTTP::class); + $transportMock->expects($this->once()) + ->method('get') + ->with( + $this->equalTo($uri), + $this->equalTo($this->authorizationHeaders($uri)), + $this->equalTo([200]) + ) + ->willThrowException(new InvalidStatusCodeException(500)); + + $client = new ReIdentificationClientV1($this->hmac); + $client->setTransport($transportMock); + + $client->getReIdentification($output["id"]); + } + + public function testGetReIdentificationIfDtoParserThrows() + { + $output = $this->getReIdentificationOutput(); + $id = $output["id"]; + unset($output["id"]); + + $this->expectException(ValidationException::class); + $this->expectExceptionMessage("id is required"); + $uri = sprintf( + ReIdentificationClientV1::ENDPOINT_GET_RE_IDENTIFICATION, + $id + ); + + $transportMock = $this->createMock(HTTP::class); + $transportMock->expects($this->once()) + ->method('get') + ->with( + $this->equalTo($uri), + $this->equalTo($this->authorizationHeaders($uri)), + $this->equalTo([200]) + ) + ->willReturn($this->createConfiguredMock(ResponseInterface::class, [ + 'getBody' => $this->createConfiguredMock(StreamInterface::class, [ + 'getContents' => json_encode(array_merge($output, [ + 'not-id' => 're-identification-id', + ])), + ]), + 'getStatusCode' => 200, + ])); + + $client = new ReIdentificationClientV1($this->hmac); + $client->setTransport($transportMock); + + $client->getReIdentification($id); + } + + public function testSetBaseURL() + { + $transportMock = $this->createMock(HTTP::class); + $transportMock->expects($this->once()) + ->method('setBaseURL') + ->with($this->equalTo("https://example-base-url.com")); + + $client = new ReIdentificationClientV1($this->hmac); + $client->setTransport($transportMock); + $client->setBaseURL("https://example-base-url.com"); + } + + public function testUseSandbox() + { + $transportMock = $this->createMock(HTTP::class); + $transportMock->expects($this->once()) + ->method('setBaseURL') + ->with($this->equalTo(ReIdentificationClient::SANDBOX_URL)); + + $client = new ReIdentificationClientV1($this->hmac); + $client->setTransport($transportMock); + $client->useSandbox(); + } + + private function createReIdentificationInput(): array + { + return [ + "customer" => [ + "id" => "customer-id", + "email" => "customer-email@mock.com", + ], + "redirect_uri" => "https://redirect-uri.com", + "webhook" => "https://webhook-uri.com", + ]; + } + + private function getReIdentificationOutput(): array + { + return array_merge( + $this->createReIdentificationInput(), + [ + "id" => "re-identification-id", + "status" => "pending", + ] + ); + } + + private function authorizationHeaders($input): array + { + return [ + "Authorization" => sprintf("hmac %s", $this->hmac->generate($input)), + ]; + } + + protected function setUp(): void + { + parent::setUp(); + $this->hmac = new HMAC("api-key", "api-secret"); + } +}