From e8e865515b360c131145196cff6de3d4f47b6be5 Mon Sep 17 00:00:00 2001 From: david ruiz Date: Fri, 18 Sep 2026 13:49:28 +0200 Subject: [PATCH 1/2] Add verification attempt-assets endpoints and card scheduled_activation_date --- lib/Checkout/ApiClient.php | 17 +- .../AddressDocumentVerificationClient.php | 44 ++- .../Entities/AttemptsQueryFilter.php | 24 ++ .../Identities/Entities/ClientInformation.php | 13 +- .../Identities/Entities/DeclaredData.php | 16 +- .../Entities/IdentityDeclaredData.php | 33 ++ .../IdentityVerificationClientInformation.php | 30 ++ .../Identities/Entities/IdvAddress.php | 55 ++++ .../Identities/Entities/PhoneNumber.php | 24 ++ .../FaceAuthenticationClient.php | 11 +- .../FaceAuthenticationAttemptRequest.php | 17 +- .../IdDocumentVerificationClient.php | 44 ++- .../IdentityVerificationClient.php | 11 +- .../IdentityVerificationAndOpenRequest.php | 5 +- .../IdentityVerificationAttemptRequest.php | 19 +- .../Requests/IdentityVerificationRequest.php | 5 +- .../Issuing/Cards/Create/CardRequest.php | 21 +- .../Cards/Update/CardUpdateHeaders.php | 49 +++ .../Cards/Update/UpdateCardRequest.php | 21 +- lib/Checkout/Issuing/IssuingClient.php | 13 +- .../AddressDocumentVerificationClientTest.php | 114 ++++++- ...essDocumentVerificationIntegrationTest.php | 67 ++++ .../FaceAuthenticationClientTest.php | 58 +++- .../FaceAuthenticationIntegrationTest.php | 50 ++- .../IdDocumentVerificationClientTest.php | 138 +++++++- .../IdDocumentVerificationIntegrationTest.php | 79 ++++- .../IdentitiesSerializationTest.php | 308 ++++++++++++++++++ .../IdentityVerificationClientTest.php | 80 ++++- .../IdentityVerificationIntegrationTest.php | 70 +++- ...heduledActivationDateSerializationTest.php | 120 +++++++ .../Issuing/Cards/CardUpdateHeadersTest.php | 192 +++++++++++ .../Issuing/Cards/CardsIntegrationTest.php | 108 ++++++ 32 files changed, 1752 insertions(+), 104 deletions(-) create mode 100644 lib/Checkout/Identities/Entities/AttemptsQueryFilter.php create mode 100644 lib/Checkout/Identities/Entities/IdentityDeclaredData.php create mode 100644 lib/Checkout/Identities/Entities/IdentityVerificationClientInformation.php create mode 100644 lib/Checkout/Identities/Entities/IdvAddress.php create mode 100644 lib/Checkout/Identities/Entities/PhoneNumber.php create mode 100644 lib/Checkout/Issuing/Cards/Update/CardUpdateHeaders.php create mode 100644 test/Checkout/Tests/Identities/IdentitiesSerializationTest.php create mode 100644 test/Checkout/Tests/Issuing/Cards/CardScheduledActivationDateSerializationTest.php create mode 100644 test/Checkout/Tests/Issuing/Cards/CardUpdateHeadersTest.php diff --git a/lib/Checkout/ApiClient.php b/lib/Checkout/ApiClient.php index 2a558e41..98ac6594 100644 --- a/lib/Checkout/ApiClient.php +++ b/lib/Checkout/ApiClient.php @@ -97,8 +97,13 @@ public function delete(string $path, SdkAuthorization $authorization): array } /** + * Performs a GET with an optional query filter. + * + * The filter is nullable: several clients expose an optional filter argument and pass it + * straight through, so a null must behave exactly like a plain get(). + * * @param string $path - * @param AbstractQueryFilter $requestBody + * @param AbstractQueryFilter|null $requestBody * @param SdkAuthorization $authorization * @param mixed $headers * @return array @@ -106,14 +111,16 @@ public function delete(string $path, SdkAuthorization $authorization): array */ public function query( string $path, - AbstractQueryFilter $requestBody, + ?AbstractQueryFilter $requestBody, SdkAuthorization $authorization, $headers = null ): array { $this->logger->info("GET " . $path); - $queryParameters = $requestBody->getEncodedQueryParameters(); - if (!empty($queryParameters)) { - $path .= "?" . $queryParameters; + if ($requestBody !== null) { + $queryParameters = $requestBody->getEncodedQueryParameters(); + if (!empty($queryParameters)) { + $path .= "?" . $queryParameters; + } } return $this->invoke("GET", $path, null, $authorization, null, $headers); } diff --git a/lib/Checkout/Identities/AddressDocumentVerification/AddressDocumentVerificationClient.php b/lib/Checkout/Identities/AddressDocumentVerification/AddressDocumentVerificationClient.php index 67146076..a701a72a 100644 --- a/lib/Checkout/Identities/AddressDocumentVerification/AddressDocumentVerificationClient.php +++ b/lib/Checkout/Identities/AddressDocumentVerification/AddressDocumentVerificationClient.php @@ -7,6 +7,8 @@ use Checkout\CheckoutApiException; use Checkout\CheckoutConfiguration; use Checkout\Client; +use Checkout\Identities\Entities\AttemptAssetsQueryFilter; +use Checkout\Identities\Entities\AttemptsQueryFilter; use Checkout\Identities\AddressDocumentVerification\Requests\AddressDocumentVerificationRequest; use Checkout\Identities\AddressDocumentVerification\Requests\AddressDocumentVerificationAttemptRequest; @@ -15,6 +17,7 @@ class AddressDocumentVerificationClient extends Client const ADDRESS_DOCUMENT_VERIFICATIONS_PATH = "address-document-verifications"; const ANONYMIZE_PATH = "anonymize"; const ATTEMPTS_PATH = "attempts"; + const ASSETS_PATH = "assets"; const PDF_REPORT_PATH = "pdf-report"; public function __construct(ApiClient $apiClient, CheckoutConfiguration $configuration) @@ -99,17 +102,21 @@ public function createAddressDocumentVerificationAttempt( * addressDocumentVerificationId is the address document verification's unique identifier. (Required) * * @param string $addressDocumentVerificationId + * @param AttemptsQueryFilter|null $query the pagination query parameters (skip and limit) * @return array * @throws CheckoutApiException */ - public function getAddressDocumentVerificationAttempts(string $addressDocumentVerificationId): array - { - return $this->apiClient->get( + public function getAddressDocumentVerificationAttempts( + string $addressDocumentVerificationId, + ?AttemptsQueryFilter $query = null + ): array { + return $this->apiClient->query( $this->buildPath( self::ADDRESS_DOCUMENT_VERIFICATIONS_PATH, $addressDocumentVerificationId, self::ATTEMPTS_PATH ), + $query, $this->sdkAuthorization() ); } @@ -156,4 +163,35 @@ public function getAddressDocumentVerificationReport(string $addressDocumentVeri $this->sdkAuthorization() ); } + + /** + * Retrieve the assets (the document image) uploaded for an address document verification attempt. + * Beta. + * + * $addressDocumentVerificationId is the address document verification's unique identifier. (Required) + * attemptId is the attempt's unique identifier. (Required) + * + * @param string $addressDocumentVerificationId + * @param string $attemptId + * @param AttemptAssetsQueryFilter|null $query the pagination query parameters (skip and limit) + * @return array + * @throws CheckoutApiException + */ + public function getAddressDocumentVerificationAttemptAssets( + string $addressDocumentVerificationId, + string $attemptId, + ?AttemptAssetsQueryFilter $query = null + ): array { + return $this->apiClient->query( + $this->buildPath( + self::ADDRESS_DOCUMENT_VERIFICATIONS_PATH, + $addressDocumentVerificationId, + self::ATTEMPTS_PATH, + $attemptId, + self::ASSETS_PATH + ), + $query, + $this->sdkAuthorization() + ); + } } diff --git a/lib/Checkout/Identities/Entities/AttemptsQueryFilter.php b/lib/Checkout/Identities/Entities/AttemptsQueryFilter.php new file mode 100644 index 00000000..54d1b946 --- /dev/null +++ b/lib/Checkout/Identities/Entities/AttemptsQueryFilter.php @@ -0,0 +1,24 @@ +apiClient->get( + public function getFaceAuthenticationAttempts( + string $faceAuthenticationId, + ?AttemptsQueryFilter $query = null + ): array { + return $this->apiClient->query( $this->buildPath(self::FACE_AUTHENTICATIONS_PATH, $faceAuthenticationId, self::ATTEMPTS_PATH), + $query, $this->sdkAuthorization() ); } diff --git a/lib/Checkout/Identities/FaceAuthentication/Requests/FaceAuthenticationAttemptRequest.php b/lib/Checkout/Identities/FaceAuthentication/Requests/FaceAuthenticationAttemptRequest.php index 9ad2e793..a866554f 100644 --- a/lib/Checkout/Identities/FaceAuthentication/Requests/FaceAuthenticationAttemptRequest.php +++ b/lib/Checkout/Identities/FaceAuthentication/Requests/FaceAuthenticationAttemptRequest.php @@ -3,20 +3,29 @@ namespace Checkout\Identities\FaceAuthentication\Requests; use Checkout\Identities\Entities\ClientInformation; +use Checkout\Identities\Entities\PhoneNumber; class FaceAuthenticationAttemptRequest { /** - * The URL to redirect the applicant to after the attempt. (Required) - * + * The URL to redirect the applicant to after the attempt. + * [Required] + * Format: uri * @var string */ public $redirect_url; + /** + * The applicant's mobile phone number, if sharing the attempt URL via SMS. + * [Optional] + * @var PhoneNumber|null + */ + public $phone_number; + /** * The applicant's details. - * - * @var ClientInformation + * [Optional] + * @var ClientInformation|null */ public $client_information; } diff --git a/lib/Checkout/Identities/IdDocumentVerification/IdDocumentVerificationClient.php b/lib/Checkout/Identities/IdDocumentVerification/IdDocumentVerificationClient.php index 84625304..a7c2f335 100644 --- a/lib/Checkout/Identities/IdDocumentVerification/IdDocumentVerificationClient.php +++ b/lib/Checkout/Identities/IdDocumentVerification/IdDocumentVerificationClient.php @@ -7,6 +7,8 @@ use Checkout\CheckoutApiException; use Checkout\CheckoutConfiguration; use Checkout\Client; +use Checkout\Identities\Entities\AttemptAssetsQueryFilter; +use Checkout\Identities\Entities\AttemptsQueryFilter; use Checkout\Identities\IdDocumentVerification\Requests\IdDocumentVerificationRequest; use Checkout\Identities\IdDocumentVerification\Requests\IdDocumentVerificationAttemptRequest; @@ -15,6 +17,7 @@ class IdDocumentVerificationClient extends Client const ID_DOCUMENT_VERIFICATIONS_PATH = "id-document-verifications"; const ANONYMIZE_PATH = "anonymize"; const ATTEMPTS_PATH = "attempts"; + const ASSETS_PATH = "assets"; const PDF_REPORT_PATH = "pdf-report"; public function __construct(ApiClient $apiClient, CheckoutConfiguration $configuration) @@ -90,13 +93,17 @@ public function createIdDocumentVerificationAttempt( * idDocumentVerificationId is the ID document verification's unique identifier. (Required) * * @param string $idDocumentVerificationId + * @param AttemptsQueryFilter|null $query the pagination query parameters (skip and limit) * @return array * @throws CheckoutApiException */ - public function getIdDocumentVerificationAttempts(string $idDocumentVerificationId): array - { - return $this->apiClient->get( + public function getIdDocumentVerificationAttempts( + string $idDocumentVerificationId, + ?AttemptsQueryFilter $query = null + ): array { + return $this->apiClient->query( $this->buildPath(self::ID_DOCUMENT_VERIFICATIONS_PATH, $idDocumentVerificationId, self::ATTEMPTS_PATH), + $query, $this->sdkAuthorization() ); } @@ -132,4 +139,35 @@ public function getIdDocumentVerificationReport(string $idDocumentVerificationId $this->sdkAuthorization() ); } + + /** + * Retrieve the assets (the front and back images of the document) uploaded for an ID document verification attempt. + * Beta. + * + * $idDocumentVerificationId is the ID document verification's unique identifier. (Required) + * attemptId is the attempt's unique identifier. (Required) + * + * @param string $idDocumentVerificationId + * @param string $attemptId + * @param AttemptAssetsQueryFilter|null $query the pagination query parameters (skip and limit) + * @return array + * @throws CheckoutApiException + */ + public function getIdDocumentVerificationAttemptAssets( + string $idDocumentVerificationId, + string $attemptId, + ?AttemptAssetsQueryFilter $query = null + ): array { + return $this->apiClient->query( + $this->buildPath( + self::ID_DOCUMENT_VERIFICATIONS_PATH, + $idDocumentVerificationId, + self::ATTEMPTS_PATH, + $attemptId, + self::ASSETS_PATH + ), + $query, + $this->sdkAuthorization() + ); + } } diff --git a/lib/Checkout/Identities/IdentityVerification/IdentityVerificationClient.php b/lib/Checkout/Identities/IdentityVerification/IdentityVerificationClient.php index 8994cd14..b133bca6 100644 --- a/lib/Checkout/Identities/IdentityVerification/IdentityVerificationClient.php +++ b/lib/Checkout/Identities/IdentityVerification/IdentityVerificationClient.php @@ -7,6 +7,7 @@ use Checkout\CheckoutApiException; use Checkout\CheckoutConfiguration; use Checkout\Client; +use Checkout\Identities\Entities\AttemptsQueryFilter; use Checkout\Identities\Entities\AttemptAssetsQueryFilter; use Checkout\Identities\IdentityVerification\Requests\IdentityVerificationRequest; use Checkout\Identities\IdentityVerification\Requests\IdentityVerificationAndOpenRequest; @@ -109,13 +110,17 @@ public function createIdentityVerificationAttempt( * identityVerificationId is the identity verification's unique identifier. (Required) * * @param string $identityVerificationId + * @param AttemptsQueryFilter|null $query the pagination query parameters (skip and limit) * @return array * @throws CheckoutApiException */ - public function getIdentityVerificationAttempts(string $identityVerificationId): array - { - return $this->apiClient->get( + public function getIdentityVerificationAttempts( + string $identityVerificationId, + ?AttemptsQueryFilter $query = null + ): array { + return $this->apiClient->query( $this->buildPath(self::IDENTITY_VERIFICATIONS_PATH, $identityVerificationId, self::ATTEMPTS_PATH), + $query, $this->sdkAuthorization() ); } diff --git a/lib/Checkout/Identities/IdentityVerification/Requests/IdentityVerificationAndOpenRequest.php b/lib/Checkout/Identities/IdentityVerification/Requests/IdentityVerificationAndOpenRequest.php index 344b1786..852d2290 100644 --- a/lib/Checkout/Identities/IdentityVerification/Requests/IdentityVerificationAndOpenRequest.php +++ b/lib/Checkout/Identities/IdentityVerification/Requests/IdentityVerificationAndOpenRequest.php @@ -2,14 +2,15 @@ namespace Checkout\Identities\IdentityVerification\Requests; -use Checkout\Identities\Entities\DeclaredData; +use Checkout\Identities\Entities\IdentityDeclaredData; class IdentityVerificationAndOpenRequest { /** * The personal details provided by the applicant. (Required) * - * @var DeclaredData + * [Required] + * @var IdentityDeclaredData */ public $declared_data; diff --git a/lib/Checkout/Identities/IdentityVerification/Requests/IdentityVerificationAttemptRequest.php b/lib/Checkout/Identities/IdentityVerification/Requests/IdentityVerificationAttemptRequest.php index db5d297d..9cb6c3d3 100644 --- a/lib/Checkout/Identities/IdentityVerification/Requests/IdentityVerificationAttemptRequest.php +++ b/lib/Checkout/Identities/IdentityVerification/Requests/IdentityVerificationAttemptRequest.php @@ -2,21 +2,30 @@ namespace Checkout\Identities\IdentityVerification\Requests; -use Checkout\Identities\Entities\ClientInformation; +use Checkout\Identities\Entities\IdentityVerificationClientInformation; +use Checkout\Identities\Entities\PhoneNumber; class IdentityVerificationAttemptRequest { /** - * The URL to redirect the applicant to after the attempt. (Required) - * + * The URL to redirect the applicant to after the attempt. + * [Required] + * Format: uri * @var string */ public $redirect_url; + /** + * The applicant's mobile phone number, if sharing the attempt URL via SMS. + * [Optional] + * @var PhoneNumber|null + */ + public $phone_number; + /** * The applicant's details. - * - * @var ClientInformation + * [Optional] + * @var IdentityVerificationClientInformation|null */ public $client_information; } diff --git a/lib/Checkout/Identities/IdentityVerification/Requests/IdentityVerificationRequest.php b/lib/Checkout/Identities/IdentityVerification/Requests/IdentityVerificationRequest.php index dbeffbe4..c6af54a1 100644 --- a/lib/Checkout/Identities/IdentityVerification/Requests/IdentityVerificationRequest.php +++ b/lib/Checkout/Identities/IdentityVerification/Requests/IdentityVerificationRequest.php @@ -2,7 +2,7 @@ namespace Checkout\Identities\IdentityVerification\Requests; -use Checkout\Identities\Entities\DeclaredData; +use Checkout\Identities\Entities\IdentityDeclaredData; class IdentityVerificationRequest { @@ -16,7 +16,8 @@ class IdentityVerificationRequest /** * The personal details provided by the applicant. * - * @var DeclaredData + * [Required] + * @var IdentityDeclaredData */ public $declared_data; diff --git a/lib/Checkout/Issuing/Cards/Create/CardRequest.php b/lib/Checkout/Issuing/Cards/Create/CardRequest.php index a378e261..b273ca51 100644 --- a/lib/Checkout/Issuing/Cards/Create/CardRequest.php +++ b/lib/Checkout/Issuing/Cards/Create/CardRequest.php @@ -73,20 +73,23 @@ protected function __construct($type) public $metadata; /** - * Date for the card to be automatically revoked. Must be after the current date and date only in the - * form yyyy-mm-dd. + * Date scheduling the card's automatic revocation. * [Optional] - * Format: yyyy-MM-dd - * @var string + * Format: date (YYYY-MM-DD, time is midnight UTC) + * Example: 2027-03-12 + * @var string|null */ public $revocation_date; /** - * ISO 8601 date scheduling the card's activation. Two formats are supported: date only (YYYY-MM-DD, - * treated as midnight UTC), or date with round hour (YYYY-MM-DDTHH:mmZ in UTC, or - * YYYY-MM-DDTHH:mm±HH:mm with offset). Only round hours are allowed when a time is provided (HH:00). + * Date scheduling the card's first activation. Only applies to the initial activation of a card. + * Two formats are supported: date only (YYYY-MM-DD, treated as midnight UTC), or date with + * round hour (YYYY-MM-DDTHH:mmZ in UTC, or YYYY-MM-DDTHH:mm+HH:mm with offset). Only round + * hours are allowed when a time is provided (HH:00). The value must be at least the next round + * hour after the request time. * [Optional] - * @var string + * Example: 2026-06-01T10:00Z + * @var string|null */ - public $activation_date; + public $scheduled_activation_date; } diff --git a/lib/Checkout/Issuing/Cards/Update/CardUpdateHeaders.php b/lib/Checkout/Issuing/Cards/Update/CardUpdateHeaders.php new file mode 100644 index 00000000..22634c80 --- /dev/null +++ b/lib/Checkout/Issuing/Cards/Update/CardUpdateHeaders.php @@ -0,0 +1,49 @@ + 'return-encrypted-cvv', + 'encryption_key' => 'Encryption-Key' + ]; + } +} diff --git a/lib/Checkout/Issuing/Cards/Update/UpdateCardRequest.php b/lib/Checkout/Issuing/Cards/Update/UpdateCardRequest.php index eb3cf60c..507eadc1 100644 --- a/lib/Checkout/Issuing/Cards/Update/UpdateCardRequest.php +++ b/lib/Checkout/Issuing/Cards/Update/UpdateCardRequest.php @@ -39,20 +39,23 @@ class UpdateCardRequest public $expiry_year; /** - * ISO 8601 date scheduling the card's activation. Two formats are supported: date only (YYYY-MM-DD, - * treated as midnight UTC), or date with round hour (YYYY-MM-DDTHH:mmZ in UTC, or - * YYYY-MM-DDTHH:mm±HH:mm with offset). Only round hours are allowed when a time is provided (HH:00). + * Date scheduling the card's first activation. Only applies to the initial activation of a card. + * Two formats are supported: date only (YYYY-MM-DD, treated as midnight UTC), or date with + * round hour (YYYY-MM-DDTHH:mmZ in UTC, or YYYY-MM-DDTHH:mm+HH:mm with offset). Only round + * hours are allowed when a time is provided (HH:00). The value must be at least the next round + * hour after the request time. * [Optional] - * @var string + * Example: 2026-06-01T10:00Z + * @var string|null */ - public $activation_date; + public $scheduled_activation_date; /** - * Date for the card to be automatically revoked. Must be after the current date and date only in the - * form yyyy-mm-dd. + * Date scheduling the card's automatic revocation. * [Optional] - * Format: yyyy-MM-dd - * @var string + * Format: date (YYYY-MM-DD, time is midnight UTC) + * Example: 2027-03-12 + * @var string|null */ public $revocation_date; } diff --git a/lib/Checkout/Issuing/IssuingClient.php b/lib/Checkout/Issuing/IssuingClient.php index 2bdff5ed..d3ce1df0 100644 --- a/lib/Checkout/Issuing/IssuingClient.php +++ b/lib/Checkout/Issuing/IssuingClient.php @@ -14,6 +14,7 @@ use Checkout\Issuing\Cards\Enrollment\UpdateThreeDSEnrollmentRequest; use Checkout\Issuing\Cards\Revoke\RevokeCardRequest; use Checkout\Issuing\Cards\Suspend\SuspendCardRequest; +use Checkout\Issuing\Cards\Update\CardUpdateHeaders; use Checkout\Issuing\Cards\Update\UpdateCardRequest; use Checkout\Issuing\Cards\Renew\RenewCardRequest; use Checkout\Issuing\Controls\Create\CardControlRequest; @@ -756,15 +757,21 @@ public function getSingleTransaction(string $transactionId) : array * * @param string $cardId - The card's unique identifier. (Required) * @param UpdateCardRequest $updateCardRequest (Required) + * @param CardUpdateHeaders|null $headers - The optional return-encrypted-cvv and + * Encryption-Key HTTP headers. (Optional) * @return array * @throws CheckoutApiException */ - public function updateCardDetails(string $cardId, UpdateCardRequest $updateCardRequest) : array - { + public function updateCardDetails( + string $cardId, + UpdateCardRequest $updateCardRequest, + ?CardUpdateHeaders $headers = null + ) : array { return $this->apiClient->patch( $this->buildPath(self::ISSUING_PATH, self::CARDS_PATH, $cardId), $updateCardRequest, - $this->sdkAuthorization() + $this->sdkAuthorization(), + $headers ); } diff --git a/test/Checkout/Tests/Identities/AddressDocumentVerification/AddressDocumentVerificationClientTest.php b/test/Checkout/Tests/Identities/AddressDocumentVerification/AddressDocumentVerificationClientTest.php index d269b99c..50758f62 100644 --- a/test/Checkout/Tests/Identities/AddressDocumentVerification/AddressDocumentVerificationClientTest.php +++ b/test/Checkout/Tests/Identities/AddressDocumentVerification/AddressDocumentVerificationClientTest.php @@ -9,6 +9,8 @@ use Checkout\Identities\AddressDocumentVerification\AddressDocumentVerificationClient; use Checkout\Identities\AddressDocumentVerification\Requests\AddressDocumentVerificationRequest; use Checkout\Identities\AddressDocumentVerification\Requests\AddressDocumentVerificationAttemptRequest; +use Checkout\Identities\Entities\AttemptAssetsQueryFilter; +use Checkout\Identities\Entities\AttemptsQueryFilter; use Checkout\Identities\Entities\DeclaredData; use Checkout\PlatformType; use Checkout\Tests\UnitTestFixture; @@ -115,7 +117,7 @@ public function shouldGetAddressDocumentVerificationAttempts() $expectedResponse = $this->buildExpectedAddressDocumentVerificationAttemptsResponse(); $this->apiClient - ->method("get") + ->method("query") ->willReturn($expectedResponse); $response = $this->client->getAddressDocumentVerificationAttempts("adv_tkoi5db4hryu5cei5vwoabr7we"); @@ -254,7 +256,7 @@ public function shouldCallCorrectApiEndpointForGetAddressDocumentVerificationAtt $this->apiClient ->expects($this->once()) - ->method("get") + ->method("query") ->with("address-document-verifications/" . $advId . "/attempts") ->willReturn($expectedResponse); @@ -304,6 +306,77 @@ public function shouldCallCorrectApiEndpointForGetAddressDocumentVerificationRep $this->assertNotNull($response); } + /** + * @test + * @throws CheckoutApiException + */ + public function shouldGetAddressDocumentVerificationAttemptsWithPagination() + { + $advId = "adv_tkoi5db4hryu5cei5vwoabr7we"; + $expectedResponse = $this->buildExpectedAddressDocumentVerificationAttemptsResponse(); + + $query = new AttemptsQueryFilter(); + $query->skip = 5; + $query->limit = 25; + + $this->apiClient + ->expects($this->once()) + ->method("query") + ->with("address-document-verifications/" . $advId . "/attempts", $query) + ->willReturn($expectedResponse); + + $response = $this->client->getAddressDocumentVerificationAttempts($advId, $query); + + $this->assertNotNull($response); + $this->assertSame("skip=5&limit=25", $query->getEncodedQueryParameters()); + } + + /** + * @test + * @throws CheckoutApiException + */ + public function shouldGetAddressDocumentVerificationAttemptAssets() + { + $expectedResponse = $this->buildExpectedAddressDocumentVerificationAttemptAssetsResponse(); + + $this->apiClient + ->method("query") + ->willReturn($expectedResponse); + + $query = new AttemptAssetsQueryFilter(); + $query->limit = 10; + + $response = $this->client->getAddressDocumentVerificationAttemptAssets( + "adv_tkoi5db4hryu5cei5vwoabr7we", + "adva_tkoi5db4hryu5cei5vwoabr7we", + $query + ); + + $this->assertNotNull($response); + $this->validateAddressDocumentVerificationAttemptAssetsResponse($response); + } + + /** + * @test + * @throws CheckoutApiException + */ + public function shouldCallCorrectApiEndpointForGetAddressDocumentVerificationAttemptAssets() + { + $advId = "adv_tkoi5db4hryu5cei5vwoabr7we"; + $attemptId = "adva_tkoi5db4hryu5cei5vwoabr7we"; + $expectedResponse = $this->buildExpectedAddressDocumentVerificationAttemptAssetsResponse(); + + $this->apiClient + ->expects($this->once()) + ->method("query") + ->with("address-document-verifications/" . $advId . "/attempts/" . $attemptId . "/assets") + ->willReturn($expectedResponse); + + $response = $this->client->getAddressDocumentVerificationAttemptAssets($advId, $attemptId); + + $this->assertNotNull($response); + } + private function buildAddressDocumentVerificationRequest(): AddressDocumentVerificationRequest { $declaredData = new DeclaredData(); @@ -366,6 +439,43 @@ private function buildExpectedAddressDocumentVerificationAttemptResponse(): arra ]; } + private function buildExpectedAddressDocumentVerificationAttemptAssetsResponse(): array + { + return [ + "total_count" => 1, + "skip" => 0, + "limit" => 10, + "data" => [ + [ + "type" => "document", + "_links" => [ + "download" => [ + "href" => "https://api.checkout.com/address-document-verifications/" + . "adv_id/attempts/adva_id/assets/document" + ] + ] + ] + ], + "_links" => [ + "self" => [ + "href" => "https://api.checkout.com/address-document-verifications/" + . "adv_id/attempts/adva_id/assets" + ] + ] + ]; + } + + private function validateAddressDocumentVerificationAttemptAssetsResponse(array $response): void + { + $this->assertArrayHasKey("total_count", $response); + $this->assertArrayHasKey("skip", $response); + $this->assertArrayHasKey("limit", $response); + $this->assertArrayHasKey("data", $response); + $this->assertArrayHasKey("_links", $response); + $this->assertSame("document", $response["data"][0]["type"]); + $this->assertArrayHasKey("download", $response["data"][0]["_links"]); + } + private function buildExpectedAddressDocumentVerificationAttemptsResponse(): array { return [ diff --git a/test/Checkout/Tests/Identities/AddressDocumentVerification/AddressDocumentVerificationIntegrationTest.php b/test/Checkout/Tests/Identities/AddressDocumentVerification/AddressDocumentVerificationIntegrationTest.php index d9ad2f22..129bc59f 100644 --- a/test/Checkout/Tests/Identities/AddressDocumentVerification/AddressDocumentVerificationIntegrationTest.php +++ b/test/Checkout/Tests/Identities/AddressDocumentVerification/AddressDocumentVerificationIntegrationTest.php @@ -8,6 +8,8 @@ use Checkout\CheckoutException; use Checkout\Identities\AddressDocumentVerification\Requests\AddressDocumentVerificationRequest; use Checkout\Identities\AddressDocumentVerification\Requests\AddressDocumentVerificationAttemptRequest; +use Checkout\Identities\Entities\AttemptAssetsQueryFilter; +use Checkout\Identities\Entities\AttemptsQueryFilter; use Checkout\Identities\Entities\DeclaredData; use Checkout\PlatformType; use Checkout\Tests\SandboxTestFixture; @@ -158,6 +160,59 @@ public function shouldGetAddressDocumentVerificationReport() $this->assertResponse($response, "id"); } + /** + * @test + * @throws CheckoutApiException + */ + public function shouldGetAddressDocumentVerificationAttemptsWithPagination() + { + $this->markTestSkipped("This test requires valid test environment setup"); + + $verificationRequest = $this->buildAddressDocumentVerificationRequest(); + $created = $this->checkoutApi->getAddressDocumentVerificationClient() + ->createAddressDocumentVerification($verificationRequest); + + $attemptRequest = $this->buildAddressDocumentVerificationAttemptRequest(); + $this->checkoutApi->getAddressDocumentVerificationClient() + ->createAddressDocumentVerificationAttempt($created["id"], $attemptRequest); + + $query = new AttemptsQueryFilter(); + $query->skip = 0; + $query->limit = 1; + + $response = $this->checkoutApi->getAddressDocumentVerificationClient() + ->getAddressDocumentVerificationAttempts($created["id"], $query); + + $this->validateRetrievedAddressDocumentVerificationAttempts($response); + $this->assertLessThanOrEqual(1, count($response["data"])); + $this->assertEquals(1, $response["limit"]); + } + + /** + * @test + * @throws CheckoutApiException + */ + public function shouldGetAddressDocumentVerificationAttemptAssets() + { + $this->markTestSkipped("This test requires valid test environment setup"); + + $verificationRequest = $this->buildAddressDocumentVerificationRequest(); + $created = $this->checkoutApi->getAddressDocumentVerificationClient() + ->createAddressDocumentVerification($verificationRequest); + + $attemptRequest = $this->buildAddressDocumentVerificationAttemptRequest(); + $createdAttempt = $this->checkoutApi->getAddressDocumentVerificationClient() + ->createAddressDocumentVerificationAttempt($created["id"], $attemptRequest); + + $query = new AttemptAssetsQueryFilter(); + $query->limit = 10; + + $response = $this->checkoutApi->getAddressDocumentVerificationClient() + ->getAddressDocumentVerificationAttemptAssets($created["id"], $createdAttempt["id"], $query); + + $this->validateRetrievedAddressDocumentVerificationAttemptAssets($response); + } + private function buildAddressDocumentVerificationRequest(): AddressDocumentVerificationRequest { $declaredData = new DeclaredData(); @@ -228,6 +283,18 @@ private function validateRetrievedAddressDocumentVerificationAttempts(array $res } } + private function validateRetrievedAddressDocumentVerificationAttemptAssets(array $response): void + { + $this->assertResponse($response, "total_count", "skip", "limit", "data", "_links"); + + $this->assertTrue(is_array($response["data"])); + + if (!empty($response["data"])) { + $this->assertResponse($response["data"][0], "type", "_links"); + $this->assertEquals("document", $response["data"][0]["type"]); + } + } + private function validateRetrievedAddressDocumentVerificationAttempt(array $retrieved, array $created): void { $this->validateBaseAddressDocumentVerificationAttemptResponse($retrieved); diff --git a/test/Checkout/Tests/Identities/FaceAuthentication/FaceAuthenticationClientTest.php b/test/Checkout/Tests/Identities/FaceAuthentication/FaceAuthenticationClientTest.php index 056a5559..9fb85feb 100644 --- a/test/Checkout/Tests/Identities/FaceAuthentication/FaceAuthenticationClientTest.php +++ b/test/Checkout/Tests/Identities/FaceAuthentication/FaceAuthenticationClientTest.php @@ -7,6 +7,7 @@ use Checkout\CheckoutAuthorizationException; use Checkout\CheckoutException; use Checkout\Identities\Entities\AttemptAssetsQueryFilter; +use Checkout\Identities\Entities\AttemptsQueryFilter; use Checkout\Identities\Entities\ClientInformation; use Checkout\Identities\FaceAuthentication\FaceAuthenticationClient; use Checkout\Identities\FaceAuthentication\Requests\FaceAuthenticationRequest; @@ -116,7 +117,7 @@ public function shouldGetFaceAuthenticationAttempts() $expectedResponse = $this->buildExpectedFaceAuthenticationAttemptsResponse(); $this->apiClient - ->method("get") + ->method("query") ->willReturn($expectedResponse); $response = $this->client->getFaceAuthenticationAttempts("face_auth_12345"); @@ -277,7 +278,7 @@ public function shouldCallCorrectApiEndpointForGetFaceAuthenticationAttempts() $this->apiClient ->expects($this->once()) - ->method("get") + ->method("query") ->with("face-authentications/" . $faceAuthId . "/attempts") ->willReturn($expectedResponse); @@ -349,6 +350,31 @@ private function buildFaceAuthenticationAttemptRequest(): FaceAuthenticationAtte return $request; } + /** + * @test + * @throws CheckoutApiException + */ + public function shouldGetFaceAuthenticationAttemptsWithPagination() + { + $faceAuthenticationId = "face_auth_12345"; + $expectedResponse = $this->buildExpectedFaceAuthenticationAttemptsResponse(); + + $query = new AttemptsQueryFilter(); + $query->skip = 5; + $query->limit = 25; + + $this->apiClient + ->expects($this->once()) + ->method("query") + ->with("face-authentications/" . $faceAuthenticationId . "/attempts", $query) + ->willReturn($expectedResponse); + + $response = $this->client->getFaceAuthenticationAttempts($faceAuthenticationId, $query); + + $this->assertNotNull($response); + $this->assertSame("skip=5&limit=25", $query->getEncodedQueryParameters()); + } + private function buildExpectedFaceAuthenticationResponse(): array { return [ @@ -375,15 +401,27 @@ private function buildExpectedFaceAuthenticationAttemptResponse(): array private function buildExpectedFaceAuthenticationAttemptsResponse(): array { return [ - "attempts" => [ + "total_count" => 1, + "skip" => 0, + "limit" => 10, + "data" => [ [ "id" => "attempt_67890", - "face_authentication_id" => "face_auth_12345", "status" => "pending", + "redirect_url" => "https://verify.checkout.com/attempt_67890", + "response_codes" => [], + "phone_number" => [ + "country_code" => "+33", + "number" => "1234567890" + ], "created_on" => "2024-03-20T10:30:00Z" ] ], - "total_count" => 1 + "_links" => [ + "self" => [ + "href" => "https://api.checkout.com/face-authentications/face_auth_12345/attempts" + ] + ] ]; } @@ -417,13 +455,15 @@ private function validateFaceAuthenticationAttemptResponse(array $response): voi private function validateFaceAuthenticationAttemptsResponse(array $response): void { - $this->assertArrayHasKey("attempts", $response); $this->assertArrayHasKey("total_count", $response); + $this->assertArrayHasKey("skip", $response); + $this->assertArrayHasKey("limit", $response); + $this->assertArrayHasKey("data", $response); + $this->assertArrayHasKey("_links", $response); - $this->assertNotNull($response["attempts"]); - $this->assertTrue(is_array($response["attempts"])); - $this->assertNotNull($response["total_count"]); $this->assertTrue(is_numeric($response["total_count"])); + $this->assertTrue(is_array($response["data"])); + $this->assertSame("+33", $response["data"][0]["phone_number"]["country_code"]); } private function buildExpectedFaceAuthenticationAttemptAssetsResponse(): array diff --git a/test/Checkout/Tests/Identities/FaceAuthentication/FaceAuthenticationIntegrationTest.php b/test/Checkout/Tests/Identities/FaceAuthentication/FaceAuthenticationIntegrationTest.php index 79e27c76..17d4d283 100644 --- a/test/Checkout/Tests/Identities/FaceAuthentication/FaceAuthenticationIntegrationTest.php +++ b/test/Checkout/Tests/Identities/FaceAuthentication/FaceAuthenticationIntegrationTest.php @@ -6,7 +6,9 @@ use Checkout\CheckoutArgumentException; use Checkout\CheckoutAuthorizationException; use Checkout\CheckoutException; +use Checkout\Identities\Entities\AttemptsQueryFilter; use Checkout\Identities\Entities\ClientInformation; +use Checkout\Identities\Entities\PhoneNumber; use Checkout\Identities\FaceAuthentication\Requests\FaceAuthenticationRequest; use Checkout\Identities\FaceAuthentication\Requests\FaceAuthenticationAttemptRequest; use Checkout\PlatformType; @@ -218,6 +220,32 @@ public function shouldPerformFaceAuthenticationWorkflow() $this->validateWorkflowProgression($createdFaceAuthentication, $retrievedFaceAuthentication, $createdAttempt, $attemptsResponse, $retrievedAttempt, $anonymizedFaceAuthentication); } + /** + * @test + * @throws CheckoutApiException + */ + public function shouldGetFaceAuthenticationAttemptsWithPagination() + { + $this->markTestSkipped("This test requires valid test environment setup"); + + $created = $this->checkoutApi->getFaceAuthenticationClient() + ->createFaceAuthentication($this->buildFaceAuthenticationRequest()); + + $createdAttempt = $this->checkoutApi->getFaceAuthenticationClient() + ->createFaceAuthenticationAttempt($created["id"], $this->buildFaceAuthenticationAttemptRequest()); + + $query = new AttemptsQueryFilter(); + $query->skip = 0; + $query->limit = 1; + + $response = $this->checkoutApi->getFaceAuthenticationClient() + ->getFaceAuthenticationAttempts($created["id"], $query); + + $this->validateRetrievedFaceAuthenticationAttempts($response, $createdAttempt); + $this->assertLessThanOrEqual(1, count($response["data"])); + $this->assertEquals(1, $response["limit"]); + } + private function buildFaceAuthenticationRequest(): FaceAuthenticationRequest { $request = new FaceAuthenticationRequest(); @@ -233,8 +261,13 @@ private function buildFaceAuthenticationAttemptRequest(): FaceAuthenticationAtte $clientInformation->pre_selected_residence_country = "US"; $clientInformation->pre_selected_language = "en-US"; + $phoneNumber = new PhoneNumber(); + $phoneNumber->country_code = "+1"; + $phoneNumber->number = "5555550102"; + $request = new FaceAuthenticationAttemptRequest(); $request->redirect_url = "https://example.com/redirect"; + $request->phone_number = $phoneNumber; $request->client_information = $clientInformation; return $request; @@ -299,15 +332,18 @@ private function validateRetrievedFaceAuthenticationAttempts(array $response, ar { $this->assertResponse( $response, - "attempts", - "total_count" + "data", + "total_count", + "skip", + "limit", + "_links" ); - $this->assertTrue(is_array($response["attempts"])); + $this->assertTrue(is_array($response["data"])); $this->assertGreaterThan(0, $response["total_count"]); - - if (!empty($response["attempts"])) { - $this->validateBaseFaceAuthenticationAttemptResponse($response["attempts"][0]); + + if (!empty($response["data"])) { + $this->validateBaseFaceAuthenticationAttemptResponse($response["data"][0]); } } @@ -337,7 +373,7 @@ private function validateWorkflowProgression( // Attempts response should contain the created attempt $this->assertGreaterThan(0, $attemptsResponse["total_count"]); - $this->assertTrue(is_array($attemptsResponse["attempts"])); + $this->assertTrue(is_array($attemptsResponse["data"])); // Retrieved attempt should match created attempt $this->assertEquals($createdAttempt["id"], $retrievedAttempt["id"]); diff --git a/test/Checkout/Tests/Identities/IdDocumentVerification/IdDocumentVerificationClientTest.php b/test/Checkout/Tests/Identities/IdDocumentVerification/IdDocumentVerificationClientTest.php index 8a601722..bfd14b33 100644 --- a/test/Checkout/Tests/Identities/IdDocumentVerification/IdDocumentVerificationClientTest.php +++ b/test/Checkout/Tests/Identities/IdDocumentVerification/IdDocumentVerificationClientTest.php @@ -6,6 +6,8 @@ use Checkout\CheckoutArgumentException; use Checkout\CheckoutAuthorizationException; use Checkout\CheckoutException; +use Checkout\Identities\Entities\AttemptAssetsQueryFilter; +use Checkout\Identities\Entities\AttemptsQueryFilter; use Checkout\Identities\Entities\DeclaredData; use Checkout\Identities\IdDocumentVerification\IdDocumentVerificationClient; use Checkout\Identities\IdDocumentVerification\Requests\IdDocumentVerificationRequest; @@ -115,7 +117,7 @@ public function shouldGetIdDocumentVerificationAttempts() $expectedResponse = $this->buildExpectedIdDocumentVerificationAttemptsResponse(); $this->apiClient - ->method("get") + ->method("query") ->willReturn($expectedResponse); $response = $this->client->getIdDocumentVerificationAttempts("iddoc_12345"); @@ -252,7 +254,7 @@ public function shouldCallCorrectApiEndpointForGetIdDocumentVerificationAttempts $this->apiClient ->expects($this->once()) - ->method("get") + ->method("query") ->with("id-document-verifications/" . $idDocVerificationId . "/attempts") ->willReturn($expectedResponse); @@ -322,6 +324,77 @@ public function shouldHandleCreateIdDocumentVerificationWithAllParameters() $this->validateIdDocumentVerificationResponse($response); } + /** + * @test + * @throws CheckoutApiException + */ + public function shouldGetIdDocumentVerificationAttemptsWithPagination() + { + $iddvId = "iddoc_12345"; + $expectedResponse = $this->buildExpectedIdDocumentVerificationAttemptsResponse(); + + $query = new AttemptsQueryFilter(); + $query->skip = 5; + $query->limit = 25; + + $this->apiClient + ->expects($this->once()) + ->method("query") + ->with("id-document-verifications/" . $iddvId . "/attempts", $query) + ->willReturn($expectedResponse); + + $response = $this->client->getIdDocumentVerificationAttempts($iddvId, $query); + + $this->assertNotNull($response); + $this->assertSame("skip=5&limit=25", $query->getEncodedQueryParameters()); + } + + /** + * @test + * @throws CheckoutApiException + */ + public function shouldGetIdDocumentVerificationAttemptAssets() + { + $expectedResponse = $this->buildExpectedIdDocumentVerificationAttemptAssetsResponse(); + + $this->apiClient + ->method("query") + ->willReturn($expectedResponse); + + $query = new AttemptAssetsQueryFilter(); + $query->limit = 10; + + $response = $this->client->getIdDocumentVerificationAttemptAssets( + "iddoc_12345", + "attempt_67890", + $query + ); + + $this->assertNotNull($response); + $this->validateIdDocumentVerificationAttemptAssetsResponse($response); + } + + /** + * @test + * @throws CheckoutApiException + */ + public function shouldCallCorrectApiEndpointForGetIdDocumentVerificationAttemptAssets() + { + $iddvId = "iddoc_12345"; + $attemptId = "attempt_67890"; + $expectedResponse = $this->buildExpectedIdDocumentVerificationAttemptAssetsResponse(); + + $this->apiClient + ->expects($this->once()) + ->method("query") + ->with("id-document-verifications/" . $iddvId . "/attempts/" . $attemptId . "/assets") + ->willReturn($expectedResponse); + + $response = $this->client->getIdDocumentVerificationAttemptAssets($iddvId, $attemptId); + + $this->assertNotNull($response); + } + private function buildIdDocumentVerificationRequest(): IdDocumentVerificationRequest { $declaredData = new DeclaredData(); @@ -369,9 +442,59 @@ private function buildExpectedIdDocumentVerificationAttemptResponse(): array ]; } + private function buildExpectedIdDocumentVerificationAttemptAssetsResponse(): array + { + return [ + "total_count" => 2, + "skip" => 0, + "limit" => 10, + "data" => [ + [ + "type" => "document_front_image", + "_links" => [ + "download" => [ + "href" => "https://api.checkout.com/id-document-verifications/" + . "iddoc_12345/attempts/attempt_67890/assets/document_front_image" + ] + ] + ], + [ + "type" => "document_back_image", + "_links" => [ + "download" => [ + "href" => "https://api.checkout.com/id-document-verifications/" + . "iddoc_12345/attempts/attempt_67890/assets/document_back_image" + ] + ] + ] + ], + "_links" => [ + "self" => [ + "href" => "https://api.checkout.com/id-document-verifications/" + . "iddoc_12345/attempts/attempt_67890/assets" + ] + ] + ]; + } + + private function validateIdDocumentVerificationAttemptAssetsResponse(array $response): void + { + $this->assertArrayHasKey("total_count", $response); + $this->assertArrayHasKey("skip", $response); + $this->assertArrayHasKey("limit", $response); + $this->assertArrayHasKey("data", $response); + $this->assertArrayHasKey("_links", $response); + $this->assertSame("document_front_image", $response["data"][0]["type"]); + $this->assertSame("document_back_image", $response["data"][1]["type"]); + $this->assertArrayHasKey("download", $response["data"][0]["_links"]); + } + private function buildExpectedIdDocumentVerificationAttemptsResponse(): array { return [ + "total_count" => 1, + "skip" => 0, + "limit" => 10, "data" => [ [ "id" => "attempt_67890", @@ -380,7 +503,11 @@ private function buildExpectedIdDocumentVerificationAttemptsResponse(): array "created_on" => "2024-03-20T10:30:00Z" ] ], - "total_count" => 1 + "_links" => [ + "self" => [ + "href" => "https://api.checkout.com/id-document-verifications/iddoc_12345/attempts" + ] + ] ]; } @@ -425,7 +552,10 @@ private function validateIdDocumentVerificationAttemptsResponse(array $response) { $this->assertArrayHasKey("data", $response); $this->assertArrayHasKey("total_count", $response); - + $this->assertArrayHasKey("skip", $response); + $this->assertArrayHasKey("limit", $response); + $this->assertArrayHasKey("_links", $response); + $this->assertNotNull($response["data"]); $this->assertTrue(is_array($response["data"])); $this->assertNotNull($response["total_count"]); diff --git a/test/Checkout/Tests/Identities/IdDocumentVerification/IdDocumentVerificationIntegrationTest.php b/test/Checkout/Tests/Identities/IdDocumentVerification/IdDocumentVerificationIntegrationTest.php index f1d8cc98..918ef3f3 100644 --- a/test/Checkout/Tests/Identities/IdDocumentVerification/IdDocumentVerificationIntegrationTest.php +++ b/test/Checkout/Tests/Identities/IdDocumentVerification/IdDocumentVerificationIntegrationTest.php @@ -6,6 +6,8 @@ use Checkout\CheckoutArgumentException; use Checkout\CheckoutAuthorizationException; use Checkout\CheckoutException; +use Checkout\Identities\Entities\AttemptAssetsQueryFilter; +use Checkout\Identities\Entities\AttemptsQueryFilter; use Checkout\Identities\Entities\DeclaredData; use Checkout\Identities\IdDocumentVerification\Requests\IdDocumentVerificationRequest; use Checkout\Identities\IdDocumentVerification\Requests\IdDocumentVerificationAttemptRequest; @@ -213,6 +215,63 @@ public function shouldPerformIdDocumentVerificationWorkflow() $this->validateWorkflowProgression($createdIdDocumentVerification, $retrievedIdDocumentVerification, $createdAttempt, $attemptsResponse, $retrievedAttempt, $reportResponse, $anonymizedIdDocumentVerification); } + /** + * @test + * @throws CheckoutApiException + */ + public function shouldGetIdDocumentVerificationAttemptsWithPagination() + { + $this->markTestSkipped("This test requires valid test environment setup"); + + $idDocVerificationRequest = $this->buildIdDocumentVerificationRequest(); + $createdIdDocumentVerification = $this->checkoutApi->getIdDocumentVerificationClient() + ->createIdDocumentVerification($idDocVerificationRequest); + + $attemptRequest = $this->buildIdDocumentVerificationAttemptRequest(); + $createdAttempt = $this->checkoutApi->getIdDocumentVerificationClient() + ->createIdDocumentVerificationAttempt($createdIdDocumentVerification["id"], $attemptRequest); + + $query = new AttemptsQueryFilter(); + $query->skip = 0; + $query->limit = 1; + + $response = $this->checkoutApi->getIdDocumentVerificationClient() + ->getIdDocumentVerificationAttempts($createdIdDocumentVerification["id"], $query); + + $this->validateRetrievedIdDocumentVerificationAttempts($response, $createdAttempt); + $this->assertLessThanOrEqual(1, count($response["data"])); + $this->assertEquals(1, $response["limit"]); + } + + /** + * @test + * @throws CheckoutApiException + */ + public function shouldGetIdDocumentVerificationAttemptAssets() + { + $this->markTestSkipped("This test requires valid test environment setup"); + + $idDocVerificationRequest = $this->buildIdDocumentVerificationRequest(); + $createdIdDocumentVerification = $this->checkoutApi->getIdDocumentVerificationClient() + ->createIdDocumentVerification($idDocVerificationRequest); + + $attemptRequest = $this->buildIdDocumentVerificationAttemptRequest(); + $createdAttempt = $this->checkoutApi->getIdDocumentVerificationClient() + ->createIdDocumentVerificationAttempt($createdIdDocumentVerification["id"], $attemptRequest); + + $query = new AttemptAssetsQueryFilter(); + $query->limit = 10; + + $response = $this->checkoutApi->getIdDocumentVerificationClient() + ->getIdDocumentVerificationAttemptAssets( + $createdIdDocumentVerification["id"], + $createdAttempt["id"], + $query + ); + + $this->validateRetrievedIdDocumentVerificationAttemptAssets($response); + } + private function buildIdDocumentVerificationRequest(): IdDocumentVerificationRequest { $declaredData = new DeclaredData(); @@ -296,7 +355,10 @@ private function validateRetrievedIdDocumentVerificationAttempts(array $response $this->assertResponse( $response, "data", - "total_count" + "total_count", + "skip", + "limit", + "_links" ); $this->assertTrue(is_array($response["data"])); @@ -307,6 +369,21 @@ private function validateRetrievedIdDocumentVerificationAttempts(array $response } } + private function validateRetrievedIdDocumentVerificationAttemptAssets(array $response): void + { + $this->assertResponse($response, "total_count", "skip", "limit", "data", "_links"); + + $this->assertTrue(is_array($response["data"])); + + if (!empty($response["data"])) { + $this->assertResponse($response["data"][0], "type", "_links"); + $this->assertContains( + $response["data"][0]["type"], + ["document_front_image", "document_back_image"] + ); + } + } + private function validateRetrievedIdDocumentVerificationAttempt(array $retrieved, array $created): void { // Base validation diff --git a/test/Checkout/Tests/Identities/IdentitiesSerializationTest.php b/test/Checkout/Tests/Identities/IdentitiesSerializationTest.php new file mode 100644 index 00000000..e7e142d0 --- /dev/null +++ b/test/Checkout/Tests/Identities/IdentitiesSerializationTest.php @@ -0,0 +1,308 @@ +serializer = new JsonSerializer(); + } + + public function testPhoneNumberSerializesBothProperties() + { + $phoneNumber = new PhoneNumber(); + $phoneNumber->country_code = "+33"; + $phoneNumber->number = "5555550102"; + + $decoded = json_decode($this->serializer->serialize($phoneNumber), true); + + $this->assertSame(["country_code", "number"], array_keys($decoded)); + $this->assertSame("+33", $decoded["country_code"]); + $this->assertSame("5555550102", $decoded["number"]); + } + + public function testIdvAddressSerializesEveryProperty() + { + $address = new IdvAddress(); + $address->address_line1 = "123 Main Street"; + $address->address_line2 = "Flat 4"; + $address->city = "London"; + $address->state = "Greater London"; + $address->zip = "SW1A 1AA"; + $address->country = "GB"; + + $decoded = json_decode($this->serializer->serialize($address), true); + + $this->assertSame( + ["address_line1", "address_line2", "city", "state", "zip", "country"], + array_keys($decoded) + ); + $this->assertSame("123 Main Street", $decoded["address_line1"]); + $this->assertSame("Flat 4", $decoded["address_line2"]); + $this->assertSame("London", $decoded["city"]); + $this->assertSame("Greater London", $decoded["state"]); + $this->assertSame("SW1A 1AA", $decoded["zip"]); + $this->assertSame("GB", $decoded["country"]); + } + + public function testDeclaredDataSerializesNameAndBirthDate() + { + $declaredData = new DeclaredData(); + $declaredData->name = "Hannah Bret"; + $declaredData->birth_date = "1994-10-15"; + + $decoded = json_decode($this->serializer->serialize($declaredData), true); + + $this->assertSame("Hannah Bret", $decoded["name"]); + $this->assertSame("1994-10-15", $decoded["birth_date"]); + $this->assertCount(2, $decoded); + } + + public function testDeclaredDataOmitsUnsetBirthDate() + { + $declaredData = new DeclaredData(); + $declaredData->name = "Hannah Bret"; + + $decoded = json_decode($this->serializer->serialize($declaredData), true); + + $this->assertSame(["name"], array_keys($decoded)); + } + + public function testIdentityDeclaredDataSerializesEveryProperty() + { + $decoded = json_decode($this->serializer->serialize($this->buildIdentityDeclaredData()), true); + + $this->assertSame("Hannah Bret", $decoded["name"]); + $this->assertSame("1994-10-15", $decoded["birth_date"]); + $this->assertSame("hannah.bret@example.com", $decoded["email"]); + $this->assertSame("+33", $decoded["phone_number"]["country_code"]); + $this->assertSame("5555550102", $decoded["phone_number"]["number"]); + $this->assertSame("123 Main Street", $decoded["address"]["address_line1"]); + $this->assertSame("GB", $decoded["address"]["country"]); + $this->assertCount(5, $decoded); + } + + public function testIdentityDeclaredDataRoundTrip() + { + $json = $this->serializer->serialize($this->buildIdentityDeclaredData()); + $decoded = $this->serializer->deserialize($json); + + $reserialized = $this->serializer->serialize($decoded); + + $this->assertSame( + json_decode($json, true), + json_decode($reserialized, true) + ); + } + + public function testFaceAuthenticationClientInformationKeepsTheTwoFieldShape() + { + $clientInformation = new ClientInformation(); + $clientInformation->pre_selected_residence_country = "FR"; + $clientInformation->pre_selected_language = "en-US"; + + $decoded = json_decode($this->serializer->serialize($clientInformation), true); + + $this->assertSame( + ["pre_selected_residence_country", "pre_selected_language"], + array_keys($decoded) + ); + $this->assertSame("FR", $decoded["pre_selected_residence_country"]); + $this->assertSame("en-US", $decoded["pre_selected_language"]); + } + + public function testIdentityVerificationClientInformationAddsTheTwoIdvOnlyFields() + { + $decoded = json_decode( + $this->serializer->serialize($this->buildIdentityVerificationClientInformation()), + true + ); + + $this->assertSame("FR", $decoded["pre_selected_residence_country"]); + $this->assertSame("en-US", $decoded["pre_selected_language"]); + $this->assertSame("GB", $decoded["pre_selected_document_issuing_country"]); + $this->assertSame("Travel Document", $decoded["pre_selected_document_type"]); + $this->assertCount(4, $decoded); + } + + public function testIdentityVerificationAttemptRequestSerializesPhoneNumberAndClientInformation() + { + $request = new IdentityVerificationAttemptRequest(); + $request->redirect_url = "https://example.com/success"; + $request->phone_number = $this->buildPhoneNumber(); + $request->client_information = $this->buildIdentityVerificationClientInformation(); + + $decoded = json_decode($this->serializer->serialize($request), true); + + $this->assertSame("https://example.com/success", $decoded["redirect_url"]); + $this->assertSame("+33", $decoded["phone_number"]["country_code"]); + $this->assertSame("5555550102", $decoded["phone_number"]["number"]); + $this->assertSame("GB", $decoded["client_information"]["pre_selected_document_issuing_country"]); + $this->assertSame("Travel Document", $decoded["client_information"]["pre_selected_document_type"]); + } + + public function testIdentityVerificationAttemptRequestFromSwaggerExample() + { + $json = '{' + . '"redirect_url":"https://example.com/success",' + . '"phone_number":{"country_code":"+33","number":"5555550102"},' + . '"client_information":{' + . '"pre_selected_residence_country":"FR",' + . '"pre_selected_document_issuing_country":"GB",' + . '"pre_selected_document_type":"Passport",' + . '"pre_selected_language":"en-US"' + . '}' + . '}'; + + $decoded = $this->serializer->deserialize($json); + + $this->assertSame("https://example.com/success", $decoded["redirect_url"]); + $this->assertSame("+33", $decoded["phone_number"]["country_code"]); + $this->assertSame("Passport", $decoded["client_information"]["pre_selected_document_type"]); + } + + public function testFaceAuthenticationAttemptRequestSerializesPhoneNumber() + { + $clientInformation = new ClientInformation(); + $clientInformation->pre_selected_residence_country = "FR"; + + $request = new FaceAuthenticationAttemptRequest(); + $request->redirect_url = "https://example.com/success"; + $request->phone_number = $this->buildPhoneNumber(); + $request->client_information = $clientInformation; + + $decoded = json_decode($this->serializer->serialize($request), true); + + $this->assertSame("+33", $decoded["phone_number"]["country_code"]); + $this->assertSame("FR", $decoded["client_information"]["pre_selected_residence_country"]); + $this->assertArrayNotHasKey("pre_selected_document_type", $decoded["client_information"]); + $this->assertArrayNotHasKey( + "pre_selected_document_issuing_country", + $decoded["client_information"] + ); + } + + public function testIdentityVerificationRequestCarriesTheIdentityDeclaredDataFields() + { + $request = new IdentityVerificationRequest(); + $request->applicant_id = "aplt_tkoi5db4hryu5cei5vwoabr7we"; + $request->user_journey_id = "usj_tkoi5db4hryu5cei5vwoabr7we"; + $request->declared_data = $this->buildIdentityDeclaredData(); + + $decoded = json_decode($this->serializer->serialize($request), true); + + $this->assertSame("aplt_tkoi5db4hryu5cei5vwoabr7we", $decoded["applicant_id"]); + $this->assertSame("usj_tkoi5db4hryu5cei5vwoabr7we", $decoded["user_journey_id"]); + $this->assertSame("hannah.bret@example.com", $decoded["declared_data"]["email"]); + $this->assertSame("+33", $decoded["declared_data"]["phone_number"]["country_code"]); + $this->assertSame("GB", $decoded["declared_data"]["address"]["country"]); + } + + public function testIdentityVerificationAndOpenRequestCarriesTheIdentityDeclaredDataFields() + { + $request = new IdentityVerificationAndOpenRequest(); + $request->applicant_id = "aplt_tkoi5db4hryu5cei5vwoabr7we"; + $request->user_journey_id = "usj_tkoi5db4hryu5cei5vwoabr7we"; + $request->redirect_url = "https://example.com/success"; + $request->declared_data = $this->buildIdentityDeclaredData(); + + $decoded = json_decode($this->serializer->serialize($request), true); + + $this->assertSame("https://example.com/success", $decoded["redirect_url"]); + $this->assertSame("1994-10-15", $decoded["declared_data"]["birth_date"]); + $this->assertSame("London", $decoded["declared_data"]["address"]["city"]); + } + + public function testAttemptsQueryFilterEncodesSkipAndLimit() + { + $query = new AttemptsQueryFilter(); + $query->skip = 5; + $query->limit = 25; + + $this->assertSame("skip=5&limit=25", $query->getEncodedQueryParameters()); + } + + public function testAttemptsQueryFilterEncodesLimitOnly() + { + $query = new AttemptsQueryFilter(); + $query->limit = 25; + + $this->assertSame("limit=25", $query->getEncodedQueryParameters()); + } + + public function testEmptyAttemptsQueryFilterEncodesToAnEmptyString() + { + $this->assertSame("", (new AttemptsQueryFilter())->getEncodedQueryParameters()); + } + + public function testAttemptAssetsQueryFilterEncodesSkipAndLimit() + { + $query = new AttemptAssetsQueryFilter(); + $query->skip = 2; + $query->limit = 50; + + $this->assertSame("skip=2&limit=50", $query->getEncodedQueryParameters()); + } + + private function buildPhoneNumber(): PhoneNumber + { + $phoneNumber = new PhoneNumber(); + $phoneNumber->country_code = "+33"; + $phoneNumber->number = "5555550102"; + + return $phoneNumber; + } + + private function buildIdentityDeclaredData(): IdentityDeclaredData + { + $address = new IdvAddress(); + $address->address_line1 = "123 Main Street"; + $address->city = "London"; + $address->zip = "SW1A 1AA"; + $address->country = "GB"; + + $declaredData = new IdentityDeclaredData(); + $declaredData->name = "Hannah Bret"; + $declaredData->birth_date = "1994-10-15"; + $declaredData->email = "hannah.bret@example.com"; + $declaredData->phone_number = $this->buildPhoneNumber(); + $declaredData->address = $address; + + return $declaredData; + } + + private function buildIdentityVerificationClientInformation(): IdentityVerificationClientInformation + { + $clientInformation = new IdentityVerificationClientInformation(); + $clientInformation->pre_selected_residence_country = "FR"; + $clientInformation->pre_selected_language = "en-US"; + $clientInformation->pre_selected_document_issuing_country = "GB"; + $clientInformation->pre_selected_document_type = "Travel Document"; + + return $clientInformation; + } +} diff --git a/test/Checkout/Tests/Identities/IdentityVerification/IdentityVerificationClientTest.php b/test/Checkout/Tests/Identities/IdentityVerification/IdentityVerificationClientTest.php index d218c940..31f5925e 100644 --- a/test/Checkout/Tests/Identities/IdentityVerification/IdentityVerificationClientTest.php +++ b/test/Checkout/Tests/Identities/IdentityVerification/IdentityVerificationClientTest.php @@ -7,8 +7,10 @@ use Checkout\CheckoutAuthorizationException; use Checkout\CheckoutException; use Checkout\Identities\Entities\AttemptAssetsQueryFilter; -use Checkout\Identities\Entities\DeclaredData; -use Checkout\Identities\Entities\ClientInformation; +use Checkout\Identities\Entities\AttemptsQueryFilter; +use Checkout\Identities\Entities\IdentityDeclaredData; +use Checkout\Identities\Entities\IdentityVerificationClientInformation; +use Checkout\Identities\Entities\PhoneNumber; use Checkout\Identities\IdentityVerification\IdentityVerificationClient; use Checkout\Identities\IdentityVerification\Requests\IdentityVerificationRequest; use Checkout\Identities\IdentityVerification\Requests\IdentityVerificationAndOpenRequest; @@ -238,7 +240,7 @@ public function shouldGetIdentityVerificationAttempts() $expectedResponse = $this->buildExpectedIdentityVerificationAttemptsResponse(); $this->apiClient - ->method("get") + ->method("query") ->willReturn($expectedResponse); $response = $this->client->getIdentityVerificationAttempts("idv_test123"); @@ -258,7 +260,7 @@ public function shouldCallCorrectApiEndpointForGetIdentityVerificationAttempts() $this->apiClient ->expects($this->once()) - ->method("get") + ->method("query") ->with("identity-verifications/" . $identityVerificationId . "/attempts") ->willReturn($expectedResponse); @@ -386,10 +388,35 @@ public function shouldCallCorrectApiEndpointForGetIdentityVerificationAttemptAss $this->assertNotNull($response); } + /** + * @test + * @throws CheckoutApiException + */ + public function shouldGetIdentityVerificationAttemptsWithPagination() + { + $identityVerificationId = "idv_test123"; + $expectedResponse = $this->buildExpectedIdentityVerificationAttemptsResponse(); + + $query = new AttemptsQueryFilter(); + $query->skip = 5; + $query->limit = 25; + + $this->apiClient + ->expects($this->once()) + ->method("query") + ->with("identity-verifications/" . $identityVerificationId . "/attempts", $query) + ->willReturn($expectedResponse); + + $response = $this->client->getIdentityVerificationAttempts($identityVerificationId, $query); + + $this->assertNotNull($response); + $this->assertSame("skip=5&limit=25", $query->getEncodedQueryParameters()); + } + // Request builders private function buildIdentityVerificationAndOpenRequest() { - $declared_data = new DeclaredData(); + $declared_data = new IdentityDeclaredData(); $declared_data->name = "John Doe"; $request = new IdentityVerificationAndOpenRequest(); @@ -403,7 +430,7 @@ private function buildIdentityVerificationAndOpenRequest() private function buildIdentityVerificationRequest() { - $declared_data = new DeclaredData(); + $declared_data = new IdentityDeclaredData(); $declared_data->name = "Jane Smith"; $request = new IdentityVerificationRequest(); @@ -416,12 +443,19 @@ private function buildIdentityVerificationRequest() private function buildIdentityVerificationAttemptRequest() { - $client_information = new ClientInformation(); + $client_information = new IdentityVerificationClientInformation(); $client_information->pre_selected_residence_country = "GB"; $client_information->pre_selected_language = "en"; + $client_information->pre_selected_document_issuing_country = "GB"; + $client_information->pre_selected_document_type = "Passport"; + + $phone_number = new PhoneNumber(); + $phone_number->country_code = "+33"; + $phone_number->number = "5555550102"; $request = new IdentityVerificationAttemptRequest(); $request->redirect_url = "https://example.com/success"; + $request->phone_number = $phone_number; $request->client_information = $client_information; return $request; @@ -487,18 +521,31 @@ private function buildExpectedIdentityVerificationAttemptResponse() private function buildExpectedIdentityVerificationAttemptsResponse() { return [ - "count" => 2, - "attempts" => [ + "total_count" => 2, + "skip" => 0, + "limit" => 10, + "data" => [ [ "id" => "att_test456", - "identity_verification_id" => "idv_test123", "status" => "pending", + "redirect_url" => "https://verify.checkout.com/att_test456", + "response_codes" => [], + "phone_number" => [ + "country_code" => "+33", + "number" => "1234567890" + ], + "client_information" => [ + "pre_selected_residence_country" => "GB", + "pre_selected_document_issuing_country" => "GB", + "pre_selected_document_type" => "Passport" + ], "created_on" => "2023-03-15T10:35:00Z" ], [ "id" => "att_test789", - "identity_verification_id" => "idv_test123", "status" => "completed", + "redirect_url" => "https://verify.checkout.com/att_test789", + "response_codes" => [], "created_on" => "2023-03-15T11:00:00Z" ] ], @@ -558,9 +605,14 @@ private function validateIdentityVerificationAttemptResponse($response) private function validateIdentityVerificationAttemptsResponse($response) { - $this->assertNotNull($response["count"]); - $this->assertTrue(is_array($response["attempts"])); - $this->assertGreaterThanOrEqual(0, $response["count"]); + $this->assertNotNull($response["total_count"]); + $this->assertArrayHasKey("skip", $response); + $this->assertArrayHasKey("limit", $response); + $this->assertTrue(is_array($response["data"])); + $this->assertGreaterThanOrEqual(0, $response["total_count"]); + $this->assertSame("+33", $response["data"][0]["phone_number"]["country_code"]); + $this->assertSame("GB", $response["data"][0]["client_information"]["pre_selected_document_issuing_country"]); + $this->assertSame("Passport", $response["data"][0]["client_information"]["pre_selected_document_type"]); } private function validateIdentityVerificationPdfReportResponse($response) diff --git a/test/Checkout/Tests/Identities/IdentityVerification/IdentityVerificationIntegrationTest.php b/test/Checkout/Tests/Identities/IdentityVerification/IdentityVerificationIntegrationTest.php index 6edb98a0..82b0ee01 100644 --- a/test/Checkout/Tests/Identities/IdentityVerification/IdentityVerificationIntegrationTest.php +++ b/test/Checkout/Tests/Identities/IdentityVerification/IdentityVerificationIntegrationTest.php @@ -6,8 +6,11 @@ use Checkout\CheckoutArgumentException; use Checkout\CheckoutAuthorizationException; use Checkout\CheckoutException; -use Checkout\Identities\Entities\DeclaredData; -use Checkout\Identities\Entities\ClientInformation; +use Checkout\Identities\Entities\AttemptsQueryFilter; +use Checkout\Identities\Entities\IdentityDeclaredData; +use Checkout\Identities\Entities\IdentityVerificationClientInformation; +use Checkout\Identities\Entities\IdvAddress; +use Checkout\Identities\Entities\PhoneNumber; use Checkout\Identities\IdentityVerification\Requests\IdentityVerificationRequest; use Checkout\Identities\IdentityVerification\Requests\IdentityVerificationAndOpenRequest; use Checkout\Identities\IdentityVerification\Requests\IdentityVerificationAttemptRequest; @@ -262,9 +265,33 @@ public function shouldPerformSeparateCreateAndAttemptWorkflow() } // Request builders + /** + * @test + * @throws CheckoutApiException + */ + public function shouldGetIdentityVerificationAttemptsWithPagination() + { + $this->markTestSkipped("This test requires valid test environment setup"); + + $created = $this->checkoutApi->getIdentityVerificationClient() + ->createIdentityVerificationAndAttempt($this->buildIdentityVerificationAndOpenRequest()); + + $query = new AttemptsQueryFilter(); + $query->skip = 0; + $query->limit = 1; + + $response = $this->checkoutApi->getIdentityVerificationClient() + ->getIdentityVerificationAttempts($created["id"], $query); + + $this->assertNotNull($response); + $this->assertLessThanOrEqual(1, count($response["data"])); + $this->assertEquals(1, $response["limit"]); + $this->assertEquals(0, $response["skip"]); + } + private function buildIdentityVerificationAndOpenRequest() { - $declared_data = new DeclaredData(); + $declared_data = new IdentityDeclaredData(); $declared_data->name = "John Doe " . $this->generateRandomString(); $request = new IdentityVerificationAndOpenRequest(); @@ -278,8 +305,22 @@ private function buildIdentityVerificationAndOpenRequest() private function buildIdentityVerificationRequest() { - $declared_data = new DeclaredData(); + $address = new IdvAddress(); + $address->address_line1 = "123 Main Street"; + $address->city = "London"; + $address->zip = "SW1A 1AA"; + $address->country = "GB"; + + $phone_number = new PhoneNumber(); + $phone_number->country_code = "+44"; + $phone_number->number = "7700900000"; + + $declared_data = new IdentityDeclaredData(); $declared_data->name = "Jane Smith " . $this->generateRandomString(); + $declared_data->birth_date = "1994-10-15"; + $declared_data->email = "jane.smith@example.com"; + $declared_data->phone_number = $phone_number; + $declared_data->address = $address; $request = new IdentityVerificationRequest(); $request->applicant_id = "applicant_" . $this->generateRandomString(); @@ -291,12 +332,19 @@ private function buildIdentityVerificationRequest() private function buildIdentityVerificationAttemptRequest() { - $client_information = new ClientInformation(); + $client_information = new IdentityVerificationClientInformation(); $client_information->pre_selected_residence_country = "GB"; $client_information->pre_selected_language = "en"; + $client_information->pre_selected_document_issuing_country = "GB"; + $client_information->pre_selected_document_type = "Passport"; + + $phone_number = new PhoneNumber(); + $phone_number->country_code = "+44"; + $phone_number->number = "7700900000"; $request = new IdentityVerificationAttemptRequest(); $request->redirect_url = "https://example.com/success?session=" . $this->generateRandomString(); + $request->phone_number = $phone_number; $request->client_information = $client_information; return $request; @@ -381,13 +429,15 @@ private function validateCreatedIdentityVerificationAttempt($response, $request) private function validateRetrievedIdentityVerificationAttempts($response, $createdAttempt) { $this->assertNotNull($response); - $this->assertNotNull($response["count"]); - $this->assertTrue(is_array($response["attempts"])); - $this->assertGreaterThanOrEqual(1, $response["count"]); - + $this->assertNotNull($response["total_count"]); + $this->assertArrayHasKey("skip", $response); + $this->assertArrayHasKey("limit", $response); + $this->assertTrue(is_array($response["data"])); + $this->assertGreaterThanOrEqual(1, $response["total_count"]); + // Find the created attempt in the list $found = false; - foreach ($response["attempts"] as $attempt) { + foreach ($response["data"] as $attempt) { if ($attempt["id"] === $createdAttempt["id"]) { $found = true; break; diff --git a/test/Checkout/Tests/Issuing/Cards/CardScheduledActivationDateSerializationTest.php b/test/Checkout/Tests/Issuing/Cards/CardScheduledActivationDateSerializationTest.php new file mode 100644 index 00000000..16547b4b --- /dev/null +++ b/test/Checkout/Tests/Issuing/Cards/CardScheduledActivationDateSerializationTest.php @@ -0,0 +1,120 @@ +serializer = new JsonSerializer(); + } + + public function testVirtualCardRequestSerializesScheduledActivationDate() + { + $request = new VirtualCardRequest(); + $request->cardholder_id = "crh_d3ozhf43pcq2xbldn2g45qnb44"; + $request->card_product_id = "pro_3fn5gcelkktzzhfu4n22tz2mjq"; + $request->reference = "X-123456-N11"; + $request->scheduled_activation_date = "2026-06-01T10:00Z"; + + $decoded = json_decode($this->serializer->serialize($request), true); + + $this->assertSame("2026-06-01T10:00Z", $decoded["scheduled_activation_date"]); + $this->assertArrayNotHasKey("activation_date", $decoded); + } + + public function testPhysicalCardRequestSerializesScheduledActivationDate() + { + $request = new PhysicalCardRequest(); + $request->cardholder_id = "crh_d3ozhf43pcq2xbldn2g45qnb44"; + $request->card_product_id = "pro_3fn5gcelkktzzhfu4n22tz2mjq"; + $request->scheduled_activation_date = "2026-06-01"; + + $decoded = json_decode($this->serializer->serialize($request), true); + + $this->assertSame("2026-06-01", $decoded["scheduled_activation_date"]); + $this->assertArrayNotHasKey("activation_date", $decoded); + } + + public function testUpdateCardRequestSerializesScheduledActivationDate() + { + $request = new UpdateCardRequest(); + $request->scheduled_activation_date = "2026-06-01T10:00Z"; + + $decoded = json_decode($this->serializer->serialize($request), true); + + $this->assertSame(["scheduled_activation_date"], array_keys($decoded)); + $this->assertArrayNotHasKey("activation_date", $decoded); + } + + public function testUpdateCardRequestSerializesRevocationDate() + { + $request = new UpdateCardRequest(); + $request->revocation_date = "2027-03-12"; + + $decoded = json_decode($this->serializer->serialize($request), true); + + $this->assertSame("2027-03-12", $decoded["revocation_date"]); + } + + public function testUpdateCardRequestSerializesEveryProperty() + { + $request = new UpdateCardRequest(); + $request->reference = "X-123456-N11"; + $request->expiry_month = 6; + $request->expiry_year = 2030; + $request->scheduled_activation_date = "2026-06-01T10:00Z"; + $request->revocation_date = "2027-03-12"; + + $decoded = json_decode($this->serializer->serialize($request), true); + + $this->assertSame("X-123456-N11", $decoded["reference"]); + $this->assertSame(6, $decoded["expiry_month"]); + $this->assertSame(2030, $decoded["expiry_year"]); + $this->assertSame("2026-06-01T10:00Z", $decoded["scheduled_activation_date"]); + $this->assertSame("2027-03-12", $decoded["revocation_date"]); + } + + public function testUpdateCardRequestRoundTrip() + { + $request = new UpdateCardRequest(); + $request->scheduled_activation_date = "2026-06-01T10:00Z"; + $request->revocation_date = "2027-03-12"; + + $json = $this->serializer->serialize($request); + $decoded = $this->serializer->deserialize($json); + + $this->assertSame(json_decode($json, true), $decoded); + $this->assertSame("2026-06-01T10:00Z", $decoded["scheduled_activation_date"]); + } + + public function testUpdateCardRequestFromSwaggerExample() + { + $json = '{' + . '"reference":"X-123456-N11",' + . '"expiry_month":6,' + . '"expiry_year":2030,' + . '"revocation_date":"2027-03-12",' + . '"scheduled_activation_date":"2026-06-01T10:00Z"' + . '}'; + + $decoded = $this->serializer->deserialize($json); + + $this->assertSame("2027-03-12", $decoded["revocation_date"]); + $this->assertSame("2026-06-01T10:00Z", $decoded["scheduled_activation_date"]); + } +} diff --git a/test/Checkout/Tests/Issuing/Cards/CardUpdateHeadersTest.php b/test/Checkout/Tests/Issuing/Cards/CardUpdateHeadersTest.php new file mode 100644 index 00000000..2ab0aa6e --- /dev/null +++ b/test/Checkout/Tests/Issuing/Cards/CardUpdateHeadersTest.php @@ -0,0 +1,192 @@ +initMocks(PlatformType::$default); + $this->client = new IssuingClient($this->apiClient, $this->configuration); + } + + /** + * @test + */ + public function shouldMapBothPropertiesToTheirExactHeaderNames() + { + $mappings = (new CardUpdateHeaders())->getHeaderMappings(); + + $this->assertSame( + [ + "return_encrypted_cvv" => "return-encrypted-cvv", + "encryption_key" => "Encryption-Key" + ], + $mappings + ); + } + + /** + * The header names are case sensitive and neither matches the default snake_case to + * Pascal-Case conversion, which would emit Return-Encrypted-Cvv and Encryption-Key. + * + * @test + * @throws CheckoutAuthorizationException + */ + public function shouldBuildTheExactHeaderNamesOnTheWire() + { + $headers = new CardUpdateHeaders(); + $headers->return_encrypted_cvv = "true"; + $headers->encryption_key = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A"; + + $built = $this->buildRequestHeaders($headers); + + $this->assertArrayHasKey("return-encrypted-cvv", $built); + $this->assertArrayNotHasKey("Return-Encrypted-Cvv", $built); + $this->assertSame("true", $built["return-encrypted-cvv"]); + $this->assertArrayHasKey("Encryption-Key", $built); + $this->assertSame("MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A", $built["Encryption-Key"]); + } + + /** + * @test + * @throws CheckoutAuthorizationException + */ + public function shouldOmitUnsetHeaders() + { + $built = $this->buildRequestHeaders(new CardUpdateHeaders()); + + $this->assertArrayNotHasKey("return-encrypted-cvv", $built); + $this->assertArrayNotHasKey("Encryption-Key", $built); + } + + /** + * @test + * @throws CheckoutAuthorizationException + */ + public function shouldSendTheEncryptionKeyWithoutTheCvvFlag() + { + $headers = new CardUpdateHeaders(); + $headers->encryption_key = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A"; + + $built = $this->buildRequestHeaders($headers); + + $this->assertArrayNotHasKey("return-encrypted-cvv", $built); + $this->assertSame("MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A", $built["Encryption-Key"]); + } + + /** + * @test + * @throws CheckoutApiException + */ + public function shouldForwardTheHeadersToThePatchCall() + { + $request = new UpdateCardRequest(); + $request->reference = "X-123456-N11"; + + $headers = new CardUpdateHeaders(); + $headers->return_encrypted_cvv = "true"; + $headers->encryption_key = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A"; + + $this->apiClient + ->expects($this->once()) + ->method("patch") + ->with( + $this->equalTo("issuing/cards/crd_12345"), + $this->equalTo($request), + $this->anything(), + $this->equalTo($headers) + ) + ->willReturn($this->buildExpectedUpdateCardResponse()); + + $response = $this->client->updateCardDetails("crd_12345", $request, $headers); + + $this->assertNotNull($response); + $this->assertSame("oJMoNMEEUiQKYOsQ4Zd", $response["encrypted_cvv"]); + $this->assertArrayHasKey("last_modified_date", $response); + } + + /** + * @test + * @throws CheckoutApiException + */ + public function shouldUpdateCardDetailsWithoutHeaders() + { + $request = new UpdateCardRequest(); + + $this->apiClient + ->expects($this->once()) + ->method("patch") + ->with( + $this->equalTo("issuing/cards/crd_12345"), + $this->equalTo($request), + $this->anything(), + $this->equalTo(null) + ) + ->willReturn(["last_modified_date" => "2026-06-01T10:00:00Z"]); + + $response = $this->client->updateCardDetails("crd_12345", $request); + + $this->assertNotNull($response); + $this->assertArrayNotHasKey("encrypted_cvv", $response); + } + + /** + * Exercises the private header builder on a real ApiClient, so the assertions pin the names + * that actually reach the wire rather than the mapping declaration alone. + * + * @param CardUpdateHeaders $headers + * @return array + * @throws CheckoutAuthorizationException + */ + private function buildRequestHeaders(CardUpdateHeaders $headers): array + { + $apiClient = new ApiClient($this->configuration); + + $method = new ReflectionMethod(ApiClient::class, "getHeaders"); + $method->setAccessible(true); + + return $method->invoke( + $apiClient, + new SdkAuthorization($this->platformType, "key"), + "application/json", + null, + $headers + ); + } + + private function buildExpectedUpdateCardResponse(): array + { + return [ + "last_modified_date" => "2026-06-01T10:00:00Z", + "encrypted_cvv" => "oJMoNMEEUiQKYOsQ4Zd", + "_links" => [ + "self" => ["href" => "https://api.checkout.com/issuing/cards/crd_12345"] + ] + ]; + } +} diff --git a/test/Checkout/Tests/Issuing/Cards/CardsIntegrationTest.php b/test/Checkout/Tests/Issuing/Cards/CardsIntegrationTest.php index 330de875..d36810d5 100644 --- a/test/Checkout/Tests/Issuing/Cards/CardsIntegrationTest.php +++ b/test/Checkout/Tests/Issuing/Cards/CardsIntegrationTest.php @@ -14,6 +14,10 @@ use Checkout\Issuing\Cards\Revoke\RevokeReason; use Checkout\Issuing\Cards\Suspend\SuspendCardRequest; use Checkout\Issuing\Cards\Suspend\SuspendReason; +use Checkout\Issuing\Cards\Create\CardLifetime; +use Checkout\Issuing\Cards\Create\LifetimeUnit; +use Checkout\Issuing\Cards\Create\VirtualCardRequest; +use Checkout\Issuing\Cards\Update\CardUpdateHeaders; use Checkout\Issuing\Cards\Update\UpdateCardRequest; use Checkout\Issuing\Cards\Renew\RenewCardRequest; use Checkout\Tests\Issuing\AbstractIssuingIntegrationTest; @@ -239,6 +243,110 @@ public function shouldUpdateCardDetails() ); } + /** + * @test + * @throws CheckoutApiException + */ + public function shouldCreateCardWithScheduledActivationDate() + { + $lifetime = new CardLifetime(); + $lifetime->unit = LifetimeUnit::$months; + $lifetime->value = 6; + + $cardRequest = new VirtualCardRequest(); + $cardRequest->cardholder_id = $this->cardholder["id"]; + $cardRequest->card_product_id = "pro_3fn6pv2ikshurn36dbd3iysyha"; + $cardRequest->lifetime = $lifetime; + $cardRequest->reference = "X-123456-N11"; + $cardRequest->display_name = "John Kennedy"; + $cardRequest->is_single_use = false; + $cardRequest->scheduled_activation_date = $this->nextRoundHour(); + + $cardResponse = $this->issuingApi->getIssuingClient()->createCard($cardRequest); + + $this->assertResponse($cardResponse, "id"); + } + + /** + * @test + * @throws CheckoutApiException + */ + public function shouldUpdateCardScheduledActivationDate() + { + $card = $this->createCard($this->cardholder["id"]); + + $updateRequest = new UpdateCardRequest(); + $updateRequest->scheduled_activation_date = $this->nextRoundHour(); + + $updateResponse = $this->issuingApi->getIssuingClient()->updateCardDetails($card["id"], $updateRequest); + + $this->assertEquals(200, $updateResponse["http_metadata"]->getStatusCode()); + $this->assertResponse($updateResponse, "last_modified_date"); + } + + /** + * @test + * @throws CheckoutApiException + */ + public function shouldUpdateCardRevocationDate() + { + $card = $this->createCard($this->cardholder["id"]); + + $updateRequest = new UpdateCardRequest(); + $updateRequest->revocation_date = gmdate("Y-m-d", strtotime("+1 year")); + + $updateResponse = $this->issuingApi->getIssuingClient()->updateCardDetails($card["id"], $updateRequest); + + $this->assertEquals(200, $updateResponse["http_metadata"]->getStatusCode()); + $this->assertResponse($updateResponse, "last_modified_date"); + } + + /** + * @test + * @throws CheckoutApiException + */ + public function shouldUpdateCardDetailsReturningEncryptedCvv() + { + $card = $this->createCard($this->cardholder["id"], true); + + $updateRequest = new UpdateCardRequest(); + $updateRequest->reference = "UPDATED-REF-123"; + + $headers = new CardUpdateHeaders(); + $headers->return_encrypted_cvv = "true"; + $headers->encryption_key = $this->getEncryptionKey(); + + $updateResponse = $this->issuingApi->getIssuingClient() + ->updateCardDetails($card["id"], $updateRequest, $headers); + + $this->assertEquals(200, $updateResponse["http_metadata"]->getStatusCode()); + $this->assertResponse($updateResponse, "last_modified_date", "encrypted_cvv"); + $this->assertNotEmpty($updateResponse["encrypted_cvv"]); + } + + /** + * The next round hour in UTC, which is the earliest value the API accepts for a scheduled + * activation date carrying a time. + * + * @return string + */ + private function nextRoundHour(): string + { + return gmdate("Y-m-d\\TH:00\\Z", strtotime("+2 hours")); + } + + /** + * The RSA public key used to encrypt returned credentials, with the PEM headers and newlines + * removed. Supplied by the environment because it pairs with a private key the test cannot + * hold. + * + * @return string + */ + private function getEncryptionKey(): string + { + return getenv("CHECKOUT_ISSUING_ENCRYPTION_KEY") ?: ""; + } + /** * @test * @throws CheckoutApiException From ccc4461564c962298c256330ee8a965393a21b04 Mon Sep 17 00:00:00 2001 From: david ruiz Date: Fri, 18 Sep 2026 15:15:24 +0200 Subject: [PATCH 2/2] Cooments update + test extension --- .../AddressDocumentVerificationClient.php | 9 +- .../Identities/Entities/DeclaredData.php | 5 +- .../Entities/IdentityDeclaredData.php | 4 +- .../Identities/Entities/IdvAddress.php | 6 + .../FaceAuthenticationClient.php | 9 +- .../IdDocumentVerificationClient.php | 10 +- .../IdentityVerificationClient.php | 10 +- .../Issuing/Cards/Create/CardRequest.php | 8 +- .../Cards/Update/UpdateCardRequest.php | 5 + .../AddressDocumentVerificationClientTest.php | 124 +++++++-- .../Identities/AttemptsPaginationWireTest.php | 259 ++++++++++++++++++ .../IdDocumentVerificationClientTest.php | 145 +++++++--- .../IdentitiesSerializationTest.php | 26 ++ .../Issuing/Cards/CardUpdateHeadersTest.php | 104 +++++++ 14 files changed, 658 insertions(+), 66 deletions(-) create mode 100644 test/Checkout/Tests/Identities/AttemptsPaginationWireTest.php diff --git a/lib/Checkout/Identities/AddressDocumentVerification/AddressDocumentVerificationClient.php b/lib/Checkout/Identities/AddressDocumentVerification/AddressDocumentVerificationClient.php index a701a72a..d3c51781 100644 --- a/lib/Checkout/Identities/AddressDocumentVerification/AddressDocumentVerificationClient.php +++ b/lib/Checkout/Identities/AddressDocumentVerification/AddressDocumentVerificationClient.php @@ -99,6 +99,10 @@ public function createAddressDocumentVerificationAttempt( } /** + * Get the details of all attempts for a specific address document verification. + * Results are paginated. Use the skip and limit query parameters to page through them. + * Beta. + * * addressDocumentVerificationId is the address document verification's unique identifier. (Required) * * @param string $addressDocumentVerificationId @@ -165,10 +169,11 @@ public function getAddressDocumentVerificationReport(string $addressDocumentVeri } /** - * Retrieve the assets (the document image) uploaded for an address document verification attempt. + * Get the assets (the document image) uploaded for an address document verification attempt. + * Results are paginated. Use the skip and limit query parameters to page through them. * Beta. * - * $addressDocumentVerificationId is the address document verification's unique identifier. (Required) + * addressDocumentVerificationId is the address document verification's unique identifier. (Required) * attemptId is the attempt's unique identifier. (Required) * * @param string $addressDocumentVerificationId diff --git a/lib/Checkout/Identities/Entities/DeclaredData.php b/lib/Checkout/Identities/Entities/DeclaredData.php index 1116525f..71daade7 100644 --- a/lib/Checkout/Identities/Entities/DeclaredData.php +++ b/lib/Checkout/Identities/Entities/DeclaredData.php @@ -14,10 +14,9 @@ class DeclaredData public $name; /** - * The applicant's birth date, as provided by the applicant. This is the declared value, not - * the value extracted from the verified document. + * The applicant's birth date. * [Optional] - * Format: date (YYYY-MM-DD) + * Format: YYYY-MM-DD * Example: 1994-10-15 * @var string|null */ diff --git a/lib/Checkout/Identities/Entities/IdentityDeclaredData.php b/lib/Checkout/Identities/Entities/IdentityDeclaredData.php index c51d6dd2..47c23724 100644 --- a/lib/Checkout/Identities/Entities/IdentityDeclaredData.php +++ b/lib/Checkout/Identities/Entities/IdentityDeclaredData.php @@ -16,9 +16,11 @@ class IdentityDeclaredData extends DeclaredData public $phone_number; /** - * The applicant's email address. + * The applicant's email address. Explicitly nullable in the spec, so the API may return null + * for it rather than omitting it. * [Optional] * Format: email + * Nullable: true * Example: hannah.bret@example.com * @var string|null */ diff --git a/lib/Checkout/Identities/Entities/IdvAddress.php b/lib/Checkout/Identities/Entities/IdvAddress.php index b10ccc75..87a3a1ea 100644 --- a/lib/Checkout/Identities/Entities/IdvAddress.php +++ b/lib/Checkout/Identities/Entities/IdvAddress.php @@ -8,6 +8,7 @@ class IdvAddress * The first line of the address. * [Optional] * max 250 characters + * Example: 123 Main Street * @var string|null */ public $address_line1; @@ -16,6 +17,7 @@ class IdvAddress * The second line of the address. * [Optional] * max 250 characters + * Example: Apt 4B * @var string|null */ public $address_line2; @@ -24,6 +26,7 @@ class IdvAddress * The city or town. * [Optional] * max 50 characters + * Example: London * @var string|null */ public $city; @@ -32,6 +35,7 @@ class IdvAddress * The state, county, or province. * [Optional] * max 50 characters + * Example: Greater London * @var string|null */ public $state; @@ -40,6 +44,7 @@ class IdvAddress * The postal or ZIP code. * [Optional] * max 50 characters + * Example: SW1A 1AA * @var string|null */ public $zip; @@ -49,6 +54,7 @@ class IdvAddress * [Optional] * Standard: ISO 3166-1 alpha-2 country code * max 2 characters + * Example: GB * @var string|null values of Checkout\Common\CountryCode */ public $country; diff --git a/lib/Checkout/Identities/FaceAuthentication/FaceAuthenticationClient.php b/lib/Checkout/Identities/FaceAuthentication/FaceAuthenticationClient.php index fa8ac41d..a231f822 100644 --- a/lib/Checkout/Identities/FaceAuthentication/FaceAuthenticationClient.php +++ b/lib/Checkout/Identities/FaceAuthentication/FaceAuthenticationClient.php @@ -89,6 +89,10 @@ public function createFaceAuthenticationAttempt( } /** + * Get the details of all attempts for a specific face authentication. + * Results are paginated. Use the skip and limit query parameters to page through them. + * Beta. + * * faceAuthenticationId is the face authentication's unique identifier. (Required) * * @param string $faceAuthenticationId @@ -125,7 +129,10 @@ public function getFaceAuthenticationAttempt(string $faceAuthenticationId, strin } /** - * Retrieves the assets (face images and videos) captured during a face authentication attempt. + * Get the assets (face images and videos) captured during a face authentication attempt. + * Videos are not exposed by default; contact your account manager to enable them. + * Results are paginated. Use the skip and limit query parameters to page through them. + * Beta. * * faceAuthenticationId is the face authentication's unique identifier. (Required) * attemptId is the attempt's unique identifier. (Required) diff --git a/lib/Checkout/Identities/IdDocumentVerification/IdDocumentVerificationClient.php b/lib/Checkout/Identities/IdDocumentVerification/IdDocumentVerificationClient.php index a7c2f335..f41a92a0 100644 --- a/lib/Checkout/Identities/IdDocumentVerification/IdDocumentVerificationClient.php +++ b/lib/Checkout/Identities/IdDocumentVerification/IdDocumentVerificationClient.php @@ -90,6 +90,10 @@ public function createIdDocumentVerificationAttempt( } /** + * Get the details of all attempts for a specific ID document verification. + * Results are paginated. Use the skip and limit query parameters to page through them. + * Beta. + * * idDocumentVerificationId is the ID document verification's unique identifier. (Required) * * @param string $idDocumentVerificationId @@ -141,10 +145,12 @@ public function getIdDocumentVerificationReport(string $idDocumentVerificationId } /** - * Retrieve the assets (the front and back images of the document) uploaded for an ID document verification attempt. + * Get the assets (the front and back images of the document) uploaded for an ID document + * verification attempt. + * Results are paginated. Use the skip and limit query parameters to page through them. * Beta. * - * $idDocumentVerificationId is the ID document verification's unique identifier. (Required) + * idDocumentVerificationId is the ID document verification's unique identifier. (Required) * attemptId is the attempt's unique identifier. (Required) * * @param string $idDocumentVerificationId diff --git a/lib/Checkout/Identities/IdentityVerification/IdentityVerificationClient.php b/lib/Checkout/Identities/IdentityVerification/IdentityVerificationClient.php index b133bca6..7a289138 100644 --- a/lib/Checkout/Identities/IdentityVerification/IdentityVerificationClient.php +++ b/lib/Checkout/Identities/IdentityVerification/IdentityVerificationClient.php @@ -107,6 +107,10 @@ public function createIdentityVerificationAttempt( } /** + * Get all the attempts for a specific identity verification. + * Results are paginated. Use the skip and limit query parameters to page through them. + * Beta. + * * identityVerificationId is the identity verification's unique identifier. (Required) * * @param string $identityVerificationId @@ -158,7 +162,11 @@ public function getIdentityVerificationPdfReport(string $identityVerificationId) } /** - * Retrieves the assets (face images, videos, and document images) captured during an identity verification attempt. + * Get the assets (face images, videos, and document images) captured during an identity + * verification attempt. Videos are not exposed by default; contact your account manager to + * enable them. + * Results are paginated. Use the skip and limit query parameters to page through them. + * Beta. * * identityVerificationId is the identity verification's unique identifier. (Required) * attemptId is the attempt's unique identifier. (Required) diff --git a/lib/Checkout/Issuing/Cards/Create/CardRequest.php b/lib/Checkout/Issuing/Cards/Create/CardRequest.php index b273ca51..a6a3a181 100644 --- a/lib/Checkout/Issuing/Cards/Create/CardRequest.php +++ b/lib/Checkout/Issuing/Cards/Create/CardRequest.php @@ -12,7 +12,9 @@ protected function __construct($type) /** * The card type. * [Required] - * @var string value of CardType + * Enum: "virtual" "physical" + * Example: virtual + * @var string value of Checkout\Issuing\CardType */ public $type; @@ -21,6 +23,7 @@ protected function __construct($type) * [Required] * ^crh_[a-z0-9]{26}$ * min 30 characters, max 30 characters + * Example: crh_d3ozhf43pcq2xbldn2g45qnb44 * @var string */ public $cardholder_id; @@ -36,6 +39,7 @@ protected function __construct($type) * Your reference. * [Optional] * max 256 characters + * Example: X-123456-N11 * @var string */ public $reference; @@ -53,6 +57,7 @@ protected function __construct($type) * [Optional] * ^[0-9a-zA-Z.\- ]{2,26}$ * min 2 characters, max 26 characters + * Example: JOHN KENNEDY * @var string */ public $display_name; @@ -61,6 +66,7 @@ protected function __construct($type) * Sets whether to activate the newly created card upon creation. If set to false, the cardholder will * not be able to process transactions until you activate the card. * [Optional] + * Default: true * @var bool */ public $activate_card; diff --git a/lib/Checkout/Issuing/Cards/Update/UpdateCardRequest.php b/lib/Checkout/Issuing/Cards/Update/UpdateCardRequest.php index 507eadc1..1dcb08d3 100644 --- a/lib/Checkout/Issuing/Cards/Update/UpdateCardRequest.php +++ b/lib/Checkout/Issuing/Cards/Update/UpdateCardRequest.php @@ -10,6 +10,7 @@ class UpdateCardRequest * Your reference. * [Optional] * max 256 characters + * Example: X-123456-N11 * @var string */ public $reference; @@ -24,8 +25,10 @@ class UpdateCardRequest /** * The card's expiration month. * [Optional] + * Format: int32 * min 1 * max 12 + * Example: 5 * @var int */ public $expiry_month; @@ -33,7 +36,9 @@ class UpdateCardRequest /** * The card's expiration year. * [Optional] + * Format: int32 * min 4 characters, max 4 characters + * Example: 2025 * @var int */ public $expiry_year; diff --git a/test/Checkout/Tests/Identities/AddressDocumentVerification/AddressDocumentVerificationClientTest.php b/test/Checkout/Tests/Identities/AddressDocumentVerification/AddressDocumentVerificationClientTest.php index 50758f62..69fdb4b9 100644 --- a/test/Checkout/Tests/Identities/AddressDocumentVerification/AddressDocumentVerificationClientTest.php +++ b/test/Checkout/Tests/Identities/AddressDocumentVerification/AddressDocumentVerificationClientTest.php @@ -377,6 +377,67 @@ public function shouldCallCorrectApiEndpointForGetAddressDocumentVerificationAtt $this->assertNotNull($response); } + /** + * @test + * @throws CheckoutApiException + */ + public function shouldDeserializeTheAddressDocumentVerificationAttemptAssetsSwaggerExample() + { + $decoded = json_decode(self::ADV_ATTEMPT_ASSETS_SWAGGER_EXAMPLE, true); + + $this->apiClient + ->method("query") + ->willReturn($decoded); + + $response = $this->client->getAddressDocumentVerificationAttemptAssets( + "adv_tkoi5db4hryu5cei5vwoabr7we", + "adva_tkoi5db4hryu5cei5vwoabr7we" + ); + + $this->assertSame(1, $response["total_count"]); + $this->assertSame(0, $response["skip"]); + $this->assertSame(10, $response["limit"]); + $this->assertCount(1, $response["data"]); + $this->assertSame("document", $response["data"][0]["type"]); + $this->assertStringContainsString( + "address_document.png", + $response["data"][0]["_links"]["asset_url"]["href"] + ); + $this->assertArrayHasKey("self", $response["_links"]); + $this->assertArrayHasKey("next", $response["_links"]); + $this->assertArrayHasKey("previous", $response["_links"]); + } + + /** + * The data array declares minItems 0, so an attempt with no assets yet is a valid page. + * + * @test + * @throws CheckoutApiException + */ + public function shouldHandleAnEmptyAddressDocumentVerificationAttemptAssetsPage() + { + $this->apiClient + ->method("query") + ->willReturn([ + "total_count" => 0, + "skip" => 0, + "limit" => 10, + "data" => [], + "_links" => [ + "self" => [ + "href" => "https://api.checkout.com/address-document-verifications/" + . "adv_id/attempts/adva_id/assets" + ] + ] + ]); + + $response = $this->client->getAddressDocumentVerificationAttemptAssets("adv_id", "adva_id"); + + $this->assertSame(0, $response["total_count"]); + $this->assertSame([], $response["data"]); + $this->assertArrayHasKey("_links", $response); + } + private function buildAddressDocumentVerificationRequest(): AddressDocumentVerificationRequest { $declaredData = new DeclaredData(); @@ -439,30 +500,44 @@ private function buildExpectedAddressDocumentVerificationAttemptResponse(): arra ]; } + /** + * The swagger example for this response, verbatim, from + * components.examples.adv_attempt_assets_response_body. Kept as raw JSON rather than a PHP + * array so that a key renamed in the spec shows up as a test failure instead of being silently + * carried over from a hand-written fixture. + */ + private const ADV_ATTEMPT_ASSETS_SWAGGER_EXAMPLE = <<<'JSON' +{ + "total_count": 1, + "skip": 0, + "limit": 10, + "data": [ + { + "type": "document", + "_links": { + "asset_url": { + "href": "https://storage-b.env.ubble.ai/ubble-ai/NDYOOVHGZPAQ/a54b3393-f02a-47c9-a9c5-2f6ee73560e1/bb603e2f-5de9-40f2-9631-8285a33c24c0/address_document.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Expires=3600" + } + } + } + ], + "_links": { + "self": { + "href": "https://identity-verification.checkout.com/address-document-verifications/adv_tkoi5db4hryu5cei5vwoabr7we/attempts/adva_tkoi5db4hryu5cei5vwoabr7we/assets" + }, + "next": { + "href": "https://identity-verification.checkout.com/address-document-verifications/adv_tkoi5db4hryu5cei5vwoabr7we/attempts/adva_tkoi5db4hryu5cei5vwoabr7we/assets?..." + }, + "previous": { + "href": "https://identity-verification.checkout.com/address-document-verifications/adv_tkoi5db4hryu5cei5vwoabr7we/attempts/adva_tkoi5db4hryu5cei5vwoabr7we/assets?..." + } + } +} +JSON; + private function buildExpectedAddressDocumentVerificationAttemptAssetsResponse(): array { - return [ - "total_count" => 1, - "skip" => 0, - "limit" => 10, - "data" => [ - [ - "type" => "document", - "_links" => [ - "download" => [ - "href" => "https://api.checkout.com/address-document-verifications/" - . "adv_id/attempts/adva_id/assets/document" - ] - ] - ] - ], - "_links" => [ - "self" => [ - "href" => "https://api.checkout.com/address-document-verifications/" - . "adv_id/attempts/adva_id/assets" - ] - ] - ]; + return json_decode(self::ADV_ATTEMPT_ASSETS_SWAGGER_EXAMPLE, true); } private function validateAddressDocumentVerificationAttemptAssetsResponse(array $response): void @@ -473,7 +548,10 @@ private function validateAddressDocumentVerificationAttemptAssetsResponse(array $this->assertArrayHasKey("data", $response); $this->assertArrayHasKey("_links", $response); $this->assertSame("document", $response["data"][0]["type"]); - $this->assertArrayHasKey("download", $response["data"][0]["_links"]); + // asset_url is the only link the AdvAttemptAsset schema declares, and it is required. + $this->assertArrayHasKey("asset_url", $response["data"][0]["_links"]); + $this->assertArrayNotHasKey("download", $response["data"][0]["_links"]); + $this->assertNotEmpty($response["data"][0]["_links"]["asset_url"]["href"]); } private function buildExpectedAddressDocumentVerificationAttemptsResponse(): array diff --git a/test/Checkout/Tests/Identities/AttemptsPaginationWireTest.php b/test/Checkout/Tests/Identities/AttemptsPaginationWireTest.php new file mode 100644 index 00000000..c43d8703 --- /dev/null +++ b/test/Checkout/Tests/Identities/AttemptsPaginationWireTest.php @@ -0,0 +1,259 @@ +capturedRequest = null; + + $stack = HandlerStack::create(); + $stack->after('prepare_body', function (callable $handler) { + return function ($request, array $options) use ($handler) { + $this->capturedRequest = $request; + return $handler($request, $options); + }; + }, 'capture_request'); + $stack->setHandler(function () { + return \GuzzleHttp\Promise\Create::promiseFor( + new Response(200, ['Content-Type' => 'application/json'], '{"total_count":0,"data":[]}') + ); + }); + + $httpBuilder = $this->createMock(HttpClientBuilderInterface::class); + $httpBuilder->method('getClient')->willReturn(new \GuzzleHttp\Client(['handler' => $stack])); + + $sdkCredentials = $this->createMock(SdkCredentialsInterface::class); + $sdkCredentials + ->method('getAuthorization') + ->willReturn(new SdkAuthorization(PlatformType::$default, 'sk_sbox_key')); + + $logger = new Logger('checkout-sdk-test-php'); + $logger->pushHandler(new StreamHandler('php://stderr')); + + $this->configuration = new CheckoutConfiguration( + $sdkCredentials, + Environment::sandbox(), + $httpBuilder, + $logger + ); + $this->apiClient = new ApiClient($this->configuration); + } + + private function capturedUri(): string + { + $this->assertNotNull($this->capturedRequest, 'Expected the outgoing request to be captured'); + return (string)$this->capturedRequest->getUri(); + } + + private function paginationFilter(): AttemptsQueryFilter + { + $query = new AttemptsQueryFilter(); + $query->skip = 5; + $query->limit = 25; + + return $query; + } + + /** + * @test + * @throws CheckoutApiException + */ + public function shouldSendSkipAndLimitForAddressDocumentVerificationAttempts() + { + $client = new AddressDocumentVerificationClient($this->apiClient, $this->configuration); + + $client->getAddressDocumentVerificationAttempts("adv_123", $this->paginationFilter()); + + $this->assertStringEndsWith( + "address-document-verifications/adv_123/attempts?skip=5&limit=25", + $this->capturedUri() + ); + } + + /** + * @test + * @throws CheckoutApiException + */ + public function shouldSendSkipAndLimitForIdDocumentVerificationAttempts() + { + $client = new IdDocumentVerificationClient($this->apiClient, $this->configuration); + + $client->getIdDocumentVerificationAttempts("iddv_123", $this->paginationFilter()); + + $this->assertStringEndsWith( + "id-document-verifications/iddv_123/attempts?skip=5&limit=25", + $this->capturedUri() + ); + } + + /** + * @test + * @throws CheckoutApiException + */ + public function shouldSendSkipAndLimitForIdentityVerificationAttempts() + { + $client = new IdentityVerificationClient($this->apiClient, $this->configuration); + + $client->getIdentityVerificationAttempts("idv_123", $this->paginationFilter()); + + $this->assertStringEndsWith( + "identity-verifications/idv_123/attempts?skip=5&limit=25", + $this->capturedUri() + ); + } + + /** + * @test + * @throws CheckoutApiException + */ + public function shouldSendSkipAndLimitForFaceAuthenticationAttempts() + { + $client = new FaceAuthenticationClient($this->apiClient, $this->configuration); + + $client->getFaceAuthenticationAttempts("fav_123", $this->paginationFilter()); + + $this->assertStringEndsWith( + "face-authentications/fav_123/attempts?skip=5&limit=25", + $this->capturedUri() + ); + } + + /** + * @test + * @throws CheckoutApiException + */ + public function shouldSendSkipAndLimitForAttemptAssets() + { + $client = new AddressDocumentVerificationClient($this->apiClient, $this->configuration); + + $query = new AttemptAssetsQueryFilter(); + $query->skip = 2; + $query->limit = 50; + + $client->getAddressDocumentVerificationAttemptAssets("adv_123", "adva_123", $query); + + $this->assertStringEndsWith( + "address-document-verifications/adv_123/attempts/adva_123/assets?skip=2&limit=50", + $this->capturedUri() + ); + } + + /** + * @test + * @throws CheckoutApiException + */ + public function shouldSendLimitOnlyWhenSkipIsNotSet() + { + $client = new IdentityVerificationClient($this->apiClient, $this->configuration); + + $query = new AttemptsQueryFilter(); + $query->limit = 25; + + $client->getIdentityVerificationAttempts("idv_123", $query); + + $this->assertStringEndsWith("identity-verifications/idv_123/attempts?limit=25", $this->capturedUri()); + } + + /** + * A null filter must behave exactly like a plain get, with no trailing question mark. This is + * the regression that ApiClient::query()'s null guard prevents. + * + * @test + * @throws CheckoutApiException + */ + public function shouldSendNoQueryStringWhenTheFilterIsOmitted() + { + $client = new IdentityVerificationClient($this->apiClient, $this->configuration); + + $client->getIdentityVerificationAttempts("idv_123"); + + $uri = $this->capturedUri(); + $this->assertStringEndsWith("identity-verifications/idv_123/attempts", $uri); + $this->assertStringNotContainsString("?", $uri); + } + + /** + * An empty filter encodes to an empty string, which must not produce a bare question mark. + * + * @test + * @throws CheckoutApiException + */ + public function shouldSendNoQueryStringWhenTheFilterIsEmpty() + { + $client = new IdentityVerificationClient($this->apiClient, $this->configuration); + + $client->getIdentityVerificationAttempts("idv_123", new AttemptsQueryFilter()); + + $uri = $this->capturedUri(); + $this->assertStringEndsWith("identity-verifications/idv_123/attempts", $uri); + $this->assertStringNotContainsString("?", $uri); + } + + /** + * The assets methods declared a nullable filter before this change but ApiClient::query() did + * not accept one, so omitting it was a TypeError. + * + * @test + * @throws CheckoutApiException + */ + public function shouldSendNoQueryStringWhenTheAssetsFilterIsOmitted() + { + $client = new IdDocumentVerificationClient($this->apiClient, $this->configuration); + + $client->getIdDocumentVerificationAttemptAssets("iddv_123", "datp_123"); + + $uri = $this->capturedUri(); + $this->assertStringEndsWith("id-document-verifications/iddv_123/attempts/datp_123/assets", $uri); + $this->assertStringNotContainsString("?", $uri); + } +} diff --git a/test/Checkout/Tests/Identities/IdDocumentVerification/IdDocumentVerificationClientTest.php b/test/Checkout/Tests/Identities/IdDocumentVerification/IdDocumentVerificationClientTest.php index bfd14b33..cc579dcd 100644 --- a/test/Checkout/Tests/Identities/IdDocumentVerification/IdDocumentVerificationClientTest.php +++ b/test/Checkout/Tests/Identities/IdDocumentVerification/IdDocumentVerificationClientTest.php @@ -395,6 +395,70 @@ public function shouldCallCorrectApiEndpointForGetIdDocumentVerificationAttemptA $this->assertNotNull($response); } + /** + * @test + * @throws CheckoutApiException + */ + public function shouldDeserializeTheIdDocumentVerificationAttemptAssetsSwaggerExample() + { + $decoded = json_decode(self::IDDV_ATTEMPT_ASSETS_SWAGGER_EXAMPLE, true); + + $this->apiClient + ->method("query") + ->willReturn($decoded); + + $response = $this->client->getIdDocumentVerificationAttemptAssets( + "iddv_tkoi5db4hryu5cei5vwoabr7we", + "datp_tkoi5db4hryu5cei5vwoabraio" + ); + + $this->assertSame(2, $response["total_count"]); + $this->assertSame(0, $response["skip"]); + $this->assertSame(10, $response["limit"]); + $this->assertCount(2, $response["data"]); + $this->assertStringContainsString( + "document_front.png", + $response["data"][0]["_links"]["asset_url"]["href"] + ); + $this->assertStringContainsString( + "document_back.png", + $response["data"][1]["_links"]["asset_url"]["href"] + ); + $this->assertArrayHasKey("self", $response["_links"]); + $this->assertArrayHasKey("next", $response["_links"]); + $this->assertArrayHasKey("previous", $response["_links"]); + } + + /** + * The data array declares minItems 0, so an attempt with no assets yet is a valid page. + * + * @test + * @throws CheckoutApiException + */ + public function shouldHandleAnEmptyIdDocumentVerificationAttemptAssetsPage() + { + $this->apiClient + ->method("query") + ->willReturn([ + "total_count" => 0, + "skip" => 0, + "limit" => 10, + "data" => [], + "_links" => [ + "self" => [ + "href" => "https://api.checkout.com/id-document-verifications/" + . "iddoc_12345/attempts/datp_12345/assets" + ] + ] + ]); + + $response = $this->client->getIdDocumentVerificationAttemptAssets("iddoc_12345", "datp_12345"); + + $this->assertSame(0, $response["total_count"]); + $this->assertSame([], $response["data"]); + $this->assertArrayHasKey("_links", $response); + } + private function buildIdDocumentVerificationRequest(): IdDocumentVerificationRequest { $declaredData = new DeclaredData(); @@ -442,39 +506,52 @@ private function buildExpectedIdDocumentVerificationAttemptResponse(): array ]; } + /** + * The swagger example for this response, verbatim, from + * components.examples.iddv_attempt_assets_response_body. Kept as raw JSON rather than a PHP + * array so that a key renamed in the spec shows up as a test failure instead of being silently + * carried over from a hand-written fixture. + */ + private const IDDV_ATTEMPT_ASSETS_SWAGGER_EXAMPLE = <<<'JSON' +{ + "total_count": 2, + "skip": 0, + "limit": 10, + "data": [ + { + "type": "document_front_image", + "_links": { + "asset_url": { + "href": "https://storage-b.env.ubble.ai/ubble-ai/NDYOOVHGZPAQ/a54b3393-f02a-47c9-a9c5-2f6ee73560e1/bb603e2f-5de9-40f2-9631-8285a33c24c0/document_front.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Expires=3600" + } + } + }, + { + "type": "document_back_image", + "_links": { + "asset_url": { + "href": "https://storage-b.env.ubble.ai/ubble-ai/NDYOOVHGZPAQ/a54b3393-f02a-47c9-a9c5-2f6ee73560e1/bb603e2f-5de9-40f2-9631-8285a33c24c0/document_back.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Expires=3600" + } + } + } + ], + "_links": { + "self": { + "href": "https://identity-verification.checkout.com/id-document-verifications/iddv_tkoi5db4hryu5cei5vwoabr7we/attempts/datp_tkoi5db4hryu5cei5vwoabraio/assets" + }, + "next": { + "href": "https://identity-verification.checkout.com/id-document-verifications/iddv_tkoi5db4hryu5cei5vwoabr7we/attempts/datp_tkoi5db4hryu5cei5vwoabraio/assets?..." + }, + "previous": { + "href": "https://identity-verification.checkout.com/id-document-verifications/iddv_tkoi5db4hryu5cei5vwoabr7we/attempts/datp_tkoi5db4hryu5cei5vwoabraio/assets?..." + } + } +} +JSON; + private function buildExpectedIdDocumentVerificationAttemptAssetsResponse(): array { - return [ - "total_count" => 2, - "skip" => 0, - "limit" => 10, - "data" => [ - [ - "type" => "document_front_image", - "_links" => [ - "download" => [ - "href" => "https://api.checkout.com/id-document-verifications/" - . "iddoc_12345/attempts/attempt_67890/assets/document_front_image" - ] - ] - ], - [ - "type" => "document_back_image", - "_links" => [ - "download" => [ - "href" => "https://api.checkout.com/id-document-verifications/" - . "iddoc_12345/attempts/attempt_67890/assets/document_back_image" - ] - ] - ] - ], - "_links" => [ - "self" => [ - "href" => "https://api.checkout.com/id-document-verifications/" - . "iddoc_12345/attempts/attempt_67890/assets" - ] - ] - ]; + return json_decode(self::IDDV_ATTEMPT_ASSETS_SWAGGER_EXAMPLE, true); } private function validateIdDocumentVerificationAttemptAssetsResponse(array $response): void @@ -486,7 +563,11 @@ private function validateIdDocumentVerificationAttemptAssetsResponse(array $resp $this->assertArrayHasKey("_links", $response); $this->assertSame("document_front_image", $response["data"][0]["type"]); $this->assertSame("document_back_image", $response["data"][1]["type"]); - $this->assertArrayHasKey("download", $response["data"][0]["_links"]); + // asset_url is the only link the IddvAttemptAsset schema declares, and it is required. + $this->assertArrayHasKey("asset_url", $response["data"][0]["_links"]); + $this->assertArrayHasKey("asset_url", $response["data"][1]["_links"]); + $this->assertArrayNotHasKey("download", $response["data"][0]["_links"]); + $this->assertNotEmpty($response["data"][0]["_links"]["asset_url"]["href"]); } private function buildExpectedIdDocumentVerificationAttemptsResponse(): array diff --git a/test/Checkout/Tests/Identities/IdentitiesSerializationTest.php b/test/Checkout/Tests/Identities/IdentitiesSerializationTest.php index e7e142d0..f8fc3d3b 100644 --- a/test/Checkout/Tests/Identities/IdentitiesSerializationTest.php +++ b/test/Checkout/Tests/Identities/IdentitiesSerializationTest.php @@ -119,6 +119,32 @@ public function testIdentityDeclaredDataRoundTrip() ); } + public function testIdentityDeclaredDataOmitsUnsetEmail() + { + $declaredData = new IdentityDeclaredData(); + $declaredData->name = "Hannah Bret"; + + $decoded = json_decode($this->serializer->serialize($declaredData), true); + + $this->assertSame(["name"], array_keys($decoded)); + $this->assertArrayNotHasKey("email", $decoded); + } + + /** + * The spec marks email nullable, so a response may carry an explicit null rather than omitting + * the key. Deserialization must preserve that instead of dropping it. + */ + public function testIdentityDeclaredDataReadsAnExplicitNullEmail() + { + $json = '{"name":"Hannah Bret","birth_date":"1994-10-15","email":null}'; + + $decoded = $this->serializer->deserialize($json); + + $this->assertArrayHasKey("email", $decoded); + $this->assertNull($decoded["email"]); + $this->assertSame("Hannah Bret", $decoded["name"]); + } + public function testFaceAuthenticationClientInformationKeepsTheTwoFieldShape() { $clientInformation = new ClientInformation(); diff --git a/test/Checkout/Tests/Issuing/Cards/CardUpdateHeadersTest.php b/test/Checkout/Tests/Issuing/Cards/CardUpdateHeadersTest.php index 2ab0aa6e..def96556 100644 --- a/test/Checkout/Tests/Issuing/Cards/CardUpdateHeadersTest.php +++ b/test/Checkout/Tests/Issuing/Cards/CardUpdateHeadersTest.php @@ -6,13 +6,21 @@ use Checkout\CheckoutApiException; use Checkout\CheckoutArgumentException; use Checkout\CheckoutAuthorizationException; +use Checkout\CheckoutConfiguration; use Checkout\CheckoutException; +use Checkout\Environment; +use Checkout\HttpClientBuilderInterface; use Checkout\Issuing\Cards\Update\CardUpdateHeaders; use Checkout\Issuing\Cards\Update\UpdateCardRequest; use Checkout\Issuing\IssuingClient; use Checkout\PlatformType; use Checkout\SdkAuthorization; +use Checkout\SdkCredentialsInterface; use Checkout\Tests\UnitTestFixture; +use GuzzleHttp\HandlerStack; +use GuzzleHttp\Psr7\Response; +use Monolog\Handler\StreamHandler; +use Monolog\Logger; use ReflectionMethod; class CardUpdateHeadersTest extends UnitTestFixture @@ -155,6 +163,102 @@ public function shouldUpdateCardDetailsWithoutHeaders() $this->assertArrayNotHasKey("encrypted_cvv", $response); } + /** + * The API answers 422 with error code encryption_key_required when return-encrypted-cvv is set + * to true without an Encryption-Key header. Asserted through a stubbed Guzzle handler rather + * than a mocked ApiClient, so the status code and the error body travel the real path into + * CheckoutApiException. + * + * @test + */ + public function shouldSurfaceThe422WhenTheEncryptionKeyIsMissing() + { + $client = $this->buildClientReturning(422, '{' + . '"request_id":"0HLHPN8802NUF:00000003",' + . '"error_type":"request_invalid",' + . '"error_codes":["encryption_key_required"]' + . '}'); + + $headers = new CardUpdateHeaders(); + $headers->return_encrypted_cvv = "true"; + + try { + $client->updateCardDetails("crd_12345", new UpdateCardRequest(), $headers); + $this->fail("Expected a CheckoutApiException for the 422 response"); + } catch (CheckoutApiException $e) { + $this->assertSame(422, $e->http_metadata->getStatusCode()); + $this->assertSame("request_invalid", $e->error_details["error_type"]); + $this->assertContains("encryption_key_required", $e->error_details["error_codes"]); + } + } + + /** + * @test + */ + public function shouldSucceedWhenBothHeadersAreSupplied() + { + $client = $this->buildClientReturning(200, '{' + . '"last_modified_date":"2026-06-01T10:00:00Z",' + . '"encrypted_cvv":"oJMoNMEEUiQKYOsQ4Zd"' + . '}'); + + $headers = new CardUpdateHeaders(); + $headers->return_encrypted_cvv = "true"; + $headers->encryption_key = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A"; + + $response = $client->updateCardDetails("crd_12345", new UpdateCardRequest(), $headers); + + $this->assertSame("oJMoNMEEUiQKYOsQ4Zd", $response["encrypted_cvv"]); + $this->assertSame(200, $response["http_metadata"]->getStatusCode()); + } + + /** + * Builds an IssuingClient whose HTTP layer always answers with the given status and body. + * + * @param int $status + * @param string $body + * @return IssuingClient + * @throws CheckoutArgumentException + * @throws CheckoutException + */ + private function buildClientReturning(int $status, string $body): IssuingClient + { + $stack = HandlerStack::create(); + $stack->setHandler(function ($request, array $options) use ($status, $body) { + $response = new Response($status, ['Content-Type' => 'application/json'], $body); + if ($status >= 400) { + return \GuzzleHttp\Promise\Create::rejectionFor( + new \GuzzleHttp\Exception\RequestException( + "error", + $request, + $response + ) + ); + } + return \GuzzleHttp\Promise\Create::promiseFor($response); + }); + + $httpBuilder = $this->createMock(HttpClientBuilderInterface::class); + $httpBuilder->method("getClient")->willReturn(new \GuzzleHttp\Client(["handler" => $stack])); + + $sdkCredentials = $this->createMock(SdkCredentialsInterface::class); + $sdkCredentials + ->method("getAuthorization") + ->willReturn(new SdkAuthorization(PlatformType::$default, "sk_sbox_key")); + + $logger = new Logger("checkout-sdk-test-php"); + $logger->pushHandler(new StreamHandler("php://stderr")); + + $configuration = new CheckoutConfiguration( + $sdkCredentials, + Environment::sandbox(), + $httpBuilder, + $logger + ); + + return new IssuingClient(new ApiClient($configuration), $configuration); + } + /** * Exercises the private header builder on a real ApiClient, so the assertions pin the names * that actually reach the wire rather than the mapping declaration alone.