diff --git a/lib/Checkout/Accounts/AccountsClient.php b/lib/Checkout/Accounts/AccountsClient.php index 016bc4f..fe909ff 100644 --- a/lib/Checkout/Accounts/AccountsClient.php +++ b/lib/Checkout/Accounts/AccountsClient.php @@ -163,10 +163,13 @@ public function updateBankPaymentInstrumentDetails( $instrumentId, UpdatePaymentInstrumentRequest $instrumentRequest ) { + // The API reads the ETag only from the If-Match HTTP header and answers 428 without it, so the + // request's headers are sent as HTTP headers. return $this->apiClient->patch( $this->buildPath(self::ACCOUNTS_PATH, self::ENTITIES_PATH, $entityId, self::PAYMENT_INSTRUMENTS_PATH, $instrumentId), $instrumentRequest, - $this->sdkAuthorization() + $this->sdkAuthorization(), + $instrumentRequest->headers ); } diff --git a/lib/Checkout/Accounts/AdditionalDocument.php b/lib/Checkout/Accounts/AdditionalDocument.php new file mode 100644 index 0000000..1be1b17 --- /dev/null +++ b/lib/Checkout/Accounts/AdditionalDocument.php @@ -0,0 +1,20 @@ +if_match. + * AccountsClient::updateBankPaymentInstrumentDetails sends it as the If-Match HTTP header; the + * API answers 428 without it and 412 when it does not match. + * [Required] by the API. + * * @var Headers */ public $headers; diff --git a/test/Checkout/Tests/Accounts/AccountsClientEntityRequirementsTest.php b/test/Checkout/Tests/Accounts/AccountsClientEntityRequirementsTest.php index 6a3c56f..2d96159 100644 --- a/test/Checkout/Tests/Accounts/AccountsClientEntityRequirementsTest.php +++ b/test/Checkout/Tests/Accounts/AccountsClientEntityRequirementsTest.php @@ -37,8 +37,8 @@ public function shouldGetEntityRequirements() ->willReturn([ "data" => [ [ - "id" => "req_123", - "resource" => "ent_456", + "id" => "req_5wmacwhrhbzhqkhx5hlqmzje44", + "resource" => "ent_azsiyswl7bwe2ynjzujy7lcjca", "reason" => "periodic_review", "priority" => "high" ] @@ -58,8 +58,8 @@ public function shouldGetEntityRequirementDetails() $this->apiClient ->method("get") ->willReturn([ - "id" => "req_123", - "resource" => "ent_456", + "id" => "req_5wmacwhrhbzhqkhx5hlqmzje44", + "resource" => "ent_azsiyswl7bwe2ynjzujy7lcjca", "reason" => "periodic_review", "priority" => "high", "message" => "Please provide your ID", @@ -79,13 +79,13 @@ public function shouldResolveEntityRequirement() $this->apiClient ->method("put") ->willReturn([ - "id" => "req_123", + "id" => "req_5wmacwhrhbzhqkhx5hlqmzje44", "status" => "processing", "submitted_at" => "2026-05-05T10:15:30Z" ]); $request = new EntityRequirementUpdateRequest(); - $request->value = ["file_id" => "file_test123"]; + $request->value = ["file_id" => "file_awonj5x3g4oupitqp6dhsc4hyy"]; $response = $this->client->resolveEntityRequirement("entity_id", "requirement_id", $request); $this->assertNotNull($response); @@ -100,7 +100,7 @@ public function shouldResolveEntityRequirementWithStringValue() $this->apiClient ->method("put") ->willReturn([ - "id" => "req_123", + "id" => "req_5wmacwhrhbzhqkhx5hlqmzje44", "status" => "processing", "submitted_at" => "2026-05-05T10:15:30Z" ]); diff --git a/test/Checkout/Tests/Accounts/AccountsClientTest.php b/test/Checkout/Tests/Accounts/AccountsClientTest.php index 7bd584f..0e95d85 100644 --- a/test/Checkout/Tests/Accounts/AccountsClientTest.php +++ b/test/Checkout/Tests/Accounts/AccountsClientTest.php @@ -8,6 +8,7 @@ use Checkout\Accounts\OnboardEntityRequest; use Checkout\Accounts\PaymentInstrumentRequest; use Checkout\Accounts\PaymentInstrumentsQuery; +use Checkout\Accounts\Headers; use Checkout\Accounts\UpdatePaymentInstrumentRequest; use Checkout\Accounts\UpdateScheduleRequest; use Checkout\Accounts\ReserveRules\Requests\CreateReserveRuleRequest; @@ -220,6 +221,33 @@ public function shouldUpdateBankPaymentInstrument() $this->assertNotNull($response); } + /** + * @test + * @throws CheckoutApiException + */ + public function shouldSendUpdateBankPaymentInstrumentEtagAsIfMatchHeader() + { + $request = new UpdatePaymentInstrumentRequest(); + $request->label = "Renamed account"; + $request->headers = new Headers(); + $request->headers->if_match = '"Y3Y9MCZydj0w"'; + + $this->apiClient + ->expects($this->once()) + ->method("patch") + ->with( + "accounts/entities/entity_id/payment-instruments/instrument_id", + $request, + $this->anything(), + $this->identicalTo($request->headers) + ) + ->willReturn(["response"]); + + $response = $this->client->updateBankPaymentInstrumentDetails("entity_id", "instrument_id", $request); + + $this->assertNotNull($response); + } + /** * @test * @throws CheckoutApiException @@ -228,7 +256,7 @@ public function shouldGetSubEntityMembers() { $this->apiClient ->method("get") - ->willReturn(["data" => [["user_id" => "usr_test123"]]]); + ->willReturn(["data" => [["user_id" => "usr_eyk754cqieqexfh6u46no5nnha"]]]); $response = $this->client->getSubEntityMembers("entity_id"); @@ -244,7 +272,7 @@ public function shouldReinviteSubEntityMember() { $this->apiClient ->method("put") - ->willReturn(["id" => "usr_test123"]); + ->willReturn(["id" => "usr_eyk754cqieqexfh6u46no5nnha"]); $response = $this->client->reinviteSubEntityMember("entity_id", "user_id"); @@ -260,7 +288,10 @@ public function shouldCreateReserveRule() { $this->apiClient ->method("post") - ->willReturn(["id" => "rsv_test123", "_links" => ["self" => ["href" => "/reserve-rules/rsv_test123"]]]); + ->willReturn([ + "id" => "rsv_qn4nis4k3ykpzzu7cvtuvhqqga", + "_links" => ["self" => ["href" => "/reserve-rules/rsv_qn4nis4k3ykpzzu7cvtuvhqqga"]] + ]); $request = $this->buildCreateReserveRuleRequest(); $response = $this->client->createReserveRule("entity_id", $request); @@ -277,7 +308,7 @@ public function shouldGetReserveRules() { $this->apiClient ->method("get") - ->willReturn(["data" => [["id" => "rsv_test123", "type" => "rolling"]]]); + ->willReturn(["data" => [["id" => "rsv_qn4nis4k3ykpzzu7cvtuvhqqga", "type" => "rolling"]]]); $response = $this->client->getReserveRules("entity_id"); @@ -293,7 +324,11 @@ public function shouldGetReserveRuleDetails() { $this->apiClient ->method("get") - ->willReturn(["id" => "rsv_test123", "type" => "rolling", "valid_from" => "2024-01-01T00:00:00Z"]); + ->willReturn([ + "id" => "rsv_qn4nis4k3ykpzzu7cvtuvhqqga", + "type" => "rolling", + "valid_from" => "2024-01-01T00:00:00Z" + ]); $response = $this->client->getReserveRuleDetails("entity_id", "reserve_rule_id"); @@ -310,7 +345,10 @@ public function shouldUpdateReserveRule() { $this->apiClient ->method("put") - ->willReturn(["id" => "rsv_test123", "_links" => ["self" => ["href" => "/reserve-rules/rsv_test123"]]]); + ->willReturn([ + "id" => "rsv_qn4nis4k3ykpzzu7cvtuvhqqga", + "_links" => ["self" => ["href" => "/reserve-rules/rsv_qn4nis4k3ykpzzu7cvtuvhqqga"]] + ]); $request = $this->buildUpdateReserveRuleRequest(); $response = $this->client->updateReserveRule("entity_id", "reserve_rule_id", "etag_value", $request); @@ -325,31 +363,44 @@ public function shouldUpdateReserveRule() */ public function shouldUploadFile() { + // Per-field examples of PlatformsFileUploadResponse in the spec. + $expected = [ + "id" => "file_6lbss42ezvoufcb2beo76rvwly", + "maximum_size_in_bytes" => 4194304, + "document_types_for_purpose" => ["image/jpeg", "image/png", "image/jpg"], + "_links" => [ + "upload" => [ + "href" => "https://s3.eu-west-1.amazonaws.com/mp-files-api-staging-prod/" + . "ent_ociwguf5a5fe3ndmpnvpnwsi3e/file_6lbss42ezvoufcb2beo76rvwly" + . "?AWSAccessKeyId=ASIX4BFJOBCQFLAMPKU3&Expires=1661355993&x-amz-security-token=some_token" + ], + "self" => [ + "href" => "https://files.checkout.com/files/file_6lbss42ezvoufcb2beo76rvwly" + ] + ] + ]; + $this->apiClient ->method("post") - ->willReturn([ - "id" => "file_6lbss42ezvoufcb2beo76rvwly", - "maximum_size_in_bytes" => 4194304, - "document_types_for_purpose" => ["image/jpeg", "image/png", "image/jpg"], - "_links" => [ - "upload" => [ - "href" => "https://s3.eu-west-1.amazonaws.com/mp-files-api-staging-prod/ent_ociwguf5a5fe3ndmpnvpnwsi3e/file_6lbss42ezvoufcb2beo76rvwly?AWSAccessKeyId=ASIX4BFJOBCQFLAMPKU3&Expires=1661355993&x-amz-security-token=some_token" - ], - "self" => [ - "href" => "https://files.checkout.com/files/file_6lbss42ezvoufcb2beo76rvwly" - ] - ] - ]); + ->willReturn($expected); $fileRequest = $this->buildUploadFileRequest(); $response = $this->client->uploadFile("entity_id", $fileRequest); - $this->assertNotNull($response); - $this->assertArrayHasKey("id", $response); - $this->assertArrayHasKey("maximum_size_in_bytes", $response); - $this->assertArrayHasKey("document_types_for_purpose", $response); - $this->assertArrayHasKey("_links", $response); - $this->assertEquals("file_6lbss42ezvoufcb2beo76rvwly", $response["id"]); + $this->assertSame("file_6lbss42ezvoufcb2beo76rvwly", $response["id"]); + $this->assertSame(4194304, $response["maximum_size_in_bytes"]); + $this->assertSame(["image/jpeg", "image/png", "image/jpg"], $response["document_types_for_purpose"]); + $this->assertSame( + "https://s3.eu-west-1.amazonaws.com/mp-files-api-staging-prod/" + . "ent_ociwguf5a5fe3ndmpnvpnwsi3e/file_6lbss42ezvoufcb2beo76rvwly" + . "?AWSAccessKeyId=ASIX4BFJOBCQFLAMPKU3&Expires=1661355993&x-amz-security-token=some_token", + $response["_links"]["upload"]["href"] + ); + $this->assertSame( + "https://files.checkout.com/files/file_6lbss42ezvoufcb2beo76rvwly", + $response["_links"]["self"]["href"] + ); + $this->assertSame($expected, $response); } /** @@ -358,38 +409,52 @@ public function shouldUploadFile() */ public function shouldRetrieveFile() { + // Per-field examples of PlatformsFileRetrieveResponse in the spec, including the + // seven fractional digits of uploaded_on, which the SDK returns as an unparsed string. + $expected = [ + "id" => "file_6lbss42ezvoufcb2beo76rvwly", + "status" => "invalid", + "status_reasons" => ["InvalidMimeType"], + "size" => 1024, + "mime_type" => "application/pdf", + "uploaded_on" => "2020-12-01T15:01:01.0000000+00:00", + "purpose" => "identity_verification", + "_links" => [ + "download" => [ + "href" => "https://s3.eu-west-1.amazonaws.com/mp-files-api-clean-prod/" + . "ent_ociwguf5a5fe3ndmpnvpnwsi3e/file_6lbss42ezvoufcb2beo76rvwly" + . "?X-Amz-Expires=3600&x-amz-security-token=some_token" + ], + "self" => [ + "href" => "https://files.checkout.com/files/file_6lbss42ezvoufcb2beo76rvwly" + ] + ] + ]; + $this->apiClient ->method("get") - ->willReturn([ - "id" => "file_6lbss42ezvoufcb2beo76rvwly", - "status" => "valid", - "status_reasons" => null, - "size" => 1024, - "mime_type" => "application/pdf", - "uploaded_on" => "2020-12-01T15:01:01Z", - "purpose" => "identity_verification", - "_links" => [ - "download" => [ - "href" => "https://s3.eu-west-1.amazonaws.com/mp-files-api-clean-prod/ent_ociwguf5a5fe3ndmpnvpnwsi3e/file_6lbss42ezvoufcb2beo76rvwly?X-Amz-Expires=3600&x-amz-security-token=some_token" - ], - "self" => [ - "href" => "https://files.checkout.com/files/file_6lbss42ezvoufcb2beo76rvwly" - ] - ] - ]); + ->willReturn($expected); $response = $this->client->retrieveFile("entity_id", "file_6lbss42ezvoufcb2beo76rvwly"); - $this->assertNotNull($response); - $this->assertArrayHasKey("id", $response); - $this->assertArrayHasKey("status", $response); - $this->assertArrayHasKey("size", $response); - $this->assertArrayHasKey("mime_type", $response); - $this->assertArrayHasKey("uploaded_on", $response); - $this->assertArrayHasKey("purpose", $response); - $this->assertArrayHasKey("_links", $response); - $this->assertEquals("file_6lbss42ezvoufcb2beo76rvwly", $response["id"]); - $this->assertEquals("valid", $response["status"]); + $this->assertSame("file_6lbss42ezvoufcb2beo76rvwly", $response["id"]); + $this->assertSame("invalid", $response["status"]); + $this->assertSame(["InvalidMimeType"], $response["status_reasons"]); + $this->assertSame(1024, $response["size"]); + $this->assertSame("application/pdf", $response["mime_type"]); + $this->assertSame("2020-12-01T15:01:01.0000000+00:00", $response["uploaded_on"]); + $this->assertSame("identity_verification", $response["purpose"]); + $this->assertSame( + "https://s3.eu-west-1.amazonaws.com/mp-files-api-clean-prod/" + . "ent_ociwguf5a5fe3ndmpnvpnwsi3e/file_6lbss42ezvoufcb2beo76rvwly" + . "?X-Amz-Expires=3600&x-amz-security-token=some_token", + $response["_links"]["download"]["href"] + ); + $this->assertSame( + "https://files.checkout.com/files/file_6lbss42ezvoufcb2beo76rvwly", + $response["_links"]["self"]["href"] + ); + $this->assertSame($expected, $response); } /** diff --git a/test/Checkout/Tests/Accounts/AccountsIntegrationTest.php b/test/Checkout/Tests/Accounts/AccountsIntegrationTest.php index 155f0e0..9842b3b 100644 --- a/test/Checkout/Tests/Accounts/AccountsIntegrationTest.php +++ b/test/Checkout/Tests/Accounts/AccountsIntegrationTest.php @@ -4,10 +4,13 @@ use Checkout\Accounts\AccountsFileRequest; use Checkout\Accounts\BusinessType; +use Checkout\Accounts\CertifiedAuthorisedSignatory; +use Checkout\Accounts\CertifiedAuthorisedSignatoryType; use Checkout\Accounts\Company; use Checkout\Accounts\ContactDetails; use Checkout\Accounts\DateOfBirth; use Checkout\Accounts\DateOfIncorporation; +use Checkout\Accounts\Document; use Checkout\Accounts\EntityEmailAddresses; use Checkout\Accounts\EntityRoles; use Checkout\Accounts\InstrumentDetailsFasterPayments; @@ -18,14 +21,20 @@ use Checkout\Accounts\ProcessingDetailsAch; use Checkout\Accounts\ProcessingDetailsPayments; use Checkout\Accounts\PaymentInstrumentRequest; +use Checkout\Accounts\Headers; +use Checkout\Accounts\UpdatePaymentInstrumentRequest; +use Checkout\Accounts\InstrumentAccountType; +use Checkout\Accounts\InstrumentDetailsAch; use Checkout\Accounts\PaymentInstrumentsQuery; use Checkout\Accounts\Profile; use Checkout\Accounts\Representative; +use Checkout\Accounts\RepresentativeDocuments; use Checkout\Accounts\RepresentativeIndividual; use Checkout\Accounts\ReserveRules\Requests\CreateReserveRuleRequest; use Checkout\Accounts\ReserveRules\Requests\UpdateReserveRuleRequest; use Checkout\Accounts\ReserveRules\Entities\Rolling; use Checkout\Accounts\ReserveRules\Entities\HoldingDuration; +use Checkout\Accounts\Files\Entities\FilePurpose; use Checkout\Accounts\Files\Requests\UploadFileRequest; use Checkout\CheckoutApi; use Checkout\CheckoutApiException; @@ -34,6 +43,7 @@ use Checkout\CheckoutSdk; use Checkout\Common\Country; use Checkout\Common\Currency; +use Checkout\Common\DocumentType; use Checkout\Common\InstrumentType; use Checkout\Common\Phone; use Checkout\OAuthScope; @@ -93,6 +103,79 @@ public function shouldCreateGetAndUpdateOnboardEntity() $this->assertEquals($response["id"], $updateResponse["id"]); } + /** + * The representative's documents go through RepresentativeDocuments. The sandbox platform + * resolves to a company variant, where identity_verification and certified_authorised_signatory + * are the representative documents the API accepts. + * + * @test + * @throws CheckoutApiException + */ + public function shouldCreateEntityWithRepresentativeDocuments() + { + $identityFile = $this->uploadFile(FilePurpose::$identity_verification); + $signatoryFile = $this->uploadFile(FilePurpose::$certified_authorised_signatory); + + $identity = new Document(); + $identity->type = DocumentType::$passport; + $identity->front = $identityFile["id"]; + + $signatory = new CertifiedAuthorisedSignatory(); + $signatory->type = CertifiedAuthorisedSignatoryType::$power_of_attorney; + $signatory->front = $signatoryFile["id"]; + + $documents = new RepresentativeDocuments(); + $documents->identity_verification = $identity; + $documents->certified_authorised_signatory = $signatory; + + $onboardEntityRequest = $this->buildOnboardCompanyRequest(); + $onboardEntityRequest->company->representatives[0]->documents = $documents; + + $response = $this->accountsApi()->getAccountsClient()->createEntity($onboardEntityRequest); + + $this->assertResponse($response, "id", "reference"); + + // The documents are linked on the representative, not dropped: the API echoes them back. + $entity = $this->accountsApi()->getAccountsClient()->getEntity($response["id"]); + $this->assertResponse($entity, "company.representatives"); + $linked = $entity["company"]["representatives"][0]["documents"]; + $this->assertEquals( + array("type" => DocumentType::$passport, "front" => $identityFile["id"]), + $linked["identity_verification"] + ); + $this->assertEquals( + array("type" => CertifiedAuthorisedSignatoryType::$power_of_attorney, "front" => $signatoryFile["id"]), + $linked["certified_authorised_signatory"] + ); + } + + /** + * The two EEA Sole Trader representative documents need their own upload purposes before + * they can be linked on the representative. Goes through POST /entities/{id}/files, the + * endpoint whose request schema (PlatformsFileUpload) defines the purpose enum. + * + * @test + * @throws CheckoutApiException + */ + public function shouldUploadRepresentativeProofFiles() + { + $entityId = $this->createTestEntity(); + $purposes = array(FilePurpose::$proof_of_residential_address, FilePurpose::$proof_of_registration); + + foreach ($purposes as $purpose) { + $uploadRequest = new UploadFileRequest(); + $uploadRequest->purpose = $purpose; + + $uploadResponse = $this->accountsApi()->getAccountsClient()->uploadFile($entityId, $uploadRequest); + $this->validateFileUploadResponse($uploadResponse); + + $retrieveResponse = $this->accountsApi()->getAccountsClient() + ->retrieveFile($entityId, $uploadResponse["id"]); + $this->validateFileRetrieveResponse($retrieveResponse); + $this->assertEquals($purpose, $retrieveResponse["purpose"]); + } + } + /** * @test * @throws CheckoutApiException @@ -144,6 +227,50 @@ public function shouldCreateAndRetrievePaymentInstrument() $this->assertResponse($queryResponse, "data"); } + /** + * The update only succeeds when the ETag reaches the API as the If-Match HTTP header: without it + * the API answers 428, and with a stale ETag 412. + * + * @test + * @throws CheckoutApiException + * @throws CheckoutArgumentException + * @throws CheckoutException + */ + public function shouldUpdatePaymentInstrumentWithEtag() + { + $client = $this->accountsApi()->getAccountsClient(); + $entity = $client->createEntity($this->buildOnboardCompanyRequest()); + $file = $this->uploadFile(); + + $instrumentRequest = new PaymentInstrumentRequest(); + $instrumentRequest->label = "Main account"; + $instrumentRequest->type = InstrumentType::$bank_account; + $instrumentRequest->currency = Currency::$USD; + $instrumentRequest->country = Country::$US; + $instrumentRequest->document = new InstrumentDocument(); + $instrumentRequest->document->type = "bank_statement"; + $instrumentRequest->document->file_id = $file["id"]; + $instrumentRequest->instrument_details = new InstrumentDetailsAch(); + $instrumentRequest->instrument_details->account_number = "123456789"; + $instrumentRequest->instrument_details->routing_number = "026009593"; + // The sandbox rejects checking (instrument_details_account_type_invalid), although the spec lists it. + $instrumentRequest->instrument_details->account_type = InstrumentAccountType::$savings; + $instrumentId = $client->createBankPaymentInstrument($entity["id"], $instrumentRequest)["id"]; + + $details = $client->retrievePaymentInstrumentDetails($entity["id"], $instrumentId); + $responseHeaders = array_change_key_case($details["http_metadata"]->getHeaders(), CASE_LOWER); + + $updateRequest = new UpdatePaymentInstrumentRequest(); + $updateRequest->label = "Renamed account"; + $updateRequest->headers = new Headers(); + $updateRequest->headers->if_match = $responseHeaders["etag"][0]; + $response = $client->updateBankPaymentInstrumentDetails($entity["id"], $instrumentId, $updateRequest); + + $this->assertEquals($instrumentId, $response["id"]); + $updated = $client->retrievePaymentInstrumentDetails($entity["id"], $instrumentId); + $this->assertEquals("Renamed account", $updated["label"]); + } + /** * @test * @throws CheckoutApiException @@ -295,7 +422,6 @@ public function shouldUpdateReserveRule() /** * @test - * @skip API temporarily unavailable * @throws CheckoutApiException */ public function shouldUploadAndRetrieveFile() @@ -611,15 +737,16 @@ private function getAccountsCheckoutApi() } /** + * @param string $purpose value of FilePurpose * @return array * @throws CheckoutApiException */ - public function uploadFile() + public function uploadFile($purpose = "bank_verification") { $fileRequest = new AccountsFileRequest(); $fileRequest->file = $this->getCheckoutFilePath(); $fileRequest->content_type = "image/jpeg"; - $fileRequest->purpose = "bank_verification"; + $fileRequest->purpose = $purpose; $response = $this->accountsApi()->getAccountsClient()->submitFile($fileRequest); diff --git a/test/Checkout/Tests/Accounts/AccountsSchemaVersionHeaderTest.php b/test/Checkout/Tests/Accounts/AccountsSchemaVersionHeaderTest.php index ed8e789..a062b00 100644 --- a/test/Checkout/Tests/Accounts/AccountsSchemaVersionHeaderTest.php +++ b/test/Checkout/Tests/Accounts/AccountsSchemaVersionHeaderTest.php @@ -3,9 +3,23 @@ namespace Checkout\Tests\Accounts; use Checkout\Accounts\AccountsClient; +use Checkout\Accounts\BankVerification; +use Checkout\Accounts\BankVerificationType; +use Checkout\Accounts\BusinessType; +use Checkout\Accounts\Company; +use Checkout\Accounts\Document; +use Checkout\Accounts\EntityRoles; use Checkout\Accounts\OnboardEntityRequest; +use Checkout\Accounts\OnboardSubEntityDocuments; +use Checkout\Accounts\ProofOfRegistration; +use Checkout\Accounts\ProofOfRegistrationType; +use Checkout\Accounts\ProofOfResidentialAddress; +use Checkout\Accounts\ProofOfResidentialAddressType; +use Checkout\Accounts\Representative; +use Checkout\Accounts\RepresentativeDocuments; use Checkout\ApiClient; use Checkout\CheckoutConfiguration; +use Checkout\Common\DocumentType; use Checkout\Environment; use Checkout\HttpClientBuilderInterface; use Checkout\PlatformType; @@ -42,7 +56,7 @@ private function createCapturingHttpClient(): \GuzzleHttp\Client $stack->setHandler(function () { return \GuzzleHttp\Promise\Create::promiseFor( - new Response(200, ['Content-Type' => 'application/json'], '{"id":"ent_123"}') + new Response(200, ['Content-Type' => 'application/json'], '{"id":"ent_w4jelhppmfiufdnatam37wrfc4"}') ); }); @@ -100,7 +114,7 @@ public function createEntitySendsSchemaVersion3ByDefault() */ public function getEntitySendsSchemaVersion3ByDefault() { - $this->buildAccountsClient()->getEntity('ent_123'); + $this->buildAccountsClient()->getEntity('ent_w4jelhppmfiufdnatam37wrfc4'); $this->assertAcceptHeader('application/json;schema_version=3.0'); } @@ -109,7 +123,7 @@ public function getEntitySendsSchemaVersion3ByDefault() */ public function updateEntitySendsSchemaVersion3ByDefault() { - $this->buildAccountsClient()->updateEntity('ent_123', new OnboardEntityRequest()); + $this->buildAccountsClient()->updateEntity('ent_w4jelhppmfiufdnatam37wrfc4', new OnboardEntityRequest()); $this->assertAcceptHeader('application/json;schema_version=3.0'); } @@ -118,7 +132,7 @@ public function updateEntitySendsSchemaVersion3ByDefault() */ public function getEntityRequirementsSendsSchemaVersion3ByDefault() { - $this->buildAccountsClient()->getEntityRequirements('ent_123'); + $this->buildAccountsClient()->getEntityRequirements('ent_w4jelhppmfiufdnatam37wrfc4'); $this->assertAcceptHeader('application/json;schema_version=3.0'); } @@ -127,7 +141,85 @@ public function getEntityRequirementsSendsSchemaVersion3ByDefault() */ public function allowsOverridingTheSchemaVersion() { - $this->buildAccountsClient()->getEntity('ent_123', '2.0'); + $this->buildAccountsClient()->getEntity('ent_w4jelhppmfiufdnatam37wrfc4', '2.0'); $this->assertAcceptHeader('application/json;schema_version=2.0'); } + + /** + * The EEA Sole Trader (3.0) representative documents cannot be exercised against the sandbox, + * whose platform is not EEA-scoped, so this asserts the bytes that reach the wire instead: on + * POST and PUT, the three documents sit under company.representatives[0].documents, on the v3.0 + * schema, and only bank_verification is sent at the top level. + * + * @test + */ + public function eeaSoleTraderRepresentativeDocumentsReachTheWire() + { + $expectedRepresentativeDocuments = array( + "identity_verification" => array("type" => "passport", "front" => "file_identityverificationaaaaaa"), + "proof_of_residential_address" => array( + "type" => "proof_of_address", + "front" => "file_proofofresidentialaddressa", + ), + "proof_of_registration" => array( + "type" => "extract_from_trade_register", + "front" => "file_proofofregistrationaaaaaaa", + ), + ); + + $this->buildAccountsClient()->createEntity($this->buildEeaSoleTraderRequest()); + $this->assertSame('POST', $this->capturedRequest->getMethod()); + $this->assertSame('/accounts/entities', $this->capturedRequest->getUri()->getPath()); + $this->assertAcceptHeader('application/json;schema_version=3.0'); + $body = json_decode((string) $this->capturedRequest->getBody(), true); + $this->assertSame($expectedRepresentativeDocuments, $body['company']['representatives'][0]['documents']); + $this->assertSame(array('bank_verification'), array_keys($body['documents'])); + + $this->buildAccountsClient()->updateEntity( + 'ent_w4jelhppmfiufdnatam37wrfc4', + $this->buildEeaSoleTraderRequest() + ); + $this->assertSame('PUT', $this->capturedRequest->getMethod()); + $this->assertSame( + '/accounts/entities/ent_w4jelhppmfiufdnatam37wrfc4', + $this->capturedRequest->getUri()->getPath() + ); + $body = json_decode((string) $this->capturedRequest->getBody(), true); + $this->assertSame($expectedRepresentativeDocuments, $body['company']['representatives'][0]['documents']); + } + + private function buildEeaSoleTraderRequest(): OnboardEntityRequest + { + $identity = new Document(); + $identity->type = DocumentType::$passport; + $identity->front = "file_identityverificationaaaaaa"; + + $proofOfResidentialAddress = new ProofOfResidentialAddress(); + $proofOfResidentialAddress->type = ProofOfResidentialAddressType::$proof_of_address; + $proofOfResidentialAddress->front = "file_proofofresidentialaddressa"; + + $proofOfRegistration = new ProofOfRegistration(); + $proofOfRegistration->type = ProofOfRegistrationType::$extract_from_trade_register; + $proofOfRegistration->front = "file_proofofregistrationaaaaaaa"; + + $representative = new Representative(); + $representative->roles = array(EntityRoles::$ubo); + $representative->documents = new RepresentativeDocuments(); + $representative->documents->identity_verification = $identity; + $representative->documents->proof_of_residential_address = $proofOfResidentialAddress; + $representative->documents->proof_of_registration = $proofOfRegistration; + + $bankVerification = new BankVerification(); + $bankVerification->type = BankVerificationType::$bank_statement; + $bankVerification->front = "file_bankverificationaaaaaaaaaa"; + + $request = new OnboardEntityRequest(); + $request->reference = "ref_sole_trader"; + $request->company = new Company(); + $request->company->business_type = BusinessType::$individual_or_sole_proprietorship; + $request->company->representatives = array($representative); + $request->documents = new OnboardSubEntityDocuments(); + $request->documents->bank_verification = $bankVerification; + return $request; + } } diff --git a/test/Checkout/Tests/Accounts/CompanySerializationTest.php b/test/Checkout/Tests/Accounts/CompanySerializationTest.php index e9dffdf..1b5cad9 100644 --- a/test/Checkout/Tests/Accounts/CompanySerializationTest.php +++ b/test/Checkout/Tests/Accounts/CompanySerializationTest.php @@ -7,12 +7,14 @@ use Checkout\Accounts\CompanyPosition; use Checkout\Accounts\DateOfBirth; use Checkout\Accounts\DateOfIncorporation; +use Checkout\Accounts\EntityFinancialDetails; use Checkout\Accounts\EntityRoles; use Checkout\Accounts\PlaceOfBirth; use Checkout\Accounts\Representative; use Checkout\Accounts\RepresentativeIndividual; use Checkout\Common\Address; use Checkout\Common\Country; +use Checkout\Common\Currency; use Checkout\JsonSerializer; use PHPUnit\Framework\TestCase; @@ -31,9 +33,12 @@ public function testCompanyWithV3RepresentativeRoundTrip() $company->legal_name = "Super Hero Masks Inc."; $company->trading_name = "Super Hero Masks"; $company->additional_trading_names = array("SHM"); - $company->is_registered_company = true; + // The spec enum is [false] only: the flag exists to declare an unregistered company. + $company->is_registered_company = false; $company->regulatory_licence_number = "REG-123456"; $company->date_of_incorporation = $dateOfIncorporation; + $company->principal_address = $this->buildAddress("1 Principal Street", "W1T 4TJ"); + $company->registered_address = $this->buildAddress("2 Registered Street", "EC1A 1BB"); $company->representatives = array($this->buildRepresentative()); $decoded = json_decode((new JsonSerializer())->serialize($company), true); @@ -43,9 +48,29 @@ public function testCompanyWithV3RepresentativeRoundTrip() $this->assertSame("Super Hero Masks Inc.", $decoded['legal_name']); $this->assertSame("Super Hero Masks", $decoded['trading_name']); $this->assertSame(array("SHM"), $decoded['additional_trading_names']); - $this->assertTrue($decoded['is_registered_company']); + // false must be sent, not dropped like null. + $this->assertArrayHasKey('is_registered_company', $decoded); + $this->assertFalse($decoded['is_registered_company']); $this->assertSame("REG-123456", $decoded['regulatory_licence_number']); - $this->assertSame(2010, $decoded['date_of_incorporation']['year']); + $this->assertSame(array("day" => 1, "month" => 6, "year" => 2010), $decoded['date_of_incorporation']); + $this->assertSame( + array( + "address_line1" => "1 Principal Street", + "city" => "London", + "zip" => "W1T 4TJ", + "country" => "GB", + ), + $decoded['principal_address'] + ); + $this->assertSame( + array( + "address_line1" => "2 Registered Street", + "city" => "London", + "zip" => "EC1A 1BB", + "country" => "GB", + ), + $decoded['registered_address'] + ); $representative = $decoded['representatives'][0]; $this->assertSame("John", $representative['individual']['first_name']); @@ -58,6 +83,35 @@ public function testCompanyWithV3RepresentativeRoundTrip() ); } + /** + * company.financial_details exists in the EEA and US Company (2.0) variants, with these four + * fields. + */ + public function testCompanyV2FinancialDetailsRoundTrip() + { + $financialDetails = new EntityFinancialDetails(); + $financialDetails->annual_processing_volume = 120000; + $financialDetails->average_transaction_value = 500; + $financialDetails->highest_transaction_value = 2500; + $financialDetails->currency = Currency::$USD; + + $company = new Company(); + $company->legal_name = "Super Hero Masks Inc."; + $company->financial_details = $financialDetails; + + $decoded = json_decode((new JsonSerializer())->serialize($company), true); + + $this->assertSame( + array( + "annual_processing_volume" => 120000, + "average_transaction_value" => 500, + "highest_transaction_value" => 2500, + "currency" => "USD", + ), + $decoded['financial_details'] + ); + } + public function testRolesSerializeToExactSwaggerValues() { $this->assertSame("ubo", EntityRoles::$ubo); @@ -70,21 +124,51 @@ public function testRolesSerializeToExactSwaggerValues() public function testBusinessTypeValuesMatchSwagger() { $this->assertSame("individual_or_sole_proprietorship", BusinessType::$individual_or_sole_proprietorship); + $this->assertSame("general_partnership", BusinessType::$general_partnership); + $this->assertSame("limited_partnership", BusinessType::$limited_partnership); + $this->assertSame("scottish_limited_partnership", BusinessType::$scottish_limited_partnership); + $this->assertSame("public_limited_company", BusinessType::$public_limited_company); + $this->assertSame("limited_company", BusinessType::$limited_company); $this->assertSame("limited_liability_corporation", BusinessType::$limited_liability_corporation); + $this->assertSame("private_corporation", BusinessType::$private_corporation); + $this->assertSame("publicly_traded_corporation", BusinessType::$publicly_traded_corporation); + $this->assertSame("professional_association", BusinessType::$professional_association); + $this->assertSame("unincorporated_association", BusinessType::$unincorporated_association); + $this->assertSame("auto_entrepreneur", BusinessType::$auto_entrepreneur); $this->assertSame("government_agency", BusinessType::$government_agency); $this->assertSame("non_profit_entity", BusinessType::$non_profit_entity); $this->assertSame("trust", BusinessType::$trust); - $this->assertSame("scottish_limited_partnership", BusinessType::$scottish_limited_partnership); + $this->assertSame("club_or_society", BusinessType::$club_or_society); + $this->assertSame("regulated_financial_institution", BusinessType::$regulated_financial_institution); + $this->assertSame("cftc_registered_entity", BusinessType::$cftc_registered_entity); + $this->assertSame("sec_registered_entity", BusinessType::$sec_registered_entity); } public function testCompanyPositionValuesMatchSwagger() { $this->assertSame("ceo", CompanyPosition::$ceo); + $this->assertSame("cfo", CompanyPosition::$cfo); + $this->assertSame("coo", CompanyPosition::$coo); $this->assertSame("managing_member", CompanyPosition::$managing_member); $this->assertSame("general_partner", CompanyPosition::$general_partner); + $this->assertSame("president", CompanyPosition::$president); + $this->assertSame("vice_president", CompanyPosition::$vice_president); + $this->assertSame("treasurer", CompanyPosition::$treasurer); + $this->assertSame("other_senior_management", CompanyPosition::$other_senior_management); + $this->assertSame("other_executive_officer", CompanyPosition::$other_executive_officer); $this->assertSame("other_non_executive_non_senior", CompanyPosition::$other_non_executive_non_senior); } + private function buildAddress($line1, $zip) + { + $address = new Address(); + $address->address_line1 = $line1; + $address->city = "London"; + $address->zip = $zip; + $address->country = Country::$GB; + return $address; + } + private function buildRepresentative() { $dateOfBirth = new DateOfBirth(); diff --git a/test/Checkout/Tests/Accounts/ContactDetailsSerializationTest.php b/test/Checkout/Tests/Accounts/ContactDetailsSerializationTest.php new file mode 100644 index 0000000..8b7c8fa --- /dev/null +++ b/test/Checkout/Tests/Accounts/ContactDetailsSerializationTest.php @@ -0,0 +1,99 @@ +country_code = "FR"; + $phone->number = "0712345678"; + + $emailAddresses = new EntityEmailAddresses(); + $emailAddresses->primary = "owner@example.com"; + + $invitee = new Invitee(); + $invitee->email = "invitee@example.com"; + + $contactDetails = new ContactDetails(); + $contactDetails->phone = $phone; + $contactDetails->email_addresses = $emailAddresses; + $contactDetails->invitee = $invitee; + + $decoded = json_decode((new JsonSerializer())->serialize($contactDetails), true); + + $this->assertSame( + array( + "phone" => array("country_code" => "FR", "number" => "0712345678"), + "email_addresses" => array("primary" => "owner@example.com"), + "invitee" => array("email" => "invitee@example.com"), + ), + $decoded + ); + } + + public function testUsIsvSellerContactDetailsMatchSwaggerExample() + { + // contact_details from the US ISV Seller Company (3.0) request example in the spec, with a + // neutral primary address in place of the example's. + $swaggerExample = '{"phone":{"country_code":"US","number":"4155678900"},' + . '"email_addresses":{"primary":"owner@example.com",' + . '"pci_compliance_contact":"pci.contact@example.com"}}'; + + $phone = new Phone(); + $phone->country_code = "US"; + $phone->number = "4155678900"; + + $emailAddresses = new EntityEmailAddresses(); + $emailAddresses->primary = "owner@example.com"; + $emailAddresses->pci_compliance_contact = "pci.contact@example.com"; + + $contactDetails = new ContactDetails(); + $contactDetails->phone = $phone; + $contactDetails->email_addresses = $emailAddresses; + + $serialized = (new JsonSerializer())->serialize($contactDetails); + $decoded = json_decode($serialized, true); + + $this->assertSame("owner@example.com", $decoded["email_addresses"]["primary"]); + $this->assertSame("pci.contact@example.com", $decoded["email_addresses"]["pci_compliance_contact"]); + $this->assertArrayNotHasKey("invitee", $decoded); + $this->assertEquals(json_decode($swaggerExample, true), $decoded); + } + + /** + * In every v2.0 variant contact_details.phone defines number only; country_code exists only + * in v3.0. Leaving country_code unset must drop the key, not send it as null. + */ + public function testV2NumberOnlyPhoneOmitsCountryCode() + { + $phone = new Phone(); + $phone->number = "4155678900"; + + $emailAddresses = new EntityEmailAddresses(); + $emailAddresses->primary = "owner@example.com"; + + $contactDetails = new ContactDetails(); + $contactDetails->phone = $phone; + $contactDetails->email_addresses = $emailAddresses; + + $decoded = json_decode((new JsonSerializer())->serialize($contactDetails), true); + + $this->assertSame( + array( + "phone" => array("number" => "4155678900"), + "email_addresses" => array("primary" => "owner@example.com"), + ), + $decoded + ); + $this->assertArrayNotHasKey("country_code", $decoded["phone"]); + } +} diff --git a/test/Checkout/Tests/Accounts/IndividualSerializationTest.php b/test/Checkout/Tests/Accounts/IndividualSerializationTest.php new file mode 100644 index 0000000..1bcc323 --- /dev/null +++ b/test/Checkout/Tests/Accounts/IndividualSerializationTest.php @@ -0,0 +1,74 @@ +address_line1 = "123 Main Street"; + $address->address_line2 = "Suite 100"; + $address->city = "San Francisco"; + $address->state = "CA"; + $address->zip = "94105"; + $address->country = Country::$US; + + $dateOfBirth = new DateOfBirth(); + $dateOfBirth->day = 15; + $dateOfBirth->month = 1; + $dateOfBirth->year = 1990; + + $placeOfBirth = new PlaceOfBirth(); + $placeOfBirth->country = Country::$US; + + $identification = new Identification(); + $identification->national_id_number = "123456789"; + + $individual = new Individual(); + $individual->first_name = "John"; + $individual->middle_name = "Robert"; + $individual->last_name = "Trader"; + $individual->trading_name = "John's Goods"; + $individual->registered_address = $address; + $individual->date_of_birth = $dateOfBirth; + $individual->place_of_birth = $placeOfBirth; + $individual->identification = $identification; + + $decoded = json_decode((new JsonSerializer())->serialize($individual), true); + + $this->assertSame( + array( + "first_name" => "John", + "middle_name" => "Robert", + "last_name" => "Trader", + "trading_name" => "John's Goods", + "registered_address" => array( + "address_line1" => "123 Main Street", + "address_line2" => "Suite 100", + "city" => "San Francisco", + "state" => "CA", + "zip" => "94105", + "country" => "US", + ), + "date_of_birth" => array("day" => 15, "month" => 1, "year" => 1990), + "place_of_birth" => array("country" => "US"), + "identification" => array("national_id_number" => "123456789"), + ), + $decoded + ); + } +} diff --git a/test/Checkout/Tests/Accounts/OnboardEntityRequestSerializationTest.php b/test/Checkout/Tests/Accounts/OnboardEntityRequestSerializationTest.php index c8ca95a..24852d2 100644 --- a/test/Checkout/Tests/Accounts/OnboardEntityRequestSerializationTest.php +++ b/test/Checkout/Tests/Accounts/OnboardEntityRequestSerializationTest.php @@ -2,25 +2,344 @@ namespace Checkout\Tests\Accounts; +use Checkout\Accounts\AdditionalDocument; use Checkout\Accounts\AgreedTerms; use Checkout\Accounts\ArticlesOfAssociation; use Checkout\Accounts\ArticlesOfAssociationType; use Checkout\Accounts\BankVerification; use Checkout\Accounts\BankVerificationType; +use Checkout\Accounts\BusinessType; +use Checkout\Accounts\Citizenship; +use Checkout\Accounts\Company; +use Checkout\Accounts\CompanyPosition; use Checkout\Accounts\CompanyVerification; +use Checkout\Accounts\CompanyVerificationType; +use Checkout\Accounts\ContactDetails; +use Checkout\Accounts\DateOfBirth; +use Checkout\Accounts\DateOfIncorporation; use Checkout\Accounts\Document; +use Checkout\Accounts\EntityEmailAddresses; +use Checkout\Accounts\EntityRoles; use Checkout\Accounts\FinancialStatements; use Checkout\Accounts\FinancialStatementsType; +use Checkout\Accounts\FinancialVerification; +use Checkout\Accounts\FinancialVerificationType; +use Checkout\Accounts\NationalIdType; use Checkout\Accounts\OnboardEntityRequest; use Checkout\Accounts\OnboardSubEntityDocuments; +use Checkout\Accounts\PlaceOfBirth; +use Checkout\Accounts\ProcessingDetails; +use Checkout\Accounts\ProcessingDetailsAch; +use Checkout\Accounts\ProcessingDetailsPayments; +use Checkout\Accounts\Profile; +use Checkout\Accounts\ProofOfLegality; +use Checkout\Accounts\ProofOfLegalityType; +use Checkout\Accounts\ProofOfPrincipalAddress; +use Checkout\Accounts\ProofOfPrincipalAddressType; +use Checkout\Accounts\Representative; +use Checkout\Accounts\RepresentativeIndividual; use Checkout\Accounts\ShareholderStructure; use Checkout\Accounts\ShareholderStructureType; use Checkout\Accounts\TaxVerification; +use Checkout\Accounts\TaxVerificationType; +use Checkout\Common\Address; +use Checkout\Common\Country; +use Checkout\Common\Currency; +use Checkout\Common\DocumentType; +use Checkout\Common\Phone; use Checkout\JsonSerializer; use PHPUnit\Framework\TestCase; class OnboardEntityRequestSerializationTest extends TestCase { + // File IDs in the spec format ^file_[a-z2-7]{26}$, keyed by the document field they are sent on. + private const FILE_IDS = array( + "identity_verification" => "file_identityverificationaaaaaa", + "identity_back" => "file_identitybackaaaaaaaaaaaaaa", + "company_verification" => "file_companyverificationaaaaaaa", + "tax_verification" => "file_taxverificationaaaaaaaaaaa", + "articles_of_association" => "file_articlesofassociationaaaaa", + "shareholder_structure" => "file_shareholderstructureaaaaaa", + "bank_verification" => "file_bankverificationaaaaaaaaaa", + "financial_statements" => "file_financialstatementsaaaaaaa", + "financial_verification" => "file_financialverificationaaaaa", + "proof_of_principal_address" => "file_proofofprincipaladdressaaa", + "proof_of_legality" => "file_proofoflegalityaaaaaaaaaaa", + "additional_document1" => "file_additionaldocumentoneaaaaa", + "additional_document2" => "file_additionaldocumenttwoaaaaa", + "additional_document3" => "file_additionaldocumentthreeaaa", + ); + + /** + * The US ISV Seller Company (3.0) and US ISV Seller Sole Trader (3.0) request examples, + * verbatim from components.schemas["USISVSellerCompany3-0"].example and + * components.schemas["USISVSellerSoleTrader3-0"].example. Kept as raw JSON so a key renamed + * in the spec or in the models shows up as a test failure. + */ + private const US_ISV_SELLER_COMPANY_SWAGGER_EXAMPLE = <<<'JSON' +{ + "reference": "isv-seller-example001", + "agreed_terms": { + "date": "2026-07-02T10:30:00.0000000+00:00", + "ip_address": "8.8.8.8", + "name": "Toby Arden", + "email": "toby.arden@example.com", + "version": "cko-platform-terms-1.0.0" + }, + "seller_category": "cat_retail_001", + "processing_details": { + "annual_processing_volume": 1000, + "average_transaction_value": 2000, + "average_order_fulfillment_time": 3, + "target_countries": [ + "US" + ], + "currency": "USD", + "payments": { + "ach": { + "annual_ach_volume": 100000, + "average_ach_transaction_size": 5000, + "estimated_monthly_credit_volume": 50000, + "average_credit_amount": 2500 + } + } + }, + "contact_details": { + "phone": { + "number": "4155678900", + "country_code": "US" + }, + "email_addresses": { + "primary": "toby.arden@example.com", + "pci_compliance_contact": "pci.contact@example.com" + } + }, + "profile": { + "urls": [ + "https://www.isv-seller-example.com" + ], + "mccs": [ + "5551" + ], + "holding_currencies": [ + "USD" + ], + "default_holding_currency": "USD" + }, + "company": { + "business_registration_number": "12-3456789", + "business_type": "private_corporation", + "legal_name": "ISV Seller Example Inc", + "trading_name": "ISV Seller Example", + "registered_address": { + "address_line1": "123 Main Street", + "city": "San Francisco", + "state": "CA", + "zip": "94105", + "country": "US" + }, + "principal_address": { + "address_line1": "123 Main Street", + "city": "San Francisco", + "state": "CA", + "zip": "94105", + "country": "US" + }, + "date_of_incorporation": { + "year": 2025, + "month": 10, + "day": 1 + }, + "representatives": [ + { + "roles": [ + "ubo", + "control_person" + ], + "ownership_percentage": 25, + "company_position": "ceo", + "individual": { + "first_name": "Toby", + "last_name": "Arden", + "email_address": "toby.arden@example.com", + "national_id_type": "ssn", + "national_id_number": "123456789", + "date_of_birth": { + "day": 15, + "month": 1, + "year": 1990 + }, + "place_of_birth": { + "country": "US" + }, + "citizenships": [ + { + "country": "US" + } + ], + "phone": { + "country_code": "US", + "number": "4155678901" + }, + "address": { + "address_line1": "123 Main Street", + "city": "San Francisco", + "state": "CA", + "zip": "94105", + "country": "US" + } + } + }, + { + "roles": [ + "authorised_signatory" + ], + "individual": { + "first_name": "Alex", + "last_name": "Morgan", + "email_address": "alex.morgan@example.com", + "national_id_type": "ssn", + "national_id_number": "987654321", + "date_of_birth": { + "day": 22, + "month": 6, + "year": 1985 + }, + "place_of_birth": { + "country": "US" + }, + "citizenships": [ + { + "country": "US" + } + ], + "phone": { + "country_code": "US", + "number": "4155678902" + }, + "address": { + "address_line1": "123 Main Street", + "city": "San Francisco", + "state": "CA", + "zip": "94105", + "country": "US" + } + } + } + ] + } +} +JSON; + + private const US_ISV_SELLER_SOLE_TRADER_SWAGGER_EXAMPLE = <<<'JSON' +{ + "reference": "isv-sole-trader-example001", + "agreed_terms": { + "date": "2026-07-02T10:30:00.0000000+00:00", + "ip_address": "8.8.8.8", + "name": "Hannah Bret", + "email": "hannah.bret@example.com", + "version": "cko-platform-terms-1.0.0" + }, + "seller_category": "cat_retail_001", + "processing_details": { + "annual_processing_volume": 1000, + "average_transaction_value": 2000, + "average_order_fulfillment_time": 3, + "target_countries": [ + "US" + ], + "currency": "USD", + "payments": { + "ach": { + "annual_ach_volume": 100000, + "average_ach_transaction_size": 5000, + "estimated_monthly_credit_volume": 50000, + "average_credit_amount": 2500 + } + } + }, + "contact_details": { + "phone": { + "number": "4155678900", + "country_code": "US" + }, + "email_addresses": { + "primary": "hannah.bret@example.com", + "pci_compliance_contact": "pci.contact@example.com" + } + }, + "profile": { + "urls": [ + "https://www.isv-sole-trader-example.com" + ], + "mccs": [ + "5551" + ], + "holding_currencies": [ + "USD" + ], + "default_holding_currency": "USD" + }, + "company": { + "business_type": "individual_or_sole_proprietorship", + "is_registered_company": false, + "trading_name": "Hannah's Goods", + "date_of_incorporation": { + "year": 2025, + "month": 10, + "day": 1 + }, + "principal_address": { + "address_line1": "123 Main Street", + "city": "San Francisco", + "state": "CA", + "zip": "94105", + "country": "US" + }, + "representatives": [ + { + "roles": [ + "ubo" + ], + "ownership_percentage": 100, + "individual": { + "first_name": "Hannah", + "last_name": "Bret", + "email_address": "hannah.bret@example.com", + "national_id_type": "ssn", + "national_id_number": "123456789", + "date_of_birth": { + "day": 15, + "month": 1, + "year": 1990 + }, + "place_of_birth": { + "country": "US" + }, + "citizenships": [ + { + "country": "US" + } + ], + "phone": { + "country_code": "US", + "number": "4155678901" + }, + "address": { + "address_line1": "123 Main Street", + "city": "San Francisco", + "state": "CA", + "zip": "94105", + "country": "US" + } + } + } + ] + } +} +JSON; + public function testOnboardEntityRequestRoundTrip() { $agreedTerms = new AgreedTerms(); @@ -50,34 +369,138 @@ public function testDocumentsRoundTripCoversEveryField() { $decoded = json_decode((new JsonSerializer())->serialize($this->buildDocuments()), true); - // Every property of OnboardSubEntityDocuments must serialize with its type + front. + // Every typed property of OnboardSubEntityDocuments must serialize with its spec type + front. $expectedTypes = array( - "identity_verification" => "identity_card", - "company_verification" => "certificate_of_incorporation", - "tax_verification" => "tax_document", + "identity_verification" => "passport", + "company_verification" => "incorporation_document", + "tax_verification" => "ein_letter", "articles_of_association" => "articles_of_association", "shareholder_structure" => "certified_shareholder_structure", "bank_verification" => "bank_statement", "financial_statements" => "financial_statements", - "financial_verification" => "financial_verification", - "proof_of_principal_address" => "utility_bill", + "financial_verification" => "financial_statement", + "proof_of_principal_address" => "proof_of_address", "proof_of_legality" => "proof_of_legality", - "additional_document1" => "additional_document", - "additional_document2" => "additional_document", - "additional_document3" => "additional_document", ); foreach ($expectedTypes as $field => $type) { $this->assertArrayHasKey($field, $decoded, "documents.$field must serialize"); $this->assertSame($type, $decoded[$field]['type'], "documents.$field.type"); - $this->assertSame("file_$field", $decoded[$field]['front'], "documents.$field.front"); + $this->assertSame(self::FILE_IDS[$field], $decoded[$field]['front'], "documents.$field.front"); } - // identity_verification is a Document (supports back); the others carry type + front only. - // articles_of_association, shareholder_structure and bank_verification are their own - // classes rather than the generic Document, so this also proves the retyped fields - // still serialize to the same JSON. - $this->assertSame("back_id", $decoded['identity_verification']['back']); + // identity_verification is the only top-level document with a back side. + $this->assertSame(self::FILE_IDS["identity_back"], $decoded['identity_verification']['back']); + foreach (array_keys($expectedTypes) as $field) { + if ($field !== "identity_verification") { + $this->assertArrayNotHasKey("back", $decoded[$field], "documents.$field has no back"); + } + } + + // The additional documents carry a file ID only: the API defines no type for them. + foreach (array("additional_document1", "additional_document2", "additional_document3") as $field) { + $this->assertSame(array("front" => self::FILE_IDS[$field]), $decoded[$field], "documents.$field"); + } + } + + public function testUsIsvSellerCompanyMatchesSwaggerExample() + { + $dateOfIncorporation = new DateOfIncorporation(); + $dateOfIncorporation->year = 2025; + $dateOfIncorporation->month = 10; + $dateOfIncorporation->day = 1; + + $controlPerson = new Representative(); + $controlPerson->roles = array(EntityRoles::$ubo, EntityRoles::$control_person); + $controlPerson->ownership_percentage = 25; + $controlPerson->company_position = CompanyPosition::$ceo; + $controlPerson->individual = $this->usIsvRepresentativeIndividual( + "Toby", + "Arden", + "toby.arden@example.com", + "123456789", + $this->dateOfBirth(15, 1, 1990), + "4155678901" + ); + + $signatory = new Representative(); + $signatory->roles = array(EntityRoles::$authorised_signatory); + $signatory->individual = $this->usIsvRepresentativeIndividual( + "Alex", + "Morgan", + "alex.morgan@example.com", + "987654321", + $this->dateOfBirth(22, 6, 1985), + "4155678902" + ); + + $company = new Company(); + $company->business_registration_number = "12-3456789"; + $company->business_type = BusinessType::$private_corporation; + $company->legal_name = "ISV Seller Example Inc"; + $company->trading_name = "ISV Seller Example"; + $company->registered_address = $this->usIsvAddress(); + $company->principal_address = $this->usIsvAddress(); + $company->date_of_incorporation = $dateOfIncorporation; + $company->representatives = array($controlPerson, $signatory); + + $request = new OnboardEntityRequest(); + $request->reference = "isv-seller-example001"; + $request->agreed_terms = $this->usIsvAgreedTerms("Toby Arden", "toby.arden@example.com"); + $request->seller_category = "cat_retail_001"; + $request->processing_details = $this->usIsvProcessingDetails(); + $request->contact_details = $this->usIsvContactDetails("toby.arden@example.com"); + $request->profile = $this->usIsvProfile("https://www.isv-seller-example.com"); + $request->company = $company; + + $this->assertEquals( + json_decode(self::US_ISV_SELLER_COMPANY_SWAGGER_EXAMPLE, true), + json_decode((new JsonSerializer())->serialize($request), true) + ); + } + + public function testUsIsvSellerSoleTraderMatchesSwaggerExample() + { + $dateOfIncorporation = new DateOfIncorporation(); + $dateOfIncorporation->year = 2025; + $dateOfIncorporation->month = 10; + $dateOfIncorporation->day = 1; + + $owner = new Representative(); + $owner->roles = array(EntityRoles::$ubo); + $owner->ownership_percentage = 100; + $owner->individual = $this->usIsvRepresentativeIndividual( + "Hannah", + "Bret", + "hannah.bret@example.com", + "123456789", + $this->dateOfBirth(15, 1, 1990), + "4155678901" + ); + + $company = new Company(); + $company->business_type = BusinessType::$individual_or_sole_proprietorship; + $company->is_registered_company = false; + $company->trading_name = "Hannah's Goods"; + $company->date_of_incorporation = $dateOfIncorporation; + $company->principal_address = $this->usIsvAddress(); + $company->representatives = array($owner); + + $request = new OnboardEntityRequest(); + $request->reference = "isv-sole-trader-example001"; + $request->agreed_terms = $this->usIsvAgreedTerms("Hannah Bret", "hannah.bret@example.com"); + $request->seller_category = "cat_retail_001"; + $request->processing_details = $this->usIsvProcessingDetails(); + $request->contact_details = $this->usIsvContactDetails("hannah.bret@example.com"); + $request->profile = $this->usIsvProfile("https://www.isv-sole-trader-example.com"); + $request->company = $company; + + $decoded = json_decode((new JsonSerializer())->serialize($request), true); + + $this->assertEquals(json_decode(self::US_ISV_SELLER_SOLE_TRADER_SWAGGER_EXAMPLE, true), $decoded); + // false is a value the API requires here, so it must be sent, not dropped like null. + $this->assertArrayHasKey("is_registered_company", $decoded["company"]); + $this->assertFalse($decoded["company"]["is_registered_company"]); } private function buildDocuments() @@ -85,56 +508,172 @@ private function buildDocuments() $documents = new OnboardSubEntityDocuments(); $identity = new Document(); - $identity->type = "identity_card"; - $identity->front = "file_identity_verification"; - $identity->back = "back_id"; + $identity->type = DocumentType::$passport; + $identity->front = self::FILE_IDS["identity_verification"]; + $identity->back = self::FILE_IDS["identity_back"]; $documents->identity_verification = $identity; $companyVerification = new CompanyVerification(); - $companyVerification->type = "certificate_of_incorporation"; - $companyVerification->front = "file_company_verification"; + $companyVerification->type = CompanyVerificationType::$incorporation_document; + $companyVerification->front = self::FILE_IDS["company_verification"]; $documents->company_verification = $companyVerification; $taxVerification = new TaxVerification(); - $taxVerification->type = "tax_document"; - $taxVerification->front = "file_tax_verification"; + $taxVerification->type = TaxVerificationType::$ein_letter; + $taxVerification->front = self::FILE_IDS["tax_verification"]; $documents->tax_verification = $taxVerification; $articlesOfAssociation = new ArticlesOfAssociation(); $articlesOfAssociation->type = ArticlesOfAssociationType::$articles_of_association; - $articlesOfAssociation->front = "file_articles_of_association"; + $articlesOfAssociation->front = self::FILE_IDS["articles_of_association"]; $documents->articles_of_association = $articlesOfAssociation; $shareholderStructure = new ShareholderStructure(); $shareholderStructure->type = ShareholderStructureType::$certified_shareholder_structure; - $shareholderStructure->front = "file_shareholder_structure"; + $shareholderStructure->front = self::FILE_IDS["shareholder_structure"]; $documents->shareholder_structure = $shareholderStructure; $bankVerification = new BankVerification(); $bankVerification->type = BankVerificationType::$bank_statement; - $bankVerification->front = "file_bank_verification"; + $bankVerification->front = self::FILE_IDS["bank_verification"]; $documents->bank_verification = $bankVerification; $financialStatements = new FinancialStatements(); $financialStatements->type = FinancialStatementsType::$financial_statements; - $financialStatements->front = "file_financial_statements"; + $financialStatements->front = self::FILE_IDS["financial_statements"]; $documents->financial_statements = $financialStatements; - $documents->financial_verification = $this->document("financial_verification", "financial_verification"); - $documents->proof_of_principal_address = $this->document("proof_of_principal_address", "utility_bill"); - $documents->proof_of_legality = $this->document("proof_of_legality", "proof_of_legality"); - $documents->additional_document1 = $this->document("additional_document1", "additional_document"); - $documents->additional_document2 = $this->document("additional_document2", "additional_document"); - $documents->additional_document3 = $this->document("additional_document3", "additional_document"); + $financialVerification = new FinancialVerification(); + $financialVerification->type = FinancialVerificationType::$financial_statement; + $financialVerification->front = self::FILE_IDS["financial_verification"]; + $documents->financial_verification = $financialVerification; + + $proofOfPrincipalAddress = new ProofOfPrincipalAddress(); + $proofOfPrincipalAddress->type = ProofOfPrincipalAddressType::$proof_of_address; + $proofOfPrincipalAddress->front = self::FILE_IDS["proof_of_principal_address"]; + $documents->proof_of_principal_address = $proofOfPrincipalAddress; + + $proofOfLegality = new ProofOfLegality(); + $proofOfLegality->type = ProofOfLegalityType::$proof_of_legality; + $proofOfLegality->front = self::FILE_IDS["proof_of_legality"]; + $documents->proof_of_legality = $proofOfLegality; + + $documents->additional_document1 = $this->additionalDocument(self::FILE_IDS["additional_document1"]); + $documents->additional_document2 = $this->additionalDocument(self::FILE_IDS["additional_document2"]); + $documents->additional_document3 = $this->additionalDocument(self::FILE_IDS["additional_document3"]); return $documents; } - private function document($field, $type) + private function additionalDocument($front) { - $document = new Document(); - $document->type = $type; - $document->front = "file_$field"; + $document = new AdditionalDocument(); + $document->front = $front; return $document; } + + private function usIsvAddress() + { + $address = new Address(); + $address->address_line1 = "123 Main Street"; + $address->city = "San Francisco"; + $address->state = "CA"; + $address->zip = "94105"; + $address->country = Country::$US; + return $address; + } + + private function usIsvPhone($number) + { + $phone = new Phone(); + $phone->country_code = Country::$US; + $phone->number = $number; + return $phone; + } + + private function dateOfBirth($day, $month, $year) + { + $dateOfBirth = new DateOfBirth(); + $dateOfBirth->day = $day; + $dateOfBirth->month = $month; + $dateOfBirth->year = $year; + return $dateOfBirth; + } + + private function usIsvRepresentativeIndividual($firstName, $lastName, $email, $ssn, $dateOfBirth, $phoneNumber) + { + $placeOfBirth = new PlaceOfBirth(); + $placeOfBirth->country = Country::$US; + + // The spec examples send citizenships[].country only, with no type. + $citizenship = new Citizenship(); + $citizenship->country = Country::$US; + + $individual = new RepresentativeIndividual(); + $individual->first_name = $firstName; + $individual->last_name = $lastName; + $individual->email_address = $email; + $individual->national_id_type = NationalIdType::$ssn; + $individual->national_id_number = $ssn; + $individual->date_of_birth = $dateOfBirth; + $individual->place_of_birth = $placeOfBirth; + $individual->citizenships = array($citizenship); + $individual->phone = $this->usIsvPhone($phoneNumber); + $individual->address = $this->usIsvAddress(); + return $individual; + } + + private function usIsvAgreedTerms($name, $email) + { + $agreedTerms = new AgreedTerms(); + $agreedTerms->date = "2026-07-02T10:30:00.0000000+00:00"; + $agreedTerms->ip_address = "8.8.8.8"; + $agreedTerms->name = $name; + $agreedTerms->email = $email; + $agreedTerms->version = "cko-platform-terms-1.0.0"; + return $agreedTerms; + } + + private function usIsvProcessingDetails() + { + $ach = new ProcessingDetailsAch(); + $ach->annual_ach_volume = 100000; + $ach->average_ach_transaction_size = 5000; + $ach->estimated_monthly_credit_volume = 50000; + $ach->average_credit_amount = 2500; + + $payments = new ProcessingDetailsPayments(); + $payments->ach = $ach; + + $processingDetails = new ProcessingDetails(); + $processingDetails->annual_processing_volume = 1000; + $processingDetails->average_transaction_value = 2000; + $processingDetails->average_order_fulfillment_time = 3; + $processingDetails->target_countries = array(Country::$US); + $processingDetails->currency = Currency::$USD; + $processingDetails->payments = $payments; + return $processingDetails; + } + + private function usIsvContactDetails($primaryEmail) + { + $emailAddresses = new EntityEmailAddresses(); + $emailAddresses->primary = $primaryEmail; + $emailAddresses->pci_compliance_contact = "pci.contact@example.com"; + + $contactDetails = new ContactDetails(); + $contactDetails->phone = $this->usIsvPhone("4155678900"); + $contactDetails->email_addresses = $emailAddresses; + return $contactDetails; + } + + private function usIsvProfile($url) + { + $profile = new Profile(); + $profile->urls = array($url); + $profile->mccs = array("5551"); + $profile->holding_currencies = array(Currency::$USD); + $profile->default_holding_currency = Currency::$USD; + return $profile; + } } diff --git a/test/Checkout/Tests/Accounts/OnboardingDocumentTypeTest.php b/test/Checkout/Tests/Accounts/OnboardingDocumentTypeTest.php index e20d6e4..9500d41 100644 --- a/test/Checkout/Tests/Accounts/OnboardingDocumentTypeTest.php +++ b/test/Checkout/Tests/Accounts/OnboardingDocumentTypeTest.php @@ -4,6 +4,14 @@ use Checkout\Accounts\ArticlesOfAssociationType; use Checkout\Accounts\BankVerificationType; +use Checkout\Accounts\CertifiedAuthorisedSignatoryType; +use Checkout\Accounts\CompanyVerificationType; +use Checkout\Accounts\FinancialStatementsType; +use Checkout\Accounts\FinancialVerificationType; +use Checkout\Accounts\ProofOfLegalityType; +use Checkout\Accounts\ProofOfPrincipalAddressType; +use Checkout\Accounts\ProofOfRegistrationType; +use Checkout\Accounts\ProofOfResidentialAddressType; use Checkout\Accounts\ShareholderStructureType; use Checkout\Common\DocumentType; use PHPUnit\Framework\TestCase; @@ -27,6 +35,38 @@ public function testExposesTheOnboardingDocumentTypes() $this->assertSame("memorandum_of_association", ArticlesOfAssociationType::$memorandum_of_association); $this->assertSame("articles_of_association", ArticlesOfAssociationType::$articles_of_association); $this->assertSame("certified_shareholder_structure", ShareholderStructureType::$certified_shareholder_structure); + $this->assertSame("proof_of_address", ProofOfPrincipalAddressType::$proof_of_address); + $this->assertSame("proof_of_legality", ProofOfLegalityType::$proof_of_legality); + $this->assertSame("financial_statement", FinancialVerificationType::$financial_statement); + $this->assertSame("financial_statements", FinancialStatementsType::$financial_statements); + $this->assertSame("incorporation_document", CompanyVerificationType::$incorporation_document); + $this->assertSame("articles_of_association", CompanyVerificationType::$articles_of_association); + } + + /** + * The identity_verification document types, the same six values in every variant. + */ + public function testExposesTheIdentityDocumentTypes() + { + $this->assertSame("passport", DocumentType::$passport); + $this->assertSame("national_identity_card", DocumentType::$national_identity_card); + $this->assertSame("driving_license", DocumentType::$driving_license); + $this->assertSame("citizen_card", DocumentType::$citizen_card); + $this->assertSame("residence_permit", DocumentType::$residence_permit); + $this->assertSame("electoral_id", DocumentType::$electoral_id); + } + + /** + * The document types of the representative documents (company.representatives[].documents). + * proof_of_address appears on both ProofOfResidentialAddressType and ProofOfPrincipalAddressType + * on purpose: the spec defines them as two enums on two different documents. + */ + public function testExposesTheRepresentativeDocumentTypes() + { + $this->assertSame("proof_of_address", ProofOfResidentialAddressType::$proof_of_address); + $this->assertSame("extract_from_trade_register", ProofOfRegistrationType::$extract_from_trade_register); + $this->assertSame("other", ProofOfRegistrationType::$other); + $this->assertSame("power_of_attorney", CertifiedAuthorisedSignatoryType::$power_of_attorney); } /** @@ -48,5 +88,8 @@ public function testKeepsOnboardingTypesOutOfTheIdentityDocumentType() $this->assertNotContains("bank_statement", $identity); $this->assertNotContains("articles_of_association", $identity); $this->assertNotContains("certified_shareholder_structure", $identity); + $this->assertNotContains("proof_of_address", $identity); + $this->assertNotContains("extract_from_trade_register", $identity); + $this->assertNotContains("power_of_attorney", $identity); } } diff --git a/test/Checkout/Tests/Accounts/RepresentativeDocumentsSerializationTest.php b/test/Checkout/Tests/Accounts/RepresentativeDocumentsSerializationTest.php new file mode 100644 index 0000000..451dc46 --- /dev/null +++ b/test/Checkout/Tests/Accounts/RepresentativeDocumentsSerializationTest.php @@ -0,0 +1,202 @@ +individual = $this->buildIndividual(); + $representative->roles = array(EntityRoles::$ubo); + $representative->documents = $this->buildEeaSoleTraderDocuments(); + + $company = new Company(); + $company->trading_name = "Test Sole Trader"; + $company->business_type = BusinessType::$individual_or_sole_proprietorship; + $company->representatives = array($representative); + + $bankVerification = new BankVerification(); + $bankVerification->type = BankVerificationType::$bank_statement; + $bankVerification->front = "file_bankverificationaaaaaaaaaa"; + + $request = new OnboardEntityRequest(); + $request->reference = "ref_sole_trader"; + $request->company = $company; + $request->documents = new OnboardSubEntityDocuments(); + $request->documents->bank_verification = $bankVerification; + + $body = (new JsonSerializer())->serialize($request); + $decoded = json_decode($body, true); + + $this->assertSame( + array( + "identity_verification" => array( + "type" => "passport", + "front" => "file_identityverificationaaaaaa", + ), + "proof_of_residential_address" => array( + "type" => "proof_of_address", + "front" => "file_proofofresidentialaddressa", + ), + "proof_of_registration" => array( + "type" => "extract_from_trade_register", + "front" => "file_proofofregistrationaaaaaaa", + ), + ), + $decoded['company']['representatives'][0]['documents'] + ); + + // The representative documents stay on the representative; the top level carries only + // bank_verification. + $this->assertSame( + array("bank_verification" => array( + "type" => "bank_statement", + "front" => "file_bankverificationaaaaaaaaaa", + )), + $decoded['documents'] + ); + + // Key-level check on the raw body, so a naming change cannot pass silently. + $this->assertStringContainsString('"proof_of_residential_address":{', $body); + $this->assertStringContainsString('"proof_of_registration":{', $body); + } + + public function testCertifiedAuthorisedSignatoryOnCompanyRepresentative() + { + $signatory = new CertifiedAuthorisedSignatory(); + $signatory->type = CertifiedAuthorisedSignatoryType::$power_of_attorney; + $signatory->front = "file_signatoryaaaaaaaaaaaaaaaaa"; + + $documents = new RepresentativeDocuments(); + $documents->certified_authorised_signatory = $signatory; + + $representative = new Representative(); + $representative->roles = array(EntityRoles::$legal_representative); + $representative->documents = $documents; + + $decoded = json_decode((new JsonSerializer())->serialize($representative), true); + + $this->assertSame( + array("certified_authorised_signatory" => array( + "type" => "power_of_attorney", + "front" => "file_signatoryaaaaaaaaaaaaaaaaa", + )), + $decoded['documents'] + ); + } + + /** + * These four are the only keys company.representatives[].documents defines in any v3.0 + * variant. The EEA, GB and US Company - Full (3.0) Person of Interest representatives and the + * EEA, GB and US Sole Trader - Full (3.0) variants reject any other key + * (additionalProperties: false), so a property added here by mistake would fail those requests. + */ + public function testRepresentativeDocumentsDeclaresOnlyTheKeysTheApiAccepts() + { + $properties = array_map( + function (ReflectionProperty $property) { + return $property->getName(); + }, + (new ReflectionClass(RepresentativeDocuments::class))->getProperties() + ); + + $this->assertSame( + array( + "identity_verification", + "certified_authorised_signatory", + "proof_of_residential_address", + "proof_of_registration", + ), + $properties + ); + } + + /** + * The typed model is additive: an associative array assigned to Representative::$documents, + * as used before RepresentativeDocuments existed, still produces byte-identical JSON. + */ + public function testAssociativeArrayAndTypedModelSerializeIdentically() + { + $typed = new Representative(); + $typed->roles = array(EntityRoles::$ubo); + $typed->documents = $this->buildEeaSoleTraderDocuments(); + + $array = new Representative(); + $array->roles = array(EntityRoles::$ubo); + $array->documents = array( + "identity_verification" => array( + "type" => "passport", + "front" => "file_identityverificationaaaaaa", + ), + "proof_of_residential_address" => array( + "type" => "proof_of_address", + "front" => "file_proofofresidentialaddressa", + ), + "proof_of_registration" => array( + "type" => "extract_from_trade_register", + "front" => "file_proofofregistrationaaaaaaa", + ), + ); + + $serializer = new JsonSerializer(); + $this->assertSame($serializer->serialize($array), $serializer->serialize($typed)); + } + + private function buildEeaSoleTraderDocuments() + { + $identity = new Document(); + $identity->type = DocumentType::$passport; + $identity->front = "file_identityverificationaaaaaa"; + + $proofOfResidentialAddress = new ProofOfResidentialAddress(); + $proofOfResidentialAddress->type = ProofOfResidentialAddressType::$proof_of_address; + $proofOfResidentialAddress->front = "file_proofofresidentialaddressa"; + + $proofOfRegistration = new ProofOfRegistration(); + $proofOfRegistration->type = ProofOfRegistrationType::$extract_from_trade_register; + $proofOfRegistration->front = "file_proofofregistrationaaaaaaa"; + + $documents = new RepresentativeDocuments(); + $documents->identity_verification = $identity; + $documents->proof_of_residential_address = $proofOfResidentialAddress; + $documents->proof_of_registration = $proofOfRegistration; + return $documents; + } + + private function buildIndividual() + { + $individual = new RepresentativeIndividual(); + $individual->first_name = "Jane"; + $individual->last_name = "Doe"; + return $individual; + } +} diff --git a/test/Checkout/Tests/Accounts/RepresentativeIndividualSerializationTest.php b/test/Checkout/Tests/Accounts/RepresentativeIndividualSerializationTest.php index 01e7b18..9036a8c 100644 --- a/test/Checkout/Tests/Accounts/RepresentativeIndividualSerializationTest.php +++ b/test/Checkout/Tests/Accounts/RepresentativeIndividualSerializationTest.php @@ -54,17 +54,37 @@ public function testRepresentativeIndividualRoundTrip() $decoded = json_decode((new JsonSerializer())->serialize($individual), true); - $this->assertSame("John", $decoded['first_name']); - $this->assertSame("Robert", $decoded['middle_name']); - $this->assertSame("Representative", $decoded['last_name']); - $this->assertSame("citizenship", $decoded['citizenships'][0]['type']); - $this->assertSame("GB", $decoded['citizenships'][0]['country']); - $this->assertSame("ssn", $decoded['national_id_type']); - $this->assertSame("123456789", $decoded['national_id_number']); - $this->assertSame("john@example.com", $decoded['email_address']); - $this->assertSame(1996, $decoded['date_of_birth']['year']); - $this->assertSame("GB", $decoded['place_of_birth']['country']); - $this->assertSame("GB", $decoded['phone']['country_code']); - $this->assertSame("W1T 4TJ", $decoded['address']['zip']); + $this->assertSame( + array( + "first_name" => "John", + "middle_name" => "Robert", + "last_name" => "Representative", + "date_of_birth" => array("day" => 5, "month" => 6, "year" => 1996), + "place_of_birth" => array("country" => "GB"), + "citizenships" => array(array("type" => "citizenship", "country" => "GB")), + "national_id_type" => "ssn", + "national_id_number" => "123456789", + "email_address" => "john@example.com", + "phone" => array("country_code" => "GB", "number" => "2072343000"), + "address" => array( + "address_line1" => "CheckoutSdk.com", + "city" => "London", + "zip" => "W1T 4TJ", + "country" => "GB", + ), + ), + $decoded + ); + } + + public function testNationalIdTypeValuesMatchSwagger() + { + $this->assertSame("ssn", NationalIdType::$ssn); + $this->assertSame("itin", NationalIdType::$itin); + $this->assertSame("passport", NationalIdType::$passport); + $this->assertSame("driving_license", NationalIdType::$driving_license); + $this->assertSame("national_id_card", NationalIdType::$national_id_card); + $this->assertSame("residence_permit", NationalIdType::$residence_permit); + $this->assertSame("other", NationalIdType::$other); } } diff --git a/test/Checkout/Tests/Accounts/RepresentativeSerializationTest.php b/test/Checkout/Tests/Accounts/RepresentativeSerializationTest.php new file mode 100644 index 0000000..f59da11 --- /dev/null +++ b/test/Checkout/Tests/Accounts/RepresentativeSerializationTest.php @@ -0,0 +1,80 @@ +address_line1 = "123 Main Street"; + $address->city = "San Francisco"; + $address->state = "CA"; + $address->zip = "94105"; + $address->country = Country::$US; + + $identification = new Identification(); + $identification->national_id_number = "123456789"; + + $phone = new Phone(); + $phone->number = "4155678901"; + + $dateOfBirth = new DateOfBirth(); + $dateOfBirth->day = 15; + $dateOfBirth->month = 1; + $dateOfBirth->year = 1990; + + $placeOfBirth = new PlaceOfBirth(); + $placeOfBirth->country = Country::$US; + + $representative = new Representative(); + $representative->id = "rep_l2wbgkhgxsoeppr5ffaplhhbh4"; + $representative->first_name = "John"; + $representative->last_name = "Representative"; + $representative->address = $address; + $representative->identification = $identification; + $representative->phone = $phone; + $representative->date_of_birth = $dateOfBirth; + $representative->place_of_birth = $placeOfBirth; + $representative->roles = array(EntityRoles::$ubo); + + $decoded = json_decode((new JsonSerializer())->serialize($representative), true); + + $this->assertMatchesRegularExpression('/^rep_[a-z0-9]{26}$/', $decoded['id']); + $this->assertSame( + array( + "id" => "rep_l2wbgkhgxsoeppr5ffaplhhbh4", + "roles" => array("ubo"), + "first_name" => "John", + "last_name" => "Representative", + "address" => array( + "address_line1" => "123 Main Street", + "city" => "San Francisco", + "state" => "CA", + "zip" => "94105", + "country" => "US", + ), + "identification" => array("national_id_number" => "123456789"), + "phone" => array("number" => "4155678901"), + "date_of_birth" => array("day" => 15, "month" => 1, "year" => 1990), + "place_of_birth" => array("country" => "US"), + ), + $decoded + ); + } +} diff --git a/test/Checkout/Tests/Accounts/UploadFileRequestSerializationTest.php b/test/Checkout/Tests/Accounts/UploadFileRequestSerializationTest.php new file mode 100644 index 0000000..24def87 --- /dev/null +++ b/test/Checkout/Tests/Accounts/UploadFileRequestSerializationTest.php @@ -0,0 +1,41 @@ +purpose = FilePurpose::$identity_verification; + + $this->assertSame('{"purpose":"identity_verification"}', (new JsonSerializer())->serialize($request)); + } + + /** + * The 14 values of PlatformsFileUpload.purpose in the spec. The spec schema named FilePurpose + * is the disputes enum, not this one. + */ + public function testFilePurposeValuesMatchSwagger() + { + $this->assertSame("additional_document", FilePurpose::$additional_document); + $this->assertSame("articles_of_association", FilePurpose::$articles_of_association); + $this->assertSame("bank_verification", FilePurpose::$bank_verification); + $this->assertSame("certified_authorised_signatory", FilePurpose::$certified_authorised_signatory); + $this->assertSame("company_ownership", FilePurpose::$company_ownership); + $this->assertSame("company_verification", FilePurpose::$company_verification); + $this->assertSame("financial_verification", FilePurpose::$financial_verification); + $this->assertSame("identity_verification", FilePurpose::$identity_verification); + $this->assertSame("proof_of_legality", FilePurpose::$proof_of_legality); + $this->assertSame("proof_of_principal_address", FilePurpose::$proof_of_principal_address); + $this->assertSame("shareholder_structure", FilePurpose::$shareholder_structure); + $this->assertSame("tax_verification", FilePurpose::$tax_verification); + $this->assertSame("proof_of_residential_address", FilePurpose::$proof_of_residential_address); + $this->assertSame("proof_of_registration", FilePurpose::$proof_of_registration); + } +}