diff --git a/build.gradle b/build.gradle index 1cd2bbb6..7c884df9 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ plugins { id 'maven-publish' id 'io.github.gradle-nexus.publish-plugin' version '2.0.0' id 'io.freefair.lombok' version '8.14.4' - id "org.sonarqube" version "7.3.1.8318" + id "org.sonarqube" version "7.5.0.8588" id 'jacoco' } diff --git a/src/main/java/com/checkout/accounts/AccountPhone.java b/src/main/java/com/checkout/accounts/AccountPhone.java index 770b3e0c..ccb5f8a0 100644 --- a/src/main/java/com/checkout/accounts/AccountPhone.java +++ b/src/main/java/com/checkout/accounts/AccountPhone.java @@ -6,14 +6,26 @@ import lombok.Data; import lombok.NoArgsConstructor; +/** + * A phone number on the Accounts API: the sub-entity's contact phone, or a representative's phone. + * See {@link ContactDetails} for the per-variant number format. + */ @Data @Builder @NoArgsConstructor @AllArgsConstructor public final class AccountPhone { + /** + * The ISO 3166-1 alpha-2 country where the number is registered, not the dialling code. + * [Required] on Accounts API v3.0; not part of the v2.0 schemas. + */ private CountryCode countryCode; + /** + * The phone number, without the country calling code. + * [Required] + */ private String number; } diff --git a/src/main/java/com/checkout/accounts/AccountsClient.java b/src/main/java/com/checkout/accounts/AccountsClient.java index f9a1a9ba..e91dd26d 100644 --- a/src/main/java/com/checkout/accounts/AccountsClient.java +++ b/src/main/java/com/checkout/accounts/AccountsClient.java @@ -18,10 +18,35 @@ public interface AccountsClient { + /** + * Uploads a file to the Files API (POST /files on the Files host), as a multipart request. The + * returned ID is what document {@code front} and {@code back} fields take. + * + * @param accountsFileRequest the path to the file, its content type, and its purpose + * @return the ID of the uploaded file + */ CompletableFuture submitFile(AccountsFileRequest accountsFileRequest); + /** + * Creates a file upload for a sub-entity (POST /entities/{entityId}/files on the Files host). + * The response carries the file ID and an upload link; the file content itself is sent to that + * link, not in this request. + * + * @param entityId the ID of the sub-entity + * @param fileUploadRequest the purpose of the file upload + * @return the file ID, the maximum size allowed, the MIME types allowed for the purpose, and the + * upload link + */ CompletableFuture uploadFile(String entityId, FileUploadRequest fileUploadRequest); + /** + * Retrieves the details of a sub-entity's file (GET /entities/{entityId}/files/{fileId} on the + * Files host). + * + * @param entityId the ID of the sub-entity + * @param fileId the ID of the file + * @return the file's status, size, MIME type, upload date and purpose + */ CompletableFuture retrieveFile(String entityId, String fileId); CompletableFuture createEntity(OnboardEntityRequest entityRequest); @@ -99,10 +124,35 @@ CompletableFuture updateReserveRule(String entityId, CompletableFuture resolveEntityRequirement(String entityId, String requirementId, EntityRequirementUpdateRequest updateRequest); // Synchronous methods + /** + * Uploads a file to the Files API (POST /files on the Files host), as a multipart request. The + * returned ID is what document {@code front} and {@code back} fields take. + * + * @param accountsFileRequest the path to the file, its content type, and its purpose + * @return the ID of the uploaded file + */ IdResponse submitFileSync(final AccountsFileRequest accountsFileRequest); + /** + * Creates a file upload for a sub-entity (POST /entities/{entityId}/files on the Files host). + * The response carries the file ID and an upload link; the file content itself is sent to that + * link, not in this request. + * + * @param entityId the ID of the sub-entity + * @param fileUploadRequest the purpose of the file upload + * @return the file ID, the maximum size allowed, the MIME types allowed for the purpose, and the + * upload link + */ FileUploadResponse uploadFileSync(final String entityId, final FileUploadRequest fileUploadRequest); + /** + * Retrieves the details of a sub-entity's file (GET /entities/{entityId}/files/{fileId} on the + * Files host). + * + * @param entityId the ID of the sub-entity + * @param fileId the ID of the file + * @return the file's status, size, MIME type, upload date and purpose + */ FileDetailsResponse retrieveFileSync(final String entityId, final String fileId); OnboardEntityResponse createEntitySync(final OnboardEntityRequest entityRequest); diff --git a/src/main/java/com/checkout/accounts/AccountsFilePurpose.java b/src/main/java/com/checkout/accounts/AccountsFilePurpose.java index 83a1a71a..434880fe 100644 --- a/src/main/java/com/checkout/accounts/AccountsFilePurpose.java +++ b/src/main/java/com/checkout/accounts/AccountsFilePurpose.java @@ -2,14 +2,31 @@ import lombok.Getter; +/** + * The purpose of a file uploaded with {@link AccountsClient#submitFile(AccountsFileRequest)}. The + * values match the purposes the Accounts API accepts for onboarding documents + * ({@code PlatformsFileUpload}), plus the legacy {@link #IDENTIFICATION}. + */ public enum AccountsFilePurpose { BANK_VERIFICATION("bank_verification"), + /** + * Legacy purpose, not among the onboarding upload purposes; use {@link #IDENTITY_VERIFICATION}. + */ IDENTIFICATION("identification"), IDENTITY_VERIFICATION("identity_verification"), COMPANY_VERIFICATION("company_verification"), FINANCIAL_VERIFICATION("financial_verification"), - TAX_VERIFICATION("tax_verification"); + TAX_VERIFICATION("tax_verification"), + ADDITIONAL_DOCUMENT("additional_document"), + ARTICLES_OF_ASSOCIATION("articles_of_association"), + CERTIFIED_AUTHORISED_SIGNATORY("certified_authorised_signatory"), + COMPANY_OWNERSHIP("company_ownership"), + PROOF_OF_LEGALITY("proof_of_legality"), + PROOF_OF_PRINCIPAL_ADDRESS("proof_of_principal_address"), + SHAREHOLDER_STRUCTURE("shareholder_structure"), + PROOF_OF_RESIDENTIAL_ADDRESS("proof_of_residential_address"), + PROOF_OF_REGISTRATION("proof_of_registration"); @Getter private final String purpose; diff --git a/src/main/java/com/checkout/accounts/AccountsFileRequest.java b/src/main/java/com/checkout/accounts/AccountsFileRequest.java index 01e76dac..c5a9bd2b 100644 --- a/src/main/java/com/checkout/accounts/AccountsFileRequest.java +++ b/src/main/java/com/checkout/accounts/AccountsFileRequest.java @@ -10,14 +10,31 @@ import java.io.File; +/** + * A file to upload with {@link AccountsClient#submitFile(AccountsFileRequest)} (POST /files on the + * Files host), sent as a multipart request. The returned ID is what document {@code front} and + * {@code back} fields take. + */ @Getter @Setter @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public final class AccountsFileRequest extends AbstractFileRequest { + /** + * The purpose of the file upload: the onboarding document the file is for. + * [Required] + */ private AccountsFilePurpose purpose; + /** + * Creates a file upload request. + * + * @param file the file to upload (JPEG, PNG or PDF) + * @param contentType the file's content type; for PDF use + * {@code ContentType.create("application/pdf")} + * @param purpose the purpose of the file upload + */ @Builder private AccountsFileRequest(final File file, final ContentType contentType, diff --git a/src/main/java/com/checkout/accounts/AdditionalDocument.java b/src/main/java/com/checkout/accounts/AdditionalDocument.java index ead77536..029a26b2 100644 --- a/src/main/java/com/checkout/accounts/AdditionalDocument.java +++ b/src/main/java/com/checkout/accounts/AdditionalDocument.java @@ -5,12 +5,22 @@ import lombok.Data; import lombok.NoArgsConstructor; +/** + * Additional space for documents to be provided when requested. Carries a file ID only; the API + * defines no document type for it. + */ @Data @Builder @NoArgsConstructor @AllArgsConstructor public final class AdditionalDocument { + /** + * The ID of the front side of the document as represented within Checkout.com systems. + * [Required] + * ^file_[a-z2-7]{26}$ + * 31 characters + */ private String front; } diff --git a/src/main/java/com/checkout/accounts/ArticlesOfAssociation.java b/src/main/java/com/checkout/accounts/ArticlesOfAssociation.java index 0b136f40..0870034a 100644 --- a/src/main/java/com/checkout/accounts/ArticlesOfAssociation.java +++ b/src/main/java/com/checkout/accounts/ArticlesOfAssociation.java @@ -8,10 +8,8 @@ /** * Memorandum or articles of association document, supplied when onboarding a sub-entity. * - *

Required on the company full onboarding variants. The API expects an object carrying the - * document type and the uploaded file ID, which is why this class exists: the field on - * {@link OnboardSubEntityDocuments} used to be the {@link ArticlesOfAssociationType} enum, so - * the SDK serialized a bare string and the API rejected the request.

+ *

Required on EEA and GB Company Full (3.0); optional on US Company Full (3.0) and the US ISV + * Seller variants. The object carries the document type and the ID of the uploaded file.

*/ @Data @Builder @@ -20,13 +18,16 @@ public final class ArticlesOfAssociation { /** - * The type of document being used as the memorandum or articles of association. + * The type of document used. + * [Required] */ private ArticlesOfAssociationType type; /** - * The ID of the front side of the document as represented within Checkout.com systems, - * as returned when the file was uploaded. + * The ID of the front side of the document as represented within Checkout.com systems. + * [Required] + * ^file_[a-z2-7]{26}$ + * 31 characters */ private String front; diff --git a/src/main/java/com/checkout/accounts/ArticlesOfAssociationType.java b/src/main/java/com/checkout/accounts/ArticlesOfAssociationType.java index 4ed853fa..31ab89ca 100644 --- a/src/main/java/com/checkout/accounts/ArticlesOfAssociationType.java +++ b/src/main/java/com/checkout/accounts/ArticlesOfAssociationType.java @@ -2,6 +2,9 @@ import com.google.gson.annotations.SerializedName; +/** + * The document types accepted as memorandum or articles of association. + */ public enum ArticlesOfAssociationType { @SerializedName("memorandum_of_association") diff --git a/src/main/java/com/checkout/accounts/BankVerification.java b/src/main/java/com/checkout/accounts/BankVerification.java index 0e01c9f7..2201abb9 100644 --- a/src/main/java/com/checkout/accounts/BankVerification.java +++ b/src/main/java/com/checkout/accounts/BankVerification.java @@ -5,14 +5,27 @@ import lombok.Data; import lombok.NoArgsConstructor; +/** + * A document showing transactions from the last 3 months. + */ @Data @Builder @NoArgsConstructor @AllArgsConstructor public final class BankVerification { + /** + * The type of document being used as bank verification. + * [Required] + */ private BankVerificationType type; + /** + * The ID of the front side of the document as represented within Checkout.com systems. + * [Required] + * ^file_[a-z2-7]{26}$ + * 31 characters + */ private String front; } diff --git a/src/main/java/com/checkout/accounts/BankVerificationType.java b/src/main/java/com/checkout/accounts/BankVerificationType.java index 2ab7293b..3bb2d005 100644 --- a/src/main/java/com/checkout/accounts/BankVerificationType.java +++ b/src/main/java/com/checkout/accounts/BankVerificationType.java @@ -2,6 +2,9 @@ import com.google.gson.annotations.SerializedName; +/** + * The document type accepted as bank verification. + */ public enum BankVerificationType { @SerializedName("bank_statement") diff --git a/src/main/java/com/checkout/accounts/CertifiedAuthorisedSignatory.java b/src/main/java/com/checkout/accounts/CertifiedAuthorisedSignatory.java new file mode 100644 index 00000000..938d3230 --- /dev/null +++ b/src/main/java/com/checkout/accounts/CertifiedAuthorisedSignatory.java @@ -0,0 +1,34 @@ +package com.checkout.accounts; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Certified authorised signatory document. Required when the legal representative or other role + * owner is not registered on the certificate of incorporation. Representative documents only + * ({@code company.representatives[].documents}), EEA, GB and US Company Full (3.0) and US ISV + * Seller Company (3.0); not accepted at the top level. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public final class CertifiedAuthorisedSignatory { + + /** + * The type of document. + * [Required] + */ + private CertifiedAuthorisedSignatoryType type; + + /** + * The ID of the front side of the document as represented within Checkout.com systems. + * [Required] + * ^file_[a-z2-7]{26}$ + * 31 characters + */ + private String front; + +} diff --git a/src/main/java/com/checkout/accounts/CertifiedAuthorisedSignatoryType.java b/src/main/java/com/checkout/accounts/CertifiedAuthorisedSignatoryType.java new file mode 100644 index 00000000..9b3e7cbf --- /dev/null +++ b/src/main/java/com/checkout/accounts/CertifiedAuthorisedSignatoryType.java @@ -0,0 +1,13 @@ +package com.checkout.accounts; + +import com.google.gson.annotations.SerializedName; + +/** + * The document type accepted as a representative's certified authorised signatory document. + */ +public enum CertifiedAuthorisedSignatoryType { + + @SerializedName("power_of_attorney") + POWER_OF_ATTORNEY + +} diff --git a/src/main/java/com/checkout/accounts/Company.java b/src/main/java/com/checkout/accounts/Company.java index 38a5a454..00c8de6a 100644 --- a/src/main/java/com/checkout/accounts/Company.java +++ b/src/main/java/com/checkout/accounts/Company.java @@ -8,36 +8,121 @@ import java.util.List; +/** + * Information about the company represented by the sub-entity: on every company and v3.0 sole + * trader variant, and as the controlling company of a {@link Representative} (where only + * {@code legalName}, {@code tradingName} and {@code registeredAddress} apply). + */ @Data @Builder @NoArgsConstructor @AllArgsConstructor public final class Company { + /** + * The legal name of the sub-entity. + * [Required] for every company variant and the controlling company; not part of the sole trader + * variants. + * min 2 characters, max 300 characters + */ private String legalName; + /** + * The trading name of the sub-entity, also referred to as 'doing business as'. + * [Required] + * min 2 characters, max 300 characters + */ private String tradingName; + /** + * The sub-entity's business registration number: a Commercial Registration or Ministry of Commerce + * certificate number, or an equivalent registration number. + * [Required] for the Full variants and US ISV Seller Company (3.0); [Optional] for the Lite (2.0) + * variants. Not part of the sole trader variants. + * The format depends on the variant: + *
    + *
  • EEA: min 2 characters, max 39 characters; a SIRET number for sub-entities based in France.
  • + *
  • GB (3.0): a Companies House number, + * ^(((AC|CE|CS|FC|FE|GE|GS|IC|LP|NC|NF|NI|NL|NO|NP|OC|OE|PC|R0|RC|SA|SC|SE|SF|SG|SI|SL|SO|SR|SZ|ZC|\d{2})\d{6})|((IP|SP|RS)[A-Z\d]{6})|(SL\d{5}[\dA]))$, + * 8 characters. GB (2.0) accepts the same pattern case-insensitively.
  • + *
  • US: an Employer Identification Number (EIN), ^[0-9]{9}$, 9 characters; US ISV Seller Company + * (3.0) also accepts the hyphenated form, ^[0-9]{2}-?[0-9]{7}$, min 9 characters, max 11 + * characters.
  • + *
+ */ private String businessRegistrationNumber; + /** + * The date the company was incorporated, or the date the sole trader started trading. + * [Required] for every v3.0 variant; [Optional] for EEA, GB and US Company Full (2.0). + */ private DateOfIncorporation dateOfIncorporation; + /** + * The regulatory licence number of the company. + * [Optional] (EEA Company Full (3.0) only) + * ^[a-zA-Z0-9\-]+$ + * min 4 characters, max 32 characters + */ private String regulatoryLicenceNumber; + /** + * The primary location where business is performed. + * [Required] for every company and v3.0 sole trader variant. + */ private Address principalAddress; + /** + * The registered address of the company. + * [Required] for every company variant and the controlling company; not part of the sole trader + * variants. + */ private Address registeredAddress; + /** + * Information about the representatives of this company. See {@link Representative}. + * [Required] + * min 1 item; max 1 item for the sole trader variants (the individual themselves, with roles + * {@code [ubo]}), max 5 on v2.0, max 25 on EEA, GB and US Company Full (3.0), no maximum on US ISV + * Seller Company (3.0) + */ private List representatives; + /** + * Not defined by any Accounts API company schema. Retained so existing code keeps compiling. + * + * @deprecated Not part of any Accounts API schema; the API does not read it. Will be removed in a + * future major version. + */ + @Deprecated private EntityDocument document; + /** + * Seller financial questions and supporting documents. + * [Required] for EEA and US Company Full (2.0); [Optional] for EEA and US Company Lite (2.0). Not + * part of the other variants. + */ private EntityFinancialDetails financialDetails; + /** + * The legal type of the company. Must be {@code individual_or_sole_proprietorship} for the sole + * trader variants. + * [Required], except on EEA and US Company Lite (2.0) where it is [Optional]. Not part of GB + * Company Full and Lite (2.0). + */ private BusinessType businessType; + /** + * The collection of additional trading names for the sub-entity. + * [Optional] (US ISV Seller variants only) + */ private List additionalTradingNames; + /** + * Indicates whether the sub-entity is a registered legal entity. Must be {@code false} for US ISV + * Seller Sole Trader (3.0). + * [Required] for US ISV Seller Sole Trader (3.0); not part of the other variants. + */ private Boolean isRegisteredCompany; } diff --git a/src/main/java/com/checkout/accounts/CompanyVerification.java b/src/main/java/com/checkout/accounts/CompanyVerification.java index 2e718eb0..f4e44b30 100644 --- a/src/main/java/com/checkout/accounts/CompanyVerification.java +++ b/src/main/java/com/checkout/accounts/CompanyVerification.java @@ -5,13 +5,29 @@ import lombok.Data; import lombok.NoArgsConstructor; +/** + * The document to use to confirm the company's identity (certified by a power of attorney within + * the last 3 months). + */ @Data @Builder @NoArgsConstructor @AllArgsConstructor public final class CompanyVerification { - private TaxVerificationType type; + /** + * The type of document used for company verification. {@code articles_of_association} is + * accepted on the US Company (2.0) variants only. + * [Required] + */ + private CompanyVerificationType type; + /** + * The ID of the front side of the document as represented within Checkout.com systems. + * [Required] + * ^file_[a-z2-7]{26}$ + * 31 characters + */ private String front; + } diff --git a/src/main/java/com/checkout/accounts/CompanyVerificationType.java b/src/main/java/com/checkout/accounts/CompanyVerificationType.java index 6fa5271b..6471a501 100644 --- a/src/main/java/com/checkout/accounts/CompanyVerificationType.java +++ b/src/main/java/com/checkout/accounts/CompanyVerificationType.java @@ -2,6 +2,11 @@ import com.google.gson.annotations.SerializedName; +/** + * The document types accepted as company verification. {@code articles_of_association} is + * accepted on the US Company (2.0) variants only; articles of association sent as their own + * document use {@link ArticlesOfAssociationType} instead. + */ public enum CompanyVerificationType { @SerializedName("incorporation_document") diff --git a/src/main/java/com/checkout/accounts/ContactDetails.java b/src/main/java/com/checkout/accounts/ContactDetails.java index 3d5f129e..5e8c4dad 100644 --- a/src/main/java/com/checkout/accounts/ContactDetails.java +++ b/src/main/java/com/checkout/accounts/ContactDetails.java @@ -5,16 +5,45 @@ import lombok.Data; import lombok.NoArgsConstructor; +/** + * Contact details of the sub-entity. + */ @Data @Builder @AllArgsConstructor @NoArgsConstructor public final class ContactDetails { + /** + * The phone number of the sub-entity. + * [Required] for every Accounts API v2.0 variant and the US ISV Seller variants; [Optional] for the + * other v3.0 variants; not part of the hosted onboarding invite request. On v3.0 + * {@code countryCode} is required and is the ISO 3166-1 alpha-2 country where the number is + * registered (for example {@code FR}), not the dialling code; v2.0 takes {@code number} only. {@code number} is the number without the country calling code, and + * its format depends on the variant: + *
    + *
  • v3.0 EEA: ^[0-9]{6,13}$, min 6 characters, max 13 characters
  • + *
  • v3.0 GB: ^[0-9]{7,11}$, min 7 characters, max 11 characters
  • + *
  • v3.0 US and US ISV Seller: ^[1-9][0-9]{9,16}$, min 10 characters, max 16 characters
  • + *
  • v2.0: ^[1-9][0-9]{7,15}$, min 8 characters, max 16 characters; on the US v2.0 variants + * ^[2-9]{1}[0-9]{9,15}$, min 10 characters
  • + *
+ */ private AccountPhone phone; + /** + * Email addresses for this sub-entity. + * [Required] for every Accounts API v2.0 variant and the US ISV Seller variants; [Optional] for the + * other v3.0 variants; not part of the hosted onboarding invite request. + */ private EntityEmailAddresses emailAddresses; + /** + * The details of the user responsible for onboarding the sub-entity. + * [Required] in the hosted onboarding invite request, together with reference and is_draft; + * [Optional] in the full onboarding variants (every Full and Lite variant); not part of the US ISV + * Seller variants. + */ private Invitee invitee; } diff --git a/src/main/java/com/checkout/accounts/Document.java b/src/main/java/com/checkout/accounts/Document.java index ac39206b..4aea3d52 100644 --- a/src/main/java/com/checkout/accounts/Document.java +++ b/src/main/java/com/checkout/accounts/Document.java @@ -6,16 +6,36 @@ import lombok.Data; import lombok.NoArgsConstructor; +/** + * The document to use to confirm an individual's identity ({@code identity_verification}): on a + * representative (Accounts API v3.0), or at the top level of the v2.0 sole trader variants. + */ @Data @Builder @AllArgsConstructor @NoArgsConstructor public final class Document { + /** + * The type of document used for identity verification. + * [Required] + */ private DocumentType type; + /** + * The ID of the front side of the document as represented within Checkout.com systems. + * [Required] + * ^file_[a-z2-7]{26}$ + * 31 characters + */ private String front; + /** + * The ID of the back side of the document as represented within Checkout.com systems. + * [Optional] + * ^file_[a-z2-7]{26}$ + * 31 characters + */ private String back; } diff --git a/src/main/java/com/checkout/accounts/EntityDocument.java b/src/main/java/com/checkout/accounts/EntityDocument.java index 388f9499..856777d3 100644 --- a/src/main/java/com/checkout/accounts/EntityDocument.java +++ b/src/main/java/com/checkout/accounts/EntityDocument.java @@ -5,14 +5,28 @@ import lombok.Data; import lombok.NoArgsConstructor; +/** + * Not defined by any Accounts API onboarding schema. Referenced only by {@link Company} {@code document} + * and {@link EntityFinancialDocuments}, both deprecated; retained so existing code keeps compiling. + * + * @deprecated Not part of any Accounts API onboarding schema. Will be removed in a future major + * version. + */ @Data @Builder @AllArgsConstructor @NoArgsConstructor +@Deprecated public final class EntityDocument { + /** + * Not defined by any Accounts API onboarding schema. + */ private String type; + /** + * Not defined by any Accounts API onboarding schema. + */ private String fileId; } diff --git a/src/main/java/com/checkout/accounts/EntityEmailAddresses.java b/src/main/java/com/checkout/accounts/EntityEmailAddresses.java index 0bbbd840..8add5e1e 100644 --- a/src/main/java/com/checkout/accounts/EntityEmailAddresses.java +++ b/src/main/java/com/checkout/accounts/EntityEmailAddresses.java @@ -5,12 +5,27 @@ import lombok.Data; import lombok.NoArgsConstructor; +/** + * Email addresses for this sub-entity. + */ @Data @Builder @AllArgsConstructor @NoArgsConstructor public final class EntityEmailAddresses { + /** + * The main email address for this sub-entity. + * [Required] in every variant that has email addresses. + * Format: email + */ private String primary; + /** + * The email address of the person responsible for PCI compliance at this sub-entity. + * [Required] for the US ISV Seller variants (3.0), together with primary; not part of the other variants. + * Format: email + */ + private String pciComplianceContact; + } diff --git a/src/main/java/com/checkout/accounts/EntityFinancialDetails.java b/src/main/java/com/checkout/accounts/EntityFinancialDetails.java index d6482ad0..7793add3 100644 --- a/src/main/java/com/checkout/accounts/EntityFinancialDetails.java +++ b/src/main/java/com/checkout/accounts/EntityFinancialDetails.java @@ -4,18 +4,48 @@ import lombok.Builder; import lombok.Data; +/** + * Seller financial questions ({@code financial_details}): on the company of EEA and US Company Full + * and Lite (2.0), and on the individual of US Sole Trader Full and Lite (2.0). + */ @Data @Builder public final class EntityFinancialDetails { + /** + * The estimated annual processing volume. In minor units without decimals. + * [Required] on the Full (2.0) variants; [Optional] on the Lite (2.0) variants. + * min 0 + */ private Long annualProcessingVolume; + /** + * The expected average transaction value. In minor units without decimals. + * [Required] on the Full (2.0) variants; [Optional] on the Lite (2.0) variants. + * min 0 + */ private Long averageTransactionValue; + /** + * The expected highest transaction value. In minor units without decimals. + * [Required] on the Full (2.0) variants; [Optional] on the Lite (2.0) variants. + * min 0 + */ private Long highestTransactionValue; + /** + * Not defined by any Accounts API schema; the API does not read it. Supporting documents go on + * the top-level request documents ({@link OnboardSubEntityDocuments}) instead. + * + * @deprecated Not part of any Accounts API schema. Will be removed in a future major version. + */ + @Deprecated private EntityFinancialDocuments documents; + /** + * The currency used for the financial details provided. + * [Required] on US Company Full and US Sole Trader Full (2.0); [Optional] on the other variants. + */ private Currency currency; } diff --git a/src/main/java/com/checkout/accounts/EntityFinancialDocuments.java b/src/main/java/com/checkout/accounts/EntityFinancialDocuments.java index 3890abeb..55f05d60 100644 --- a/src/main/java/com/checkout/accounts/EntityFinancialDocuments.java +++ b/src/main/java/com/checkout/accounts/EntityFinancialDocuments.java @@ -3,11 +3,24 @@ import lombok.Builder; import lombok.Data; +/** + * Not defined by any Accounts API schema: {@code financial_details} carries the three amounts and + * the currency only. Retained so existing code keeps compiling. + * + * @deprecated Not part of any Accounts API schema. Will be removed in a future major version. + */ @Data @Builder +@Deprecated public final class EntityFinancialDocuments { + /** + * Not defined by any Accounts API schema. + */ private EntityDocument bankStatement; + /** + * Not defined by any Accounts API schema. + */ private EntityDocument financialStatement; } diff --git a/src/main/java/com/checkout/accounts/EntityProcessingDetails.java b/src/main/java/com/checkout/accounts/EntityProcessingDetails.java new file mode 100644 index 00000000..e4786cf2 --- /dev/null +++ b/src/main/java/com/checkout/accounts/EntityProcessingDetails.java @@ -0,0 +1,62 @@ +package com.checkout.accounts; + +import com.checkout.common.Currency; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * The sub-entity's expected processing, as returned by GET /accounts/entities/{id} + * ({@code processing_details}, Accounts API v3.0). + * + *

A response-only type, separate from the request's {@link ProcessingDetails}: the amounts are + * {@code Long} here because the API declares them as integers in minor units with no maximum, and + * an {@code Integer} would fail to read any value above 2,147,483,647.

+ */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public final class EntityProcessingDetails { + + /** + * The country code (iso-3166-1 alpha-2) where the settlement bank account is located. + * Format: iso-3166-1-alpha-2 + * 2 characters + */ + private String settlementCountry; + + /** + * Target country codes (iso-3166-1 alpha-2) with more than 10% expected volume processing with + * Checkout.com. + * min 1 item, max 10 items + */ + private List targetCountries; + + /** + * The estimated annual processing volume. In minor units without decimals. + * min 0 + */ + private Long annualProcessingVolume; + + /** + * The expected average transaction value. In minor units without decimals. + * min 0 + */ + private Long averageTransactionValue; + + /** + * The expected highest transaction value. In minor units without decimals. + * min 0 + */ + private Long highestTransactionValue; + + /** + * The currency used for the processing details provided. + */ + private Currency currency; + +} diff --git a/src/main/java/com/checkout/accounts/FinancialStatements.java b/src/main/java/com/checkout/accounts/FinancialStatements.java index a93a815a..ae60b0f4 100644 --- a/src/main/java/com/checkout/accounts/FinancialStatements.java +++ b/src/main/java/com/checkout/accounts/FinancialStatements.java @@ -5,14 +5,29 @@ import lombok.Data; import lombok.NoArgsConstructor; +/** + * Audited or management-prepared financial statements (when applicable). US ISV Seller variants + * only. Not the same document as {@link FinancialVerification}, whose type is the singular + * {@code financial_statement}. + */ @Data @Builder @NoArgsConstructor @AllArgsConstructor public final class FinancialStatements { + /** + * The type of document. + * [Required] + */ private FinancialStatementsType type; + /** + * The ID of the front side of the document as represented within Checkout.com systems. + * [Required] + * ^file_[a-z2-7]{26}$ + * 31 characters + */ private String front; } diff --git a/src/main/java/com/checkout/accounts/FinancialStatementsType.java b/src/main/java/com/checkout/accounts/FinancialStatementsType.java index b373a10d..dc147912 100644 --- a/src/main/java/com/checkout/accounts/FinancialStatementsType.java +++ b/src/main/java/com/checkout/accounts/FinancialStatementsType.java @@ -2,6 +2,10 @@ import com.google.gson.annotations.SerializedName; +/** + * The document type accepted as financial statements (US ISV Seller variants). Note the plural + * {@code financial_statements}; {@link FinancialVerificationType} is a different enum. + */ public enum FinancialStatementsType { @SerializedName("financial_statements") diff --git a/src/main/java/com/checkout/accounts/FinancialVerification.java b/src/main/java/com/checkout/accounts/FinancialVerification.java index f7abbf14..efdb4b75 100644 --- a/src/main/java/com/checkout/accounts/FinancialVerification.java +++ b/src/main/java/com/checkout/accounts/FinancialVerification.java @@ -5,14 +5,29 @@ import lombok.Data; import lombok.NoArgsConstructor; +/** + * Financial statement document. Becomes mandatory depending on the answer provided for + * {@code annual_processing_volume}; the sub-entity's status changes to {@code requirements_due} + * when it is needed. + */ @Data @Builder @NoArgsConstructor @AllArgsConstructor public final class FinancialVerification { + /** + * The type of the file. + * [Required] + */ private FinancialVerificationType type; + /** + * The ID of the front side of the document as represented within Checkout.com systems. + * [Required] + * ^file_[a-z2-7]{26}$ + * 31 characters + */ private String front; } diff --git a/src/main/java/com/checkout/accounts/FinancialVerificationType.java b/src/main/java/com/checkout/accounts/FinancialVerificationType.java index d1b14f21..56dcb142 100644 --- a/src/main/java/com/checkout/accounts/FinancialVerificationType.java +++ b/src/main/java/com/checkout/accounts/FinancialVerificationType.java @@ -2,6 +2,10 @@ import com.google.gson.annotations.SerializedName; +/** + * The document type accepted as financial verification. Note the singular + * {@code financial_statement}; {@link FinancialStatementsType} is a different enum. + */ public enum FinancialVerificationType { @SerializedName("financial_statement") diff --git a/src/main/java/com/checkout/accounts/Identification.java b/src/main/java/com/checkout/accounts/Identification.java index 5331ba44..1e726d11 100644 --- a/src/main/java/com/checkout/accounts/Identification.java +++ b/src/main/java/com/checkout/accounts/Identification.java @@ -5,14 +5,32 @@ import lombok.Data; import lombok.NoArgsConstructor; +/** + * The identification of a representative or individual on the Accounts API v2.0 US variants. + */ @Data @Builder @NoArgsConstructor @AllArgsConstructor public final class Identification { + /** + * Social Security Number (SSN), or Individual Taxpayer Identification Number (ITIN) for non-US + * citizens. + * [Required] + * ^\d{9}$ + * 9 characters + */ private String nationalIdNumber; + /** + * Not defined by the Accounts API: the identification object carries {@code national_id_number} + * only. Retained so existing code keeps compiling. + * + * @deprecated Not part of any Accounts API schema; the API does not read it. Will be removed in a + * future major version. + */ + @Deprecated private Document document; } diff --git a/src/main/java/com/checkout/accounts/Individual.java b/src/main/java/com/checkout/accounts/Individual.java index 8f7da8a9..bba1878b 100644 --- a/src/main/java/com/checkout/accounts/Individual.java +++ b/src/main/java/com/checkout/accounts/Individual.java @@ -4,28 +4,78 @@ import lombok.Builder; import lombok.Data; +/** + * The top-level {@code individual} of the Accounts API v2.0 sole trader variants. + */ @Data @Builder public final class Individual { + /** + * The individual's first name. + * [Required] + * min 2 characters, max 50 characters + */ private String firstName; + /** + * The individual's middle name. Required if it appears in official documents. + * [Optional] + * min 2 characters, max 50 characters + */ private String middleName; + /** + * The individual's last name. + * [Required] + * min 2 characters, max 50 characters + */ private String lastName; + /** + * The trading name of the sub-entity, also referred to as 'doing business as'. + * [Required] + * min 2 characters, max 300 characters + */ private String tradingName; + /** + * Not defined by any Accounts API schema. Retained so existing code keeps compiling. + * + * @deprecated Not defined by any Accounts API schema; the API does not read it. Will be removed in + * a future major version. + */ + @Deprecated private String nationalTaxId; + /** + * The registered address of the sole trader's business. + * [Required] + */ private Address registeredAddress; + /** + * The date of birth of the person according to the Gregorian calendar. + * [Required], except on GB Sole Trader Lite (2.0) where it is [Optional]. + */ private DateOfBirth dateOfBirth; + /** + * The place of birth of the person. + * [Required] for EEA Sole Trader Full and Lite (2.0); not part of the other v2.0 variants. + */ private PlaceOfBirth placeOfBirth; + /** + * The individual's identification. US Sole Trader (2.0) only. + * [Required] for US Sole Trader Full (2.0); [Optional] for US Sole Trader Lite (2.0). + */ private Identification identification; - + + /** + * Seller financial questions and supporting documents. US Sole Trader (2.0) only. + * [Required] for US Sole Trader Full (2.0); [Optional] for US Sole Trader Lite (2.0). + */ private EntityFinancialDetails financialDetails; } diff --git a/src/main/java/com/checkout/accounts/Invitee.java b/src/main/java/com/checkout/accounts/Invitee.java index c39ae45d..a64caa93 100644 --- a/src/main/java/com/checkout/accounts/Invitee.java +++ b/src/main/java/com/checkout/accounts/Invitee.java @@ -5,11 +5,21 @@ import lombok.Data; import lombok.NoArgsConstructor; +/** + * The details of the user responsible for onboarding the sub-entity. + */ @Data @Builder @AllArgsConstructor @NoArgsConstructor public final class Invitee { + /** + * The email of the user responsible for onboarding the sub-entity. + * [Required] in the hosted onboarding invite request (with reference and is_draft); [Optional] in + * the full onboarding variants (every Full and Lite variant); not part of the US ISV Seller variants. + * Format: email + */ private String email; + } diff --git a/src/main/java/com/checkout/accounts/OnboardEntityDetailsResponse.java b/src/main/java/com/checkout/accounts/OnboardEntityDetailsResponse.java index bf521a7d..c2d65d1b 100644 --- a/src/main/java/com/checkout/accounts/OnboardEntityDetailsResponse.java +++ b/src/main/java/com/checkout/accounts/OnboardEntityDetailsResponse.java @@ -7,29 +7,76 @@ import java.util.List; +/** + * The details of a sub-entity, as returned by GET /accounts/entities/{id}. + */ @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public final class OnboardEntityDetailsResponse extends Resource { + /** + * The ID of the sub-entity. + */ private String id; + /** + * A unique reference you can later use to identify the sub-entity. + */ private String reference; + /** + * The onboarding status of the sub-entity. + */ private OnboardingStatus status; + /** + * The capabilities of the entity. + */ private Capabilities capabilities; + /** + * List of requirements due in order to be onboarded. + */ private List requirementsDue; + /** + * Contact details of this sub-entity. + */ private ContactDetails contactDetails; + /** + * Information about the profile of the sub-entity, primarily regarding the products and services + * offered. + */ private Profile profile; + /** + * Information about the company represented by the sub-entity (company and v3.0 sole trader + * variants). + */ private Company company; + /** + * Information about the individual represented by the sub-entity (v2.0 sole trader variants). + */ private Individual individual; + /** + * The sub-entity's payment instruments. + */ private List instruments; + /** + * The sub-entity's expected processing (Accounts API v3.0). Amounts are {@code Long}; see + * {@link EntityProcessingDetails}. + */ + private EntityProcessingDetails processingDetails; + + /** + * The top-level documents used to support the verification of the sub-entity's details. + * Representative documents are on {@link Representative}, under {@code company}. + */ + private OnboardSubEntityDocuments documents; + } diff --git a/src/main/java/com/checkout/accounts/OnboardSubEntityDocuments.java b/src/main/java/com/checkout/accounts/OnboardSubEntityDocuments.java index 1784e352..508a43a4 100644 --- a/src/main/java/com/checkout/accounts/OnboardSubEntityDocuments.java +++ b/src/main/java/com/checkout/accounts/OnboardSubEntityDocuments.java @@ -6,39 +6,151 @@ import lombok.Data; import lombok.NoArgsConstructor; +/** + * Verification documents for a sub-entity. This one type serves two different objects on the + * Accounts API, which accept different keys: + *
    + *
  • The top-level request {@code documents}, on {@link OnboardEntityRequest}. The API + * ignores keys it does not recognise here rather than rejecting them, so a misplaced document is + * dropped silently.
  • + *
  • A representative's {@code documents}, on {@link Representative}. It takes only + * {@code identity_verification}, {@code certified_authorised_signatory} (EEA, GB and US Company + * Full (3.0) and US ISV Seller Company (3.0)), {@code proof_of_residential_address} and + * {@code proof_of_registration}. On the EEA, GB and US Company Full (3.0) and Sole Trader Full + * (3.0) variants this object is strict and rejects any other key; it is not strict on the US ISV + * Seller variants (3.0) nor on any v2.0 variant.
  • + *
+ * Each field below says which of the two it belongs to. + */ @Data @Builder @NoArgsConstructor @AllArgsConstructor public final class OnboardSubEntityDocuments { + // Both + + /** + * The document to use to confirm the individual's identity. Valid in both objects: + *
    + *
  • Representative: [Required] for the EEA, GB and US Sole Trader Full (3.0) variants; + * [Optional] for the company variants.
  • + *
  • Top level: [Required] for the six sole trader variants of Accounts API v2.0, the only + * variants that take it there.
  • + *
+ */ private Document identityVerification; + // Top level + + /** + * The document to use to confirm the company's identity (certified by a power of attorney + * within the last 3 months). Top level only. + * [Required] for EEA Company Full (2.0 and 3.0) and GB Company Full (2.0); [Optional] for the + * other company variants and the US ISV Seller variants. + */ private CompanyVerification companyVerification; + /** + * Memorandum or Articles of Association document. Top level only. + * [Required] for EEA and GB Company Full (3.0); [Optional] for US Company Full (3.0) and the US + * ISV Seller variants. + */ private ArticlesOfAssociation articlesOfAssociation; + /** + * A document showing transactions from the last 3 months. Top level only. + * [Required] for EEA Company Full (3.0) and the EEA, GB and US Sole Trader Full (3.0) variants; + * [Optional] for GB and US Company Full (3.0) and EEA Company Full and Lite (2.0). + */ private BankVerification bankVerification; + /** + * Shareholder structure chart (including % of shares) certified by a competent authority + * individual and dated within the last 3 months. Top level only. + * [Required] for EEA and GB Company Full (3.0); [Optional] for US Company Full (3.0) and US ISV + * Seller Company (3.0). + */ private ShareholderStructure shareholderStructure; + /** + * A regulatory licence document required for the company to operate (when applicable). Top + * level only. + * [Optional] (EEA, GB and US Company Full (3.0) and the US ISV Seller variants) + */ private ProofOfLegality proofOfLegality; + /** + * Proof of the company's principal place of business. Top level only. + * [Optional] (EEA, GB and US Company Full (3.0) and the US ISV Seller variants) + */ private ProofOfPrincipalAddress proofOfPrincipalAddress; + /** + * Additional space for documents to be provided when requested. Top level only. + * [Optional] (EEA, GB and US Company and Sole Trader Full (3.0); not the US ISV Seller variants) + */ @SerializedName("additional_document1") private AdditionalDocument additionalDocument1; + /** + * Additional space for documents to be provided when requested. Top level only. + * [Optional] (EEA, GB and US Company and Sole Trader Full (3.0); not the US ISV Seller variants) + */ @SerializedName("additional_document2") private AdditionalDocument additionalDocument2; + /** + * Additional space for documents to be provided when requested. Top level only. + * [Optional] (EEA, GB and US Company and Sole Trader Full (3.0); not the US ISV Seller variants) + */ @SerializedName("additional_document3") private AdditionalDocument additionalDocument3; + /** + * IRS-issued Employer Identification Number document used to verify the entity's tax + * identification. Top level only. + * [Optional] (US Company variants and the US ISV Seller variants only) + */ private TaxVerification taxVerification; + /** + * Financial statement document. Becomes mandatory depending on the answer provided for + * {@code annual_processing_volume}; the sub-entity's status changes to {@code requirements_due} + * when it is needed. Top level only. + * [Optional] (EEA Company Full and Lite (2.0) only) + */ private FinancialVerification financialVerification; + /** + * Audited or management-prepared financial statements (when applicable). Top level only. + * [Optional] (US ISV Seller variants only) + */ private FinancialStatements financialStatements; + // Representative only + + /** + * Certified authorised signatory document. Required when the legal representative or other + * role owner is not registered on the certificate of incorporation. Representative only + * ({@code company.representatives[].documents}); not accepted at the top level. + * [Optional] (EEA, GB and US Company Full (3.0) and US ISV Seller Company (3.0)) + */ + private CertifiedAuthorisedSignatory certifiedAuthorisedSignatory; + + /** + * Proof of residential address of the representative. Representative only + * ({@code company.representatives[].documents}); not accepted at the top level. + * [Required] for EEA Sole Trader Full (3.0), and only valid there. + */ + private ProofOfResidentialAddress proofOfResidentialAddress; + + /** + * Proof of the sole trader's registration, for example an extract from a trade register. + * Representative only ({@code company.representatives[].documents}); not accepted at the top + * level. + * [Required] for EEA Sole Trader Full (3.0), and only valid there. + */ + private ProofOfRegistration proofOfRegistration; + } diff --git a/src/main/java/com/checkout/accounts/ProofOfLegality.java b/src/main/java/com/checkout/accounts/ProofOfLegality.java index 35263323..3ea7c861 100644 --- a/src/main/java/com/checkout/accounts/ProofOfLegality.java +++ b/src/main/java/com/checkout/accounts/ProofOfLegality.java @@ -5,14 +5,27 @@ import lombok.Data; import lombok.NoArgsConstructor; +/** + * A regulatory licence document required for the company to operate (when applicable). + */ @Data @Builder @NoArgsConstructor @AllArgsConstructor public final class ProofOfLegality { + /** + * The type of document used for proof of legality. + * [Required] + */ private ProofOfLegalityType type; + /** + * The ID of the front side of the document as represented within Checkout.com systems. + * [Required] + * ^file_[a-z2-7]{26}$ + * 31 characters + */ private String front; } diff --git a/src/main/java/com/checkout/accounts/ProofOfLegalityType.java b/src/main/java/com/checkout/accounts/ProofOfLegalityType.java index a1130bb3..3ba4f497 100644 --- a/src/main/java/com/checkout/accounts/ProofOfLegalityType.java +++ b/src/main/java/com/checkout/accounts/ProofOfLegalityType.java @@ -2,6 +2,9 @@ import com.google.gson.annotations.SerializedName; +/** + * The document type accepted as proof of legality. + */ public enum ProofOfLegalityType { @SerializedName("proof_of_legality") diff --git a/src/main/java/com/checkout/accounts/ProofOfPrincipalAddress.java b/src/main/java/com/checkout/accounts/ProofOfPrincipalAddress.java index b7488f84..3aa25a95 100644 --- a/src/main/java/com/checkout/accounts/ProofOfPrincipalAddress.java +++ b/src/main/java/com/checkout/accounts/ProofOfPrincipalAddress.java @@ -5,14 +5,27 @@ import lombok.Data; import lombok.NoArgsConstructor; +/** + * Proof of the company's principal place of business. + */ @Data @Builder @NoArgsConstructor @AllArgsConstructor public final class ProofOfPrincipalAddress { + /** + * The type of document being used as address verification. + * [Required] + */ private ProofOfPrincipalAddressType type; + /** + * The ID of the front side of the document as represented within Checkout.com systems. + * [Required] + * ^file_[a-z2-7]{26}$ + * 31 characters + */ private String front; } diff --git a/src/main/java/com/checkout/accounts/ProofOfPrincipalAddressType.java b/src/main/java/com/checkout/accounts/ProofOfPrincipalAddressType.java index e04df292..d247d496 100644 --- a/src/main/java/com/checkout/accounts/ProofOfPrincipalAddressType.java +++ b/src/main/java/com/checkout/accounts/ProofOfPrincipalAddressType.java @@ -2,6 +2,11 @@ import com.google.gson.annotations.SerializedName; +/** + * The document type accepted as proof of the company's principal place of business. Carries the + * same {@code proof_of_address} value as {@link ProofOfResidentialAddressType}, but the API defines + * the two as separate enums on separate documents. + */ public enum ProofOfPrincipalAddressType { @SerializedName("proof_of_address") diff --git a/src/main/java/com/checkout/accounts/ProofOfRegistration.java b/src/main/java/com/checkout/accounts/ProofOfRegistration.java new file mode 100644 index 00000000..7bbbe9fa --- /dev/null +++ b/src/main/java/com/checkout/accounts/ProofOfRegistration.java @@ -0,0 +1,33 @@ +package com.checkout.accounts; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Proof of the sole trader's registration, for example an extract from a trade register. + * Representative documents only ({@code company.representatives[].documents}), EEA Sole Trader + * Full (3.0); not accepted at the top level. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public final class ProofOfRegistration { + + /** + * The type of document being used as proof of registration. + * [Required] + */ + private ProofOfRegistrationType type; + + /** + * The ID of the front side of the document as represented within Checkout.com systems. + * [Required] + * ^file_[a-z2-7]{26}$ + * 31 characters + */ + private String front; + +} diff --git a/src/main/java/com/checkout/accounts/ProofOfRegistrationType.java b/src/main/java/com/checkout/accounts/ProofOfRegistrationType.java new file mode 100644 index 00000000..cb52e352 --- /dev/null +++ b/src/main/java/com/checkout/accounts/ProofOfRegistrationType.java @@ -0,0 +1,17 @@ +package com.checkout.accounts; + +import com.google.gson.annotations.SerializedName; + +/** + * The document types accepted as a sole trader's proof of registration (EEA Sole Trader Full + * (3.0)). + */ +public enum ProofOfRegistrationType { + + @SerializedName("extract_from_trade_register") + EXTRACT_FROM_TRADE_REGISTER, + + @SerializedName("other") + OTHER + +} diff --git a/src/main/java/com/checkout/accounts/ProofOfResidentialAddress.java b/src/main/java/com/checkout/accounts/ProofOfResidentialAddress.java new file mode 100644 index 00000000..a86e9fc5 --- /dev/null +++ b/src/main/java/com/checkout/accounts/ProofOfResidentialAddress.java @@ -0,0 +1,33 @@ +package com.checkout.accounts; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Proof of residential address of the representative. Representative documents only + * ({@code company.representatives[].documents}), EEA Sole Trader Full (3.0); not accepted at the + * top level. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public final class ProofOfResidentialAddress { + + /** + * The type of document being used as address verification. + * [Required] + */ + private ProofOfResidentialAddressType type; + + /** + * The ID of the front side of the document as represented within Checkout.com systems. + * [Required] + * ^file_[a-z2-7]{26}$ + * 31 characters + */ + private String front; + +} diff --git a/src/main/java/com/checkout/accounts/ProofOfResidentialAddressType.java b/src/main/java/com/checkout/accounts/ProofOfResidentialAddressType.java new file mode 100644 index 00000000..800d4258 --- /dev/null +++ b/src/main/java/com/checkout/accounts/ProofOfResidentialAddressType.java @@ -0,0 +1,15 @@ +package com.checkout.accounts; + +import com.google.gson.annotations.SerializedName; + +/** + * The document type accepted as a representative's proof of residential address (EEA Sole Trader + * Full (3.0)). Carries the same {@code proof_of_address} value as {@link ProofOfPrincipalAddressType}, + * but the API defines the two as separate enums on separate documents. + */ +public enum ProofOfResidentialAddressType { + + @SerializedName("proof_of_address") + PROOF_OF_ADDRESS + +} diff --git a/src/main/java/com/checkout/accounts/Representative.java b/src/main/java/com/checkout/accounts/Representative.java index 9443d6f9..1de7078f 100644 --- a/src/main/java/com/checkout/accounts/Representative.java +++ b/src/main/java/com/checkout/accounts/Representative.java @@ -6,68 +6,151 @@ import java.util.List; +/** + * A representative of the sub-entity. One class covers every shape the Accounts API defines: + *
    + *
  • v3.0 person of interest: {@code individual}, {@code roles}, {@code companyPosition}, + * {@code ownershipPercentage}, {@code documents}.
  • + *
  • v3.0 controlling company (EEA and GB Company Full): {@code company} and + * {@code ownershipPercentage}.
  • + *
  • v2.0 company representatives: the deprecated flat person fields, {@code roles}, + * {@code documents} and, on the US variants, {@code identification}.
  • + *
+ */ @Data @Builder public final class Representative { /** + * The representative's first name. Accounts API v2.0 only. + * [Required] (v2.0) + * min 2 characters, max 50 characters + * * @deprecated Not used by the Accounts API v3.0 schema; use {@link #individual} instead. */ @Deprecated private String firstName; /** + * The representative's middle name. Required if it appears in official documents. Accounts API + * v2.0 only. + * [Optional] + * min 2 characters, max 50 characters + * * @deprecated Not used by the Accounts API v3.0 schema; use {@link #individual} instead. */ @Deprecated private String middleName; /** + * The representative's last name. Accounts API v2.0 only. + * [Required] (v2.0) + * min 2 characters, max 50 characters + * * @deprecated Not used by the Accounts API v3.0 schema; use {@link #individual} instead. */ @Deprecated private String lastName; /** + * The representative's address. Accounts API v2.0 only. + * [Required] (v2.0) + * * @deprecated Not used by the Accounts API v3.0 schema; use {@link #individual} instead. */ @Deprecated private Address address; /** + * The representative's identification. Accounts API v2.0 US Company variants only. + * [Required] for US Company Full (2.0); [Optional] for US Company Lite (2.0). + * * @deprecated Not used by the Accounts API v3.0 schema; use {@link #individual} instead. */ @Deprecated private Identification identification; /** + * The representative's phone number. Accounts API v2.0 only. + * [Optional] + * * @deprecated Not used by the Accounts API v3.0 schema; use {@link #individual} instead. */ @Deprecated private AccountPhone phone; /** + * The date of birth of the person according to the Gregorian calendar. Accounts API v2.0 only. + * [Required] for the v2.0 Full variants; [Optional] for the v2.0 Lite variants. + * * @deprecated Not used by the Accounts API v3.0 schema; use {@link #individual} instead. */ @Deprecated private DateOfBirth dateOfBirth; /** + * The place of birth of the person. Accounts API v2.0 only. + * [Required] for EEA Company Full (2.0); [Optional] for EEA Company Lite (2.0). Not part of the + * other v2.0 variants. + * * @deprecated Not used by the Accounts API v3.0 schema; use {@link #individual} instead. */ @Deprecated private PlaceOfBirth placeOfBirth; + /** + * The individual's roles within the company. For sole traders, must be {@code ubo} only. + * [Required] for every variant except EEA and US Company Lite (2.0), where it is [Optional]. + */ private List roles; + /** + * Verification documents for the individual representative. On the EEA, GB and US Company Full + * (3.0) and Sole Trader Full (3.0) variants this object is strict: it accepts only + * {@code identity_verification}, {@code certified_authorised_signatory}, + * {@code proof_of_residential_address} and {@code proof_of_registration}, and rejects any other + * key. It is not strict on the US ISV Seller variants (3.0) nor on any v2.0 variant. See + * {@link OnboardSubEntityDocuments} for which apply to each variant. + * [Required] for the EEA, GB and US Sole Trader Full (3.0) variants and EEA Company Full (2.0); + * [Optional] otherwise. + */ private OnboardSubEntityDocuments documents; + /** + * Information about the individual representing the sub-entity. + * [Required] for every v3.0 person of interest. + */ private RepresentativeIndividual individual; + /** + * The representative's id. + * [Optional] + * ^rep_[a-z0-9]{26}$ + * 30 characters + */ private String id; + /** + * The position of the representative within the company (required for the + * {@code control_person} role). + * [Optional] (EEA, GB and US Company Full (3.0) and US ISV Seller Company (3.0)) + */ private CompanyPosition companyPosition; + /** + * The percentage ownership of the UBO or controlling company (required when over 25%). + * [Optional] + * min 25, max 100 on the EEA, GB and US Company Full (3.0) variants; min 0, max 100 on the US ISV + * Seller variants + */ private Integer ownershipPercentage; + /** + * The controlling company, when the representative is a company rather than an individual. + * [Required] for a controlling company representative (EEA and GB Company Full (3.0) only). + * The API reads only three fields here, all [Required]: {@code legalName}, {@code tradingName} + * and {@code registeredAddress}. Leave the other {@link Company} fields unset. + */ + private Company company; + } diff --git a/src/main/java/com/checkout/accounts/RepresentativeIndividual.java b/src/main/java/com/checkout/accounts/RepresentativeIndividual.java index e35dc5e7..1cc74b74 100644 --- a/src/main/java/com/checkout/accounts/RepresentativeIndividual.java +++ b/src/main/java/com/checkout/accounts/RepresentativeIndividual.java @@ -8,32 +8,93 @@ import java.util.List; +/** + * The personal details of a company representative ({@code company.representatives[].individual}), + * Accounts API v3.0. + */ @Data @Builder @NoArgsConstructor @AllArgsConstructor public final class RepresentativeIndividual { + /** + * The representative's first name. + * [Required] + * min 2 characters, max 50 characters + */ private String firstName; + /** + * The representative's middle name. Required if it appears in official documents. + * [Optional] + * min 2 characters, max 50 characters + */ private String middleName; + /** + * The representative's last name. + * [Required] + * min 2 characters, max 50 characters + */ private String lastName; + /** + * The date of birth of the person according to the Gregorian calendar. + * [Required] + */ private DateOfBirth dateOfBirth; + /** + * The place of birth of the person. + * [Required] + */ private PlaceOfBirth placeOfBirth; + /** + * The list of citizenships or legal statuses for the representative. + * [Required] for the US ISV Seller variants only; not part of the other v3.0 schemas, leave unset + * for them. + */ private List citizenships; + /** + * The classification of the national identification number provided. + * [Required] for the US ISV Seller variants only; not part of the other v3.0 schemas, leave unset + * for them. + */ private NationalIdType nationalIdType; + /** + * The representative's national identification number. v3.0 only. + * [Required] for the US ISV Seller variants; [Optional] for the other v3.0 variants. + * The format depends on the variant: + *
    + *
  • US ISV Seller: the number for the {@code nationalIdType} given. ^[a-zA-Z0-9\-]+$, min 5 + * characters, max 16 characters.
  • + *
  • Other v3.0 variants: a Social Security Number (SSN) or Individual Taxpayer Identification + * Number (ITIN), US residents only. ^\d{9}$, 9 characters.
  • + *
+ */ private String nationalIdNumber; + /** + * The representative's personal email address. + * [Required] for the US ISV Seller variants; [Optional] for the other v3.0 variants. + * Format: email + */ private String emailAddress; + /** + * The representative's phone number. + * [Required] for the US ISV Seller variants; [Optional] for the other v3.0 variants. + */ private AccountPhone phone; + /** + * The representative's address. + * [Required] + */ private Address address; } diff --git a/src/main/java/com/checkout/accounts/ShareholderStructure.java b/src/main/java/com/checkout/accounts/ShareholderStructure.java index 9d9de8ab..cd504d4d 100644 --- a/src/main/java/com/checkout/accounts/ShareholderStructure.java +++ b/src/main/java/com/checkout/accounts/ShareholderStructure.java @@ -5,14 +5,28 @@ import lombok.Data; import lombok.NoArgsConstructor; +/** + * Shareholder structure chart (including % of shares) certified by a competent authority + * individual and dated within the last 3 months. + */ @Data @Builder @NoArgsConstructor @AllArgsConstructor public final class ShareholderStructure { + /** + * The type of document. + * [Required] + */ private ShareholderStructureType type; + /** + * The ID of the front side of the document as represented within Checkout.com systems. + * [Required] + * ^file_[a-z2-7]{26}$ + * 31 characters + */ private String front; } diff --git a/src/main/java/com/checkout/accounts/ShareholderStructureType.java b/src/main/java/com/checkout/accounts/ShareholderStructureType.java index c7a3ce7b..7e5cab0e 100644 --- a/src/main/java/com/checkout/accounts/ShareholderStructureType.java +++ b/src/main/java/com/checkout/accounts/ShareholderStructureType.java @@ -2,6 +2,9 @@ import com.google.gson.annotations.SerializedName; +/** + * The document type accepted as a certified shareholder structure. + */ public enum ShareholderStructureType { @SerializedName("certified_shareholder_structure") diff --git a/src/main/java/com/checkout/accounts/TaxVerification.java b/src/main/java/com/checkout/accounts/TaxVerification.java index 8b856e01..256bff68 100644 --- a/src/main/java/com/checkout/accounts/TaxVerification.java +++ b/src/main/java/com/checkout/accounts/TaxVerification.java @@ -5,13 +5,28 @@ import lombok.Data; import lombok.NoArgsConstructor; +/** + * IRS-issued Employer Identification Number document used to verify the entity's tax + * identification (US variants). + */ @Data @Builder @NoArgsConstructor @AllArgsConstructor public final class TaxVerification { - private CompanyVerificationType type; + /** + * The type of IRS-issued document used for tax verification. + * [Required] + */ + private TaxVerificationType type; + /** + * The ID of the front side of the document as represented within Checkout.com systems. + * [Required] + * ^file_[a-z2-7]{26}$ + * 31 characters + */ private String front; + } diff --git a/src/main/java/com/checkout/accounts/TaxVerificationType.java b/src/main/java/com/checkout/accounts/TaxVerificationType.java index 096ea421..e1545e4c 100644 --- a/src/main/java/com/checkout/accounts/TaxVerificationType.java +++ b/src/main/java/com/checkout/accounts/TaxVerificationType.java @@ -2,6 +2,10 @@ import com.google.gson.annotations.SerializedName; +/** + * The document type accepted as tax verification: an IRS-issued Employer Identification Number + * letter. + */ public enum TaxVerificationType { @SerializedName("ein_letter") diff --git a/src/main/java/com/checkout/accounts/files/entities/FilePurpose.java b/src/main/java/com/checkout/accounts/files/entities/FilePurpose.java index cb030fd0..22024de3 100644 --- a/src/main/java/com/checkout/accounts/files/entities/FilePurpose.java +++ b/src/main/java/com/checkout/accounts/files/entities/FilePurpose.java @@ -2,6 +2,9 @@ import com.google.gson.annotations.SerializedName; +/** + * The purpose of a sub-entity file upload (POST /entities/{entityId}/files). + */ public enum FilePurpose { @SerializedName("additional_document") ADDITIONAL_DOCUMENT, @@ -31,6 +34,10 @@ public enum FilePurpose { PROOF_OF_RESIDENTIAL_ADDRESS, @SerializedName("proof_of_registration") PROOF_OF_REGISTRATION, + /** + * Not an onboarding upload purpose: POST /entities/{entityId}/files does not accept it + * ({@code PlatformsFileUpload} defines the other fourteen values only). + */ @SerializedName("dispute_evidence") DISPUTE_EVIDENCE } \ No newline at end of file diff --git a/src/main/java/com/checkout/accounts/files/request/FileUploadRequest.java b/src/main/java/com/checkout/accounts/files/request/FileUploadRequest.java index 9f076e23..7685e003 100644 --- a/src/main/java/com/checkout/accounts/files/request/FileUploadRequest.java +++ b/src/main/java/com/checkout/accounts/files/request/FileUploadRequest.java @@ -8,11 +8,19 @@ import lombok.NoArgsConstructor; import lombok.AllArgsConstructor; +/** + * The request body of POST /entities/{entityId}/files. + */ @Data @Builder @NoArgsConstructor @AllArgsConstructor public final class FileUploadRequest { + /** + * The purpose of the file upload. + * [Required] + */ private FilePurpose purpose; -} \ No newline at end of file + +} diff --git a/src/main/java/com/checkout/accounts/files/response/FileDetailsResponse.java b/src/main/java/com/checkout/accounts/files/response/FileDetailsResponse.java index 467a45ae..fbfa5755 100644 --- a/src/main/java/com/checkout/accounts/files/response/FileDetailsResponse.java +++ b/src/main/java/com/checkout/accounts/files/response/FileDetailsResponse.java @@ -11,23 +11,50 @@ import java.time.Instant; import java.util.List; +/** + * The details of a sub-entity's file, as returned by GET /entities/{entityId}/files/{fileId}. + */ @Data @NoArgsConstructor @AllArgsConstructor @EqualsAndHashCode(callSuper = true) public final class FileDetailsResponse extends Resource { + /** + * The ID of the file. + */ private String id; + /** + * The current status of the file. + */ private String status; + /** + * If {@code status} is {@code invalid}, the reasons why the file was invalid; otherwise null. + */ private List statusReasons; + /** + * The size of the file, in KB. + */ private Long size; + /** + * The MIME type of the file. + */ private String mimeType; + /** + * The date and time the file was uploaded, in ISO 8601 UTC format. + * Format: date-time (RFC 3339) + */ private Instant uploadedOn; + /** + * The purpose of the file, as provided in the initial request. A value {@link FilePurpose} does + * not define deserializes to null. + */ private FilePurpose purpose; -} \ No newline at end of file + +} diff --git a/src/main/java/com/checkout/accounts/files/response/FileUploadResponse.java b/src/main/java/com/checkout/accounts/files/response/FileUploadResponse.java index da8c4ff2..4ff25d35 100644 --- a/src/main/java/com/checkout/accounts/files/response/FileUploadResponse.java +++ b/src/main/java/com/checkout/accounts/files/response/FileUploadResponse.java @@ -9,15 +9,29 @@ import java.util.List; +/** + * The response of POST /entities/{entityId}/files: the file ID and the upload link. The file content + * itself is sent to that link, not in the request. + */ @Data @NoArgsConstructor @AllArgsConstructor @EqualsAndHashCode(callSuper = true) public final class FileUploadResponse extends Resource { + /** + * The file identifier. + */ private String id; + /** + * The maximum file size allowed, in bytes. + */ private Long maximumSizeInBytes; + /** + * The MIME file types allowed for the document purpose provided on the initial request. + */ private List documentTypesForPurpose; -} \ No newline at end of file + +} diff --git a/src/main/java/com/checkout/common/AbstractFileRequest.java b/src/main/java/com/checkout/common/AbstractFileRequest.java index 985ec79a..626b05a6 100644 --- a/src/main/java/com/checkout/common/AbstractFileRequest.java +++ b/src/main/java/com/checkout/common/AbstractFileRequest.java @@ -10,6 +10,10 @@ @AllArgsConstructor public abstract class AbstractFileRequest { + /** + * The file to upload. + * [Required] + */ private File file; /** diff --git a/src/main/java/com/checkout/common/DocumentType.java b/src/main/java/com/checkout/common/DocumentType.java index 40262589..81daab25 100644 --- a/src/main/java/com/checkout/common/DocumentType.java +++ b/src/main/java/com/checkout/common/DocumentType.java @@ -2,6 +2,9 @@ import com.google.gson.annotations.SerializedName; +/** + * The document types accepted to confirm an individual's identity. + */ public enum DocumentType { @SerializedName("passport") diff --git a/src/test/java/com/checkout/accounts/AccountsTestIT.java b/src/test/java/com/checkout/accounts/AccountsTestIT.java index 95327a19..b8d278e8 100644 --- a/src/test/java/com/checkout/accounts/AccountsTestIT.java +++ b/src/test/java/com/checkout/accounts/AccountsTestIT.java @@ -19,6 +19,7 @@ import com.checkout.accounts.files.entities.FilePurpose; import com.checkout.common.Address; import com.checkout.common.CountryCode; +import com.checkout.common.DocumentType; import com.checkout.common.Currency; import com.checkout.common.IdResponse; import com.checkout.common.InstrumentType; @@ -172,6 +173,59 @@ void shouldCreateGetAndUpdateOnboardCompanyEntitySync() { // onboarding tests above (which pin schema_version to "2.0"); here createEntity/getEntity/ // updateEntity use the SDK default (3.0). They run through the accounts-scoped OAuth client, // which is the one provisioned for v3.0 onboarding. + // The representative's documents on schema 3.0. The sandbox platform resolves to a company + // variant (GB/US scope, USD only), where identity_verification and certified_authorised_signatory + // are the representative documents the API accepts; the EEA Sole Trader keys are covered by + // OnboardSubEntityDocumentsSerializationTest, since this platform rejects them. + @Test + void shouldCreateEntityWithRepresentativeDocuments() throws URISyntaxException { + final CheckoutApi checkoutApi = accountsApi(); + final IdResponse identityFile = submitAccountsFile(checkoutApi, AccountsFilePurpose.IDENTITY_VERIFICATION); + final IdResponse signatoryFile = submitAccountsFile(checkoutApi, AccountsFilePurpose.CERTIFIED_AUTHORISED_SIGNATORY); + + final OnboardEntityRequest request = buildCompanyEntityV3(RandomStringUtils.random(15, true, true)); + request.getCompany().getRepresentatives().get(0).setDocuments(OnboardSubEntityDocuments.builder() + .identityVerification(Document.builder() + .type(DocumentType.PASSPORT) + .front(identityFile.getId()) + .build()) + .certifiedAuthorisedSignatory(CertifiedAuthorisedSignatory.builder() + .type(CertifiedAuthorisedSignatoryType.POWER_OF_ATTORNEY) + .front(signatoryFile.getId()) + .build()) + .build()); + + final OnboardEntityResponse entityResponse = blocking(() -> checkoutApi.accountsClient().createEntity(request)); + assertNotNull(entityResponse.getId()); + + // The documents are linked on the representative, not dropped: the API echoes them back. + final OnboardEntityDetailsResponse details = blocking(() -> checkoutApi.accountsClient().getEntity(entityResponse.getId())); + final OnboardSubEntityDocuments linked = details.getCompany().getRepresentatives().get(0).getDocuments(); + assertEquals(DocumentType.PASSPORT, linked.getIdentityVerification().getType()); + assertEquals(identityFile.getId(), linked.getIdentityVerification().getFront()); + assertEquals(CertifiedAuthorisedSignatoryType.POWER_OF_ATTORNEY, linked.getCertifiedAuthorisedSignatory().getType()); + assertEquals(signatoryFile.getId(), linked.getCertifiedAuthorisedSignatory().getFront()); + } + + // The two EEA Sole Trader representative documents need their own upload purposes before they + // can be linked. Goes through POST /entities/{id}/files, the endpoint whose request schema + // (PlatformsFileUpload) defines the purpose enum. + @Test + void shouldUploadRepresentativeProofFilesForEntity() { + final String entityId = createTestEntity(); + + for (final FilePurpose purpose : new FilePurpose[]{FilePurpose.PROOF_OF_RESIDENTIAL_ADDRESS, FilePurpose.PROOF_OF_REGISTRATION}) { + final FileUploadResponse uploadResponse = blocking(() -> accountsApi().accountsClient() + .uploadFile(entityId, FileUploadRequest.builder().purpose(purpose).build())); + validateFileUploadResponseForEntity(uploadResponse); + + final FileDetailsResponse details = blocking(() -> accountsApi().accountsClient() + .retrieveFile(entityId, uploadResponse.getId())); + validateFileDetailsResponseForEntity(details, uploadResponse.getId()); + assertEquals(purpose, details.getPurpose()); + } + } + @Test void shouldCreateGetAndUpdateOnboardCompanyEntityV3() { final CheckoutApi checkoutApi = getAccountsCheckoutApi(); @@ -836,6 +890,18 @@ private IdResponse uploadFile() throws URISyntaxException { return fileResponse; } + private IdResponse submitAccountsFile(final CheckoutApi api, final AccountsFilePurpose purpose) throws URISyntaxException { + final File file = new File(getClass().getClassLoader().getResource("checkout.jpeg").toURI()); + final IdResponse fileResponse = blocking(() -> api.accountsClient().submitFile(AccountsFileRequest.builder() + .file(file) + .contentType(ContentType.IMAGE_JPEG) + .purpose(purpose) + .build())); + assertNotNull(fileResponse); + assertNotNull(fileResponse.getId()); + return fileResponse; + } + private CheckoutApi accountsApi() { if (accountsApi == null) { accountsApi = getAccountsCheckoutApi(); diff --git a/src/test/java/com/checkout/accounts/AccountsV3SerializationTest.java b/src/test/java/com/checkout/accounts/AccountsV3SerializationTest.java index bb9ab2b6..d8395748 100644 --- a/src/test/java/com/checkout/accounts/AccountsV3SerializationTest.java +++ b/src/test/java/com/checkout/accounts/AccountsV3SerializationTest.java @@ -1,12 +1,18 @@ package com.checkout.accounts; import com.checkout.GsonSerializer; +import com.checkout.common.Address; import com.checkout.common.CountryCode; import com.checkout.common.Currency; +import com.checkout.common.DocumentType; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; import org.junit.jupiter.api.Test; import java.util.Arrays; import java.util.Collections; +import java.util.EnumMap; +import java.util.Map; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -37,17 +43,14 @@ void shouldSerializeProcessingDetailsWithPayments() { final String json = serializer.toJson(processingDetails); - assertTrue(json.contains("\"annual_processing_volume\"")); - assertTrue(json.contains("\"average_order_fulfillment_time\"")); - assertTrue(json.contains("\"highest_transaction_value\"")); - assertTrue(json.contains("\"settlement_country\"")); - assertTrue(json.contains("\"target_countries\"")); - assertTrue(json.contains("\"payments\"")); - assertTrue(json.contains("\"ach\"")); - assertTrue(json.contains("\"annual_ach_volume\"")); - assertTrue(json.contains("\"average_ach_transaction_size\"")); - assertTrue(json.contains("\"estimated_monthly_credit_volume\"")); - assertTrue(json.contains("\"average_credit_amount\"")); + assertEquals(JsonParser.parseString("{\"settlement_country\":\"GB\",\"target_countries\":[\"GB\"]," + + "\"annual_processing_volume\":1000000,\"average_transaction_value\":5000," + + "\"highest_transaction_value\":25000,\"currency\":\"GBP\"," + + "\"average_order_fulfillment_time\":3," + + "\"payments\":{\"ach\":{\"annual_ach_volume\":1000000,\"average_ach_transaction_size\":5000," + + "\"estimated_monthly_credit_volume\":100000,\"average_credit_amount\":5000}}}"), + JsonParser.parseString(json), json); + assertEquals(processingDetails, serializer.fromJson(json, ProcessingDetails.class)); } @Test @@ -94,10 +97,13 @@ void shouldSerializeCompanyV3Fields() { @Test void shouldSerializeRepresentativeV3Fields() { final Representative representative = Representative.builder() - .id("rep_00000000000000000000000000") + .id("rep_r2y49v5j1skna5zx0swaprf2he") .individual(RepresentativeIndividual.builder() .firstName("John") + .middleName("Paul") .lastName("Representative") + .dateOfBirth(DateOfBirth.builder().day(5).month(6).year(1995).build()) + .placeOfBirth(PlaceOfBirth.builder().country(CountryCode.US).build()) .citizenships(Collections.singletonList(Citizenship.builder() .type("citizenship") .country(CountryCode.US) @@ -105,6 +111,14 @@ void shouldSerializeRepresentativeV3Fields() { .nationalIdType(NationalIdType.SSN) .nationalIdNumber("AB123456C") .emailAddress("john@example.com") + .phone(AccountPhone.builder().countryCode(CountryCode.US).number("4155678901").build()) + .address(Address.builder() + .addressLine1("123 Main Street") + .city("San Francisco") + .state("CA") + .zip("94105") + .country(CountryCode.US) + .build()) .build()) .companyPosition(CompanyPosition.CEO) .ownershipPercentage(100) @@ -113,19 +127,20 @@ void shouldSerializeRepresentativeV3Fields() { final String json = serializer.toJson(representative); - assertTrue(json.contains("\"individual\"")); - assertTrue(json.contains("\"first_name\"")); - assertTrue(json.contains("\"citizenships\"")); - assertTrue(json.contains("\"country\"")); - assertTrue(json.contains("\"national_id_type\"")); - assertTrue(json.contains("ssn")); - assertTrue(json.contains("\"national_id_number\"")); - assertTrue(json.contains("\"email_address\"")); - assertTrue(json.contains("\"company_position\"")); - assertTrue(json.contains("ceo")); - assertTrue(json.contains("\"ownership_percentage\"")); - assertTrue(json.contains("director")); - assertTrue(json.contains("control_person")); + assertEquals(JsonParser.parseString("{\"id\":\"rep_r2y49v5j1skna5zx0swaprf2he\"," + + "\"roles\":[\"ubo\",\"authorised_signatory\",\"director\",\"control_person\"]," + + "\"company_position\":\"ceo\",\"ownership_percentage\":100," + + "\"individual\":{\"first_name\":\"John\",\"middle_name\":\"Paul\",\"last_name\":\"Representative\"," + + "\"date_of_birth\":{\"day\":5,\"month\":6,\"year\":1995}," + + "\"place_of_birth\":{\"country\":\"US\"}," + + "\"citizenships\":[{\"type\":\"citizenship\",\"country\":\"US\"}]," + + "\"national_id_type\":\"ssn\",\"national_id_number\":\"AB123456C\"," + + "\"email_address\":\"john@example.com\"," + + "\"phone\":{\"country_code\":\"US\",\"number\":\"4155678901\"}," + + "\"address\":{\"address_line1\":\"123 Main Street\",\"city\":\"San Francisco\"," + + "\"state\":\"CA\",\"zip\":\"94105\",\"country\":\"US\"}}}"), + JsonParser.parseString(json), json); + assertEquals(representative, serializer.fromJson(json, Representative.class)); } @Test @@ -133,7 +148,7 @@ void shouldSerializeFinancialStatementsDocument() { final OnboardSubEntityDocuments documents = OnboardSubEntityDocuments.builder() .financialStatements(FinancialStatements.builder() .type(FinancialStatementsType.FINANCIAL_STATEMENTS) - .front("file_00000000000000000000000000") + .front("file_3g7msixotdi2bfqpuwrckgnotu") .build()) .build(); @@ -144,6 +159,25 @@ void shouldSerializeFinancialStatementsDocument() { assertTrue(json.contains("\"front\"")); } + @Test + void shouldSerializeAndRoundtripEntityEmailAddressesWithPciComplianceContact() { + final EntityEmailAddresses emailAddresses = EntityEmailAddresses.builder() + .primary("admin@example.com") + .pciComplianceContact("pci@example.com") + .build(); + + final String json = serializer.toJson(emailAddresses); + + assertTrue(json.contains("\"primary\"")); + assertTrue(json.contains("\"pci_compliance_contact\"")); + + final EntityEmailAddresses roundtrip = serializer.fromJson(json, EntityEmailAddresses.class); + + assertEquals("admin@example.com", roundtrip.getPrimary()); + assertEquals("pci@example.com", roundtrip.getPciComplianceContact()); + assertEquals(emailAddresses, roundtrip); + } + @Test void shouldSerializeOnboardEntityRequestWithAgreedTermsAndSellerCategory() { final OnboardEntityRequest request = OnboardEntityRequest.builder() @@ -168,4 +202,507 @@ void shouldDeserializeNewEnumValuesToExactSwaggerStrings() { assertEquals(CompanyPosition.CEO, serializer.fromJson("\"ceo\"", CompanyPosition.class)); assertEquals(CompanyPosition.OTHER_NON_EXECUTIVE_NON_SENIOR, serializer.fromJson("\"other_non_executive_non_senior\"", CompanyPosition.class)); } + + // ------------------------------------------------------------------------ + // Controlling company representative + // EEA and GB Company Full (3.0) allow a representative that is a company: + // { id, company: { legal_name, trading_name, registered_address }, + // ownership_percentage }. The field was not modelled. + // ------------------------------------------------------------------------ + + @Test + void shouldSerializeControllingCompanyRepresentative() { + final Representative representative = Representative.builder() + .company(Company.builder() + .legalName("Parent Holdings Ltd") + .tradingName("Parent Holdings") + .registeredAddress(Address.builder() + .addressLine1("1 Main Street") + .city("London") + .zip("W1T 4TJ") + .country(CountryCode.GB) + .build()) + .build()) + .ownershipPercentage(60) + .build(); + + assertEquals(JsonParser.parseString("{\"ownership_percentage\":60,\"company\":{" + + "\"legal_name\":\"Parent Holdings Ltd\",\"trading_name\":\"Parent Holdings\"," + + "\"registered_address\":{\"address_line1\":\"1 Main Street\",\"city\":\"London\"," + + "\"zip\":\"W1T 4TJ\",\"country\":\"GB\"}}}"), + JsonParser.parseString(serializer.toJson(representative))); + } + + // ------------------------------------------------------------------------ + // OnboardEntityDetailsResponse + // GET /accounts/entities/{id} returns documents and processing_details; neither + // was modelled, so the top-level documents could not be read back. + // ------------------------------------------------------------------------ + + @Test + void shouldDeserializeEntityDetailsDocumentsAndProcessingDetails() { + final OnboardEntityDetailsResponse response = serializer.fromJson("{" + + "\"id\":\"ent_qx5bjrdqes9rxo9xi8fym2by6o\"," + + "\"processing_details\":{\"currency\":\"USD\",\"annual_processing_volume\":1000000}," + + "\"documents\":{\"bank_verification\":{\"type\":\"bank_statement\"," + + "\"front\":\"file_bankverificationaaaaaaaaaa\"}}," + + "\"company\":{\"representatives\":[{\"documents\":{\"proof_of_registration\":" + + "{\"type\":\"extract_from_trade_register\",\"front\":\"file_proofofregistrationaaaaaaa\"}}}]}}", + OnboardEntityDetailsResponse.class); + + assertEquals(Currency.USD, response.getProcessingDetails().getCurrency()); + assertEquals(Long.valueOf(1000000), response.getProcessingDetails().getAnnualProcessingVolume()); + assertEquals(BankVerificationType.BANK_STATEMENT, response.getDocuments().getBankVerification().getType()); + assertEquals("file_bankverificationaaaaaaaaaa", response.getDocuments().getBankVerification().getFront()); + assertEquals(ProofOfRegistrationType.EXTRACT_FROM_TRADE_REGISTER, response.getCompany().getRepresentatives() + .get(0).getDocuments().getProofOfRegistration().getType()); + } + + // Regression: processing_details amounts are integers in minor units with no maximum. Typed + // as Integer, any value above 2,147,483,647 (about 21.4 million in a two-decimal currency) + // made the whole GET /accounts/entities/{id} fail to deserialize. + @Test + void shouldDeserializeProcessingDetailsAmountsAboveIntegerRange() { + final OnboardEntityDetailsResponse response = serializer.fromJson("{\"processing_details\":{" + + "\"annual_processing_volume\":3000000000," + + "\"average_transaction_value\":2500000000," + + "\"highest_transaction_value\":9000000000}}", + OnboardEntityDetailsResponse.class); + + assertEquals(Long.valueOf(3000000000L), response.getProcessingDetails().getAnnualProcessingVolume()); + assertEquals(Long.valueOf(2500000000L), response.getProcessingDetails().getAverageTransactionValue()); + assertEquals(Long.valueOf(9000000000L), response.getProcessingDetails().getHighestTransactionValue()); + } + + // ------------------------------------------------------------------------ + // AccountsFilePurpose + // submitFile sends getPurpose() on the wire, so every value is asserted as a string. + // ------------------------------------------------------------------------ + + @Test + void shouldExposeEveryAccountsFilePurposeWireValue() { + final Map expected = new EnumMap<>(AccountsFilePurpose.class); + expected.put(AccountsFilePurpose.BANK_VERIFICATION, "bank_verification"); + expected.put(AccountsFilePurpose.IDENTIFICATION, "identification"); + expected.put(AccountsFilePurpose.IDENTITY_VERIFICATION, "identity_verification"); + expected.put(AccountsFilePurpose.COMPANY_VERIFICATION, "company_verification"); + expected.put(AccountsFilePurpose.FINANCIAL_VERIFICATION, "financial_verification"); + expected.put(AccountsFilePurpose.TAX_VERIFICATION, "tax_verification"); + expected.put(AccountsFilePurpose.ADDITIONAL_DOCUMENT, "additional_document"); + expected.put(AccountsFilePurpose.ARTICLES_OF_ASSOCIATION, "articles_of_association"); + expected.put(AccountsFilePurpose.CERTIFIED_AUTHORISED_SIGNATORY, "certified_authorised_signatory"); + expected.put(AccountsFilePurpose.COMPANY_OWNERSHIP, "company_ownership"); + expected.put(AccountsFilePurpose.PROOF_OF_LEGALITY, "proof_of_legality"); + expected.put(AccountsFilePurpose.PROOF_OF_PRINCIPAL_ADDRESS, "proof_of_principal_address"); + expected.put(AccountsFilePurpose.SHAREHOLDER_STRUCTURE, "shareholder_structure"); + expected.put(AccountsFilePurpose.PROOF_OF_RESIDENTIAL_ADDRESS, "proof_of_residential_address"); + expected.put(AccountsFilePurpose.PROOF_OF_REGISTRATION, "proof_of_registration"); + + assertEquals(AccountsFilePurpose.values().length, expected.size(), "every value must be asserted"); + expected.forEach((purpose, wire) -> assertEquals(wire, purpose.getPurpose(), purpose.name())); + } + + // ------------------------------------------------------------------------ + // Spec request examples (USISVSellerCompany3-0, USISVSellerSoleTrader3-0) + // Each example is deserialized into OnboardEntityRequest and serialized back, + // and the two JSON trees must match exactly. + // ------------------------------------------------------------------------ + + @Test + void shouldRoundTripUsIsvSellerCompanyExample() { + final String example = "{" + + "\"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\"}}}]}" + + "}"; + + final JsonObject actual = JsonParser.parseString( + serializer.toJson(serializer.fromJson(example, OnboardEntityRequest.class))).getAsJsonObject(); + // is_draft is a primitive boolean on the request, so it is always serialized. + actual.remove("is_draft"); + + assertEquals(JsonParser.parseString(example), actual); + } + + @Test + void shouldRoundTripUsIsvSellerSoleTraderExample() { + final String example = "{" + + "\"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\"}}}]}" + + "}"; + + final JsonObject actual = JsonParser.parseString( + serializer.toJson(serializer.fromJson(example, OnboardEntityRequest.class))).getAsJsonObject(); + // is_draft is a primitive boolean on the request, so it is always serialized. + actual.remove("is_draft"); + + assertEquals(JsonParser.parseString(example), actual); + } + + // ------------------------------------------------------------------------ + // ContactDetails + // phone with country_code, email_addresses and invitee, none of which the + // spec examples reach together. + // ------------------------------------------------------------------------ + + @Test + void shouldSerializeAndRoundTripContactDetailsWithInvitee() { + final ContactDetails contactDetails = ContactDetails.builder() + .phone(AccountPhone.builder().countryCode(CountryCode.GB).number("2072345678").build()) + .emailAddresses(EntityEmailAddresses.builder() + .primary("admin@example.com") + .pciComplianceContact("pci@example.com") + .build()) + .invitee(Invitee.builder().email("invitee@example.com").build()) + .build(); + + final String json = serializer.toJson(contactDetails); + + assertEquals(JsonParser.parseString("{\"phone\":{\"country_code\":\"GB\",\"number\":\"2072345678\"}," + + "\"email_addresses\":{\"primary\":\"admin@example.com\"," + + "\"pci_compliance_contact\":\"pci@example.com\"}," + + "\"invitee\":{\"email\":\"invitee@example.com\"}}"), + JsonParser.parseString(json), json); + assertEquals(contactDetails, serializer.fromJson(json, ContactDetails.class)); + } + + // ------------------------------------------------------------------------ + // Company + // Every spec property, including principal_address, regulatory_licence_number, + // financial_details and the full date_of_incorporation. The deprecated + // document field is absent from every variant and stays unset. + // ------------------------------------------------------------------------ + + @Test + void shouldSerializeAndRoundTripEveryCompanyField() { + final Company company = Company.builder() + .legalName("Super Hero Masks Ltd") + .tradingName("Super Hero Masks") + .businessRegistrationNumber("01234567") + .dateOfIncorporation(DateOfIncorporation.builder().day(1).month(6).year(2010).build()) + .regulatoryLicenceNumber("FRN123456") + .principalAddress(Address.builder() + .addressLine1("90 Tottenham Court Road") + .addressLine2("Floor 2") + .city("London") + .state("London") + .zip("W1T 4TJ") + .country(CountryCode.GB) + .build()) + .registeredAddress(Address.builder() + .addressLine1("1 Main Street") + .city("London") + .zip("W1T 4TJ") + .country(CountryCode.GB) + .build()) + .representatives(Collections.singletonList(Representative.builder() + .id("rep_xoo3xudh9mgxw6tv4140063pdi") + .individual(RepresentativeIndividual.builder().firstName("Jane").lastName("Doe").build()) + .roles(Collections.singletonList(EntityRoles.UBO)) + .ownershipPercentage(75) + .build())) + .financialDetails(EntityFinancialDetails.builder() + .annualProcessingVolume(120000000L) + .averageTransactionValue(10000L) + .highestTransactionValue(2500000L) + .currency(Currency.GBP) + .build()) + .businessType(BusinessType.LIMITED_COMPANY) + .additionalTradingNames(Arrays.asList("SHM", "Hero Masks")) + .isRegisteredCompany(true) + .build(); + + final String json = serializer.toJson(company); + + assertEquals(JsonParser.parseString("{\"legal_name\":\"Super Hero Masks Ltd\"," + + "\"trading_name\":\"Super Hero Masks\",\"business_registration_number\":\"01234567\"," + + "\"date_of_incorporation\":{\"day\":1,\"month\":6,\"year\":2010}," + + "\"regulatory_licence_number\":\"FRN123456\"," + + "\"principal_address\":{\"address_line1\":\"90 Tottenham Court Road\",\"address_line2\":\"Floor 2\"," + + "\"city\":\"London\",\"state\":\"London\",\"zip\":\"W1T 4TJ\",\"country\":\"GB\"}," + + "\"registered_address\":{\"address_line1\":\"1 Main Street\",\"city\":\"London\"," + + "\"zip\":\"W1T 4TJ\",\"country\":\"GB\"}," + + "\"representatives\":[{\"id\":\"rep_xoo3xudh9mgxw6tv4140063pdi\",\"roles\":[\"ubo\"]," + + "\"ownership_percentage\":75,\"individual\":{\"first_name\":\"Jane\",\"last_name\":\"Doe\"}}]," + + "\"financial_details\":{\"annual_processing_volume\":120000000,\"average_transaction_value\":10000," + + "\"highest_transaction_value\":2500000,\"currency\":\"GBP\"}," + + "\"business_type\":\"limited_company\",\"additional_trading_names\":[\"SHM\",\"Hero Masks\"]," + + "\"is_registered_company\":true}"), + JsonParser.parseString(json), json); + assertEquals(company, serializer.fromJson(json, Company.class)); + } + + // ------------------------------------------------------------------------ + // Representative and Individual (v2.0) + // The v2.0 shape puts the person fields directly on the representative, and + // the v2.0 top-level individual carries identification and financial_details. + // ------------------------------------------------------------------------ + + @Test + @SuppressWarnings("deprecation") + void shouldSerializeAndRoundTripV2Representative() { + final Representative representative = Representative.builder() + .id("rep_r2y49v5j1skna5zx0swaprf2he") + .firstName("John") + .middleName("Paul") + .lastName("Doe") + .dateOfBirth(DateOfBirth.builder().day(5).month(6).year(1995).build()) + .phone(AccountPhone.builder().number("2072345678").build()) + .address(Address.builder() + .addressLine1("90 Tottenham Court Road") + .city("London") + .zip("W1T 4TJ") + .country(CountryCode.GB) + .build()) + .placeOfBirth(PlaceOfBirth.builder().country(CountryCode.FR).build()) + .identification(Identification.builder().nationalIdNumber("AB123456C").build()) + .build(); + + final String json = serializer.toJson(representative); + + assertEquals(JsonParser.parseString("{\"id\":\"rep_r2y49v5j1skna5zx0swaprf2he\"," + + "\"first_name\":\"John\",\"middle_name\":\"Paul\",\"last_name\":\"Doe\"," + + "\"date_of_birth\":{\"day\":5,\"month\":6,\"year\":1995}," + + "\"phone\":{\"number\":\"2072345678\"}," + + "\"address\":{\"address_line1\":\"90 Tottenham Court Road\",\"city\":\"London\"," + + "\"zip\":\"W1T 4TJ\",\"country\":\"GB\"}," + + "\"place_of_birth\":{\"country\":\"FR\"}," + + "\"identification\":{\"national_id_number\":\"AB123456C\"}}"), + JsonParser.parseString(json), json); + assertEquals(representative, serializer.fromJson(json, Representative.class)); + } + + @Test + void shouldSerializeAndRoundTripV2Individual() { + final Individual individual = Individual.builder() + .firstName("Jane") + .middleName("Anne") + .lastName("Doe") + .tradingName("Jane's Crafts") + .registeredAddress(Address.builder() + .addressLine1("90 Tottenham Court Road") + .city("London") + .zip("W1T 4TJ") + .country(CountryCode.GB) + .build()) + .dateOfBirth(DateOfBirth.builder().day(15).month(1).year(1990).build()) + .placeOfBirth(PlaceOfBirth.builder().country(CountryCode.GB).build()) + .identification(Identification.builder().nationalIdNumber("QQ123456C").build()) + .financialDetails(EntityFinancialDetails.builder() + .annualProcessingVolume(5000000L) + .averageTransactionValue(2500L) + .highestTransactionValue(100000L) + .currency(Currency.GBP) + .build()) + .build(); + + final String json = serializer.toJson(individual); + + assertEquals(JsonParser.parseString("{\"first_name\":\"Jane\",\"middle_name\":\"Anne\"," + + "\"last_name\":\"Doe\",\"trading_name\":\"Jane's Crafts\"," + + "\"registered_address\":{\"address_line1\":\"90 Tottenham Court Road\",\"city\":\"London\"," + + "\"zip\":\"W1T 4TJ\",\"country\":\"GB\"}," + + "\"date_of_birth\":{\"day\":15,\"month\":1,\"year\":1990}," + + "\"place_of_birth\":{\"country\":\"GB\"}," + + "\"identification\":{\"national_id_number\":\"QQ123456C\"}," + + "\"financial_details\":{\"annual_processing_volume\":5000000,\"average_transaction_value\":2500," + + "\"highest_transaction_value\":100000,\"currency\":\"GBP\"}}"), + JsonParser.parseString(json), json); + assertEquals(individual, serializer.fromJson(json, Individual.class)); + } + + // ------------------------------------------------------------------------ + // Identification + // The spec has national_id_number only; the deprecated document stays unset. + // ------------------------------------------------------------------------ + + @Test + void shouldSerializeAndRoundTripIdentification() { + final Identification identification = Identification.builder().nationalIdNumber("AB123456C").build(); + + final String json = serializer.toJson(identification); + + assertEquals(JsonParser.parseString("{\"national_id_number\":\"AB123456C\"}"), JsonParser.parseString(json), json); + assertEquals(identification, serializer.fromJson(json, Identification.class)); + } + + // ------------------------------------------------------------------------ + // AccountsFileRequest + // The purpose is sent as a multipart text part built from getPurpose(), not + // through Gson, so the wire value is the getPurpose() string. + // ------------------------------------------------------------------------ + + @Test + void shouldSendAccountsFileRequestPurposeWireValue() { + final AccountsFileRequest request = AccountsFileRequest.builder() + .purpose(AccountsFilePurpose.IDENTITY_VERIFICATION) + .build(); + + assertEquals("identity_verification", request.getPurpose().getPurpose()); + } + + // ------------------------------------------------------------------------ + // Enum wire values + // Every value is serialized through Gson and compared to the spec enum string. + // ------------------------------------------------------------------------ + + @Test + void shouldSerializeEveryIdentityVerificationDocumentTypeWireValue() { + final Map expected = new EnumMap<>(DocumentType.class); + expected.put(DocumentType.PASSPORT, "passport"); + expected.put(DocumentType.NATIONAL_IDENTITY_CARD, "national_identity_card"); + expected.put(DocumentType.DRIVING_LICENSE, "driving_license"); + expected.put(DocumentType.CITIZEN_CARD, "citizen_card"); + expected.put(DocumentType.RESIDENCE_PERMIT, "residence_permit"); + expected.put(DocumentType.ELECTORAL_ID, "electoral_id"); + + assertEveryWireValue(DocumentType.class, expected); + } + + @Test + void shouldSerializeEveryCompanyVerificationTypeWireValue() { + final Map expected = new EnumMap<>(CompanyVerificationType.class); + expected.put(CompanyVerificationType.INCORPORATION_DOCUMENT, "incorporation_document"); + expected.put(CompanyVerificationType.ARTICLES_OF_ASSOCIATION, "articles_of_association"); + + assertEveryWireValue(CompanyVerificationType.class, expected); + } + + @Test + void shouldSerializeEveryArticlesOfAssociationTypeWireValue() { + final Map expected = new EnumMap<>(ArticlesOfAssociationType.class); + expected.put(ArticlesOfAssociationType.ARTICLES_OF_ASSOCIATION, "articles_of_association"); + expected.put(ArticlesOfAssociationType.MEMORANDUM_OF_ASSOCIATION, "memorandum_of_association"); + + assertEveryWireValue(ArticlesOfAssociationType.class, expected); + } + + @Test + void shouldSerializeEveryNationalIdTypeWireValue() { + final Map expected = new EnumMap<>(NationalIdType.class); + expected.put(NationalIdType.SSN, "ssn"); + expected.put(NationalIdType.ITIN, "itin"); + expected.put(NationalIdType.PASSPORT, "passport"); + expected.put(NationalIdType.DRIVING_LICENSE, "driving_license"); + expected.put(NationalIdType.NATIONAL_ID_CARD, "national_id_card"); + expected.put(NationalIdType.RESIDENCE_PERMIT, "residence_permit"); + expected.put(NationalIdType.OTHER, "other"); + + assertEveryWireValue(NationalIdType.class, expected); + } + + @Test + void shouldSerializeEveryEntityRolesWireValue() { + final Map expected = new EnumMap<>(EntityRoles.class); + expected.put(EntityRoles.UBO, "ubo"); + expected.put(EntityRoles.LEGAL_REPRESENTATIVE, "legal_representative"); + expected.put(EntityRoles.AUTHORISED_SIGNATORY, "authorised_signatory"); + expected.put(EntityRoles.DIRECTOR, "director"); + expected.put(EntityRoles.CONTROL_PERSON, "control_person"); + + assertEveryWireValue(EntityRoles.class, expected); + } + + @Test + void shouldSerializeEveryCompanyPositionWireValue() { + final Map expected = new EnumMap<>(CompanyPosition.class); + expected.put(CompanyPosition.CEO, "ceo"); + expected.put(CompanyPosition.CFO, "cfo"); + expected.put(CompanyPosition.COO, "coo"); + expected.put(CompanyPosition.MANAGING_MEMBER, "managing_member"); + expected.put(CompanyPosition.GENERAL_PARTNER, "general_partner"); + expected.put(CompanyPosition.PRESIDENT, "president"); + expected.put(CompanyPosition.VICE_PRESIDENT, "vice_president"); + expected.put(CompanyPosition.TREASURER, "treasurer"); + expected.put(CompanyPosition.OTHER_SENIOR_MANAGEMENT, "other_senior_management"); + expected.put(CompanyPosition.OTHER_EXECUTIVE_OFFICER, "other_executive_officer"); + expected.put(CompanyPosition.OTHER_NON_EXECUTIVE_NON_SENIOR, "other_non_executive_non_senior"); + + assertEveryWireValue(CompanyPosition.class, expected); + } + + @Test + void shouldSerializeEveryBusinessTypeWireValue() { + final Map expected = new EnumMap<>(BusinessType.class); + expected.put(BusinessType.GENERAL_PARTNERSHIP, "general_partnership"); + expected.put(BusinessType.LIMITED_PARTNERSHIP, "limited_partnership"); + expected.put(BusinessType.PUBLIC_LIMITED_COMPANY, "public_limited_company"); + expected.put(BusinessType.LIMITED_COMPANY, "limited_company"); + expected.put(BusinessType.PROFESSIONAL_ASSOCIATION, "professional_association"); + expected.put(BusinessType.UNINCORPORATED_ASSOCIATION, "unincorporated_association"); + expected.put(BusinessType.AUTO_ENTREPRENEUR, "auto_entrepreneur"); + expected.put(BusinessType.INDIVIDUAL_OR_SOLE_PROPRIETORSHIP, "individual_or_sole_proprietorship"); + expected.put(BusinessType.SCOTTISH_LIMITED_PARTNERSHIP, "scottish_limited_partnership"); + expected.put(BusinessType.LIMITED_LIABILITY_CORPORATION, "limited_liability_corporation"); + expected.put(BusinessType.PRIVATE_CORPORATION, "private_corporation"); + expected.put(BusinessType.PUBLICLY_TRADED_CORPORATION, "publicly_traded_corporation"); + expected.put(BusinessType.GOVERNMENT_AGENCY, "government_agency"); + expected.put(BusinessType.NON_PROFIT_ENTITY, "non_profit_entity"); + expected.put(BusinessType.TRUST, "trust"); + expected.put(BusinessType.CLUB_OR_SOCIETY, "club_or_society"); + expected.put(BusinessType.REGULATED_FINANCIAL_INSTITUTION, "regulated_financial_institution"); + expected.put(BusinessType.CFTC_REGISTERED_ENTITY, "cftc_registered_entity"); + expected.put(BusinessType.SEC_REGISTERED_ENTITY, "sec_registered_entity"); + + assertEveryWireValue(BusinessType.class, expected); + } + + private > void assertEveryWireValue(final Class type, final Map expected) { + assertEquals(type.getEnumConstants().length, expected.size(), "every value must be asserted"); + expected.forEach((value, wire) -> { + assertEquals("\"" + wire + "\"", serializer.toJson(value), value.name()); + assertEquals(value, serializer.fromJson("\"" + wire + "\"", type), wire); + }); + } } diff --git a/src/test/java/com/checkout/accounts/OnboardSubEntityDocumentsSerializationTest.java b/src/test/java/com/checkout/accounts/OnboardSubEntityDocumentsSerializationTest.java index b2182d88..8da1d0fd 100644 --- a/src/test/java/com/checkout/accounts/OnboardSubEntityDocumentsSerializationTest.java +++ b/src/test/java/com/checkout/accounts/OnboardSubEntityDocumentsSerializationTest.java @@ -1,8 +1,13 @@ package com.checkout.accounts; import com.checkout.GsonSerializer; +import com.checkout.common.DocumentType; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; import org.junit.jupiter.api.Test; +import java.util.Collections; + import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -58,11 +63,11 @@ void shouldSerializeBankVerificationAndShareholderStructureAsObjects() { final OnboardSubEntityDocuments documents = OnboardSubEntityDocuments.builder() .bankVerification(BankVerification.builder() .type(BankVerificationType.BANK_STATEMENT) - .front("file_bank") + .front("file_jj7e4kwpcenegfwy4dpscnf4kl") .build()) .shareholderStructure(ShareholderStructure.builder() .type(ShareholderStructureType.CERTIFIED_SHAREHOLDER_STRUCTURE) - .front("file_shareholder") + .front("file_v2jnxxmuzhnmne2xemjvypx3lb") .build()) .build(); @@ -82,4 +87,186 @@ void shouldDeserializeArticlesOfAssociation() { assertEquals(ArticlesOfAssociationType.ARTICLES_OF_ASSOCIATION, documents.getArticlesOfAssociation().getType()); assertEquals("file_6lbss42ezvoufcb2beo76rvwly", documents.getArticlesOfAssociation().getFront()); } + + // ------------------------------------------------------------------------ + // Representative documents (company.representatives[].documents) + // The EEA Sole Trader (3.0) keys and the company-variant certified authorised + // signatory. The representative object is strict on the Company Full and Sole + // Trader Full (3.0) variants of EEA, GB and US, so the exact key set matters. + // ------------------------------------------------------------------------ + + // Regression: EEA Sole Trader (3.0) needs proof_of_residential_address and proof_of_registration + // on the representative, with bank_verification alone at the top level. Neither could be + // expressed on the representative before. + @Test + void shouldSerializeEeaSoleTraderRepresentativeDocuments() { + final OnboardEntityRequest request = OnboardEntityRequest.builder() + .reference("ref_sole_trader") + .company(Company.builder() + .businessType(BusinessType.INDIVIDUAL_OR_SOLE_PROPRIETORSHIP) + .representatives(Collections.singletonList(Representative.builder() + .individual(RepresentativeIndividual.builder().firstName("Jane").lastName("Doe").build()) + .roles(Collections.singletonList(EntityRoles.UBO)) + .documents(eeaSoleTraderRepresentativeDocuments()) + .build())) + .build()) + .documents(OnboardSubEntityDocuments.builder() + .bankVerification(BankVerification.builder() + .type(BankVerificationType.BANK_STATEMENT) + .front("file_bankverificationaaaaaaaaaa") + .build()) + .build()) + .build(); + + final String json = serializer.toJson(request); + final JsonObject body = JsonParser.parseString(json).getAsJsonObject(); + + assertEquals(JsonParser.parseString("{" + + "\"identity_verification\":{\"type\":\"passport\",\"front\":\"file_identityverificationaaaaaa\"}," + + "\"proof_of_residential_address\":{\"type\":\"proof_of_address\",\"front\":\"file_proofofresidentialaddressa\"}," + + "\"proof_of_registration\":{\"type\":\"extract_from_trade_register\",\"front\":\"file_proofofregistrationaaaaaaa\"}}"), + body.getAsJsonObject("company").getAsJsonArray("representatives").get(0) + .getAsJsonObject().get("documents"), json); + assertEquals(Collections.singleton("bank_verification"), body.getAsJsonObject("documents").keySet(), json); + // Key-level check on the raw body, so a naming-policy change cannot pass silently. + assertTrue(json.contains("\"proof_of_residential_address\":{"), json); + assertTrue(json.contains("\"proof_of_registration\":{"), json); + } + + @Test + void shouldRoundTripRepresentativeDocuments() { + final OnboardSubEntityDocuments original = eeaSoleTraderRepresentativeDocuments(); + + final OnboardSubEntityDocuments roundTripped = + serializer.fromJson(serializer.toJson(original), OnboardSubEntityDocuments.class); + + assertEquals(original, roundTripped); + } + + @Test + void shouldDeserializeProofOfRegistrationOtherType() { + final OnboardSubEntityDocuments documents = serializer.fromJson( + "{\"proof_of_registration\":{\"type\":\"other\",\"front\":\"file_proofofregistrationaaaaaaa\"}}", + OnboardSubEntityDocuments.class); + + assertEquals(ProofOfRegistrationType.OTHER, documents.getProofOfRegistration().getType()); + } + + @Test + void shouldSerializeCertifiedAuthorisedSignatoryWithTypeAndFrontOnly() { + final OnboardSubEntityDocuments documents = OnboardSubEntityDocuments.builder() + .certifiedAuthorisedSignatory(CertifiedAuthorisedSignatory.builder() + .type(CertifiedAuthorisedSignatoryType.POWER_OF_ATTORNEY) + .front("file_signatoryaaaaaaaaaaaaaaaaa") + .build()) + .build(); + + assertEquals(JsonParser.parseString("{\"certified_authorised_signatory\":" + + "{\"type\":\"power_of_attorney\",\"front\":\"file_signatoryaaaaaaaaaaaaaaaaa\"}}"), + JsonParser.parseString(serializer.toJson(documents))); + } + + // ------------------------------------------------------------------------ + // Company and tax verification + // Regression: CompanyVerification carried TaxVerificationType and TaxVerification + // carried CompanyVerificationType, so incorporation_document (required on the + // EEA and GB Company Full variants) and ein_letter could not be sent. + // ------------------------------------------------------------------------ + + @Test + void shouldSendCompanyAndTaxVerificationTypesUnderTheirOwnKeys() { + final OnboardSubEntityDocuments documents = OnboardSubEntityDocuments.builder() + .companyVerification(CompanyVerification.builder() + .type(CompanyVerificationType.INCORPORATION_DOCUMENT) + .front("file_reoytgtkcvtxlnco2u4l2ewg37") + .build()) + .taxVerification(TaxVerification.builder() + .type(TaxVerificationType.EIN_LETTER) + .front("file_reoytgtkcvtxlnco2u4l2ewg37") + .build()) + .build(); + + final JsonObject json = JsonParser.parseString(serializer.toJson(documents)).getAsJsonObject(); + + assertEquals("incorporation_document", + json.getAsJsonObject("company_verification").get("type").getAsString()); + assertEquals("ein_letter", json.getAsJsonObject("tax_verification").get("type").getAsString()); + } + + // ------------------------------------------------------------------------ + // Every field of OnboardSubEntityDocuments + // Exact JSON for all 16 fields, so a naming-policy change on any key cannot pass + // silently, then a full round trip. + // ------------------------------------------------------------------------ + + @Test + void shouldSerializeAndRoundTripEveryDocumentsField() { + final String file = "file_reoytgtkcvtxlnco2u4l2ewg37"; + final OnboardSubEntityDocuments documents = OnboardSubEntityDocuments.builder() + .identityVerification(Document.builder().type(DocumentType.PASSPORT).front(file).back(file).build()) + .companyVerification(CompanyVerification.builder() + .type(CompanyVerificationType.INCORPORATION_DOCUMENT).front(file).build()) + .articlesOfAssociation(ArticlesOfAssociation.builder() + .type(ArticlesOfAssociationType.ARTICLES_OF_ASSOCIATION).front(file).build()) + .bankVerification(BankVerification.builder().type(BankVerificationType.BANK_STATEMENT).front(file).build()) + .shareholderStructure(ShareholderStructure.builder() + .type(ShareholderStructureType.CERTIFIED_SHAREHOLDER_STRUCTURE).front(file).build()) + .proofOfLegality(ProofOfLegality.builder().type(ProofOfLegalityType.PROOF_OF_LEGALITY).front(file).build()) + .proofOfPrincipalAddress(ProofOfPrincipalAddress.builder() + .type(ProofOfPrincipalAddressType.PROOF_OF_ADDRESS).front(file).build()) + .additionalDocument1(AdditionalDocument.builder().front(file).build()) + .additionalDocument2(AdditionalDocument.builder().front(file).build()) + .additionalDocument3(AdditionalDocument.builder().front(file).build()) + .taxVerification(TaxVerification.builder().type(TaxVerificationType.EIN_LETTER).front(file).build()) + .financialVerification(FinancialVerification.builder() + .type(FinancialVerificationType.FINANCIAL_STATEMENT).front(file).build()) + .financialStatements(FinancialStatements.builder() + .type(FinancialStatementsType.FINANCIAL_STATEMENTS).front(file).build()) + .certifiedAuthorisedSignatory(CertifiedAuthorisedSignatory.builder() + .type(CertifiedAuthorisedSignatoryType.POWER_OF_ATTORNEY).front(file).build()) + .proofOfResidentialAddress(ProofOfResidentialAddress.builder() + .type(ProofOfResidentialAddressType.PROOF_OF_ADDRESS).front(file).build()) + .proofOfRegistration(ProofOfRegistration.builder() + .type(ProofOfRegistrationType.EXTRACT_FROM_TRADE_REGISTER).front(file).build()) + .build(); + + final String json = serializer.toJson(documents); + + assertEquals(JsonParser.parseString("{" + + "\"identity_verification\":{\"type\":\"passport\",\"front\":\"" + file + "\",\"back\":\"" + file + "\"}," + + "\"company_verification\":{\"type\":\"incorporation_document\",\"front\":\"" + file + "\"}," + + "\"articles_of_association\":{\"type\":\"articles_of_association\",\"front\":\"" + file + "\"}," + + "\"bank_verification\":{\"type\":\"bank_statement\",\"front\":\"" + file + "\"}," + + "\"shareholder_structure\":{\"type\":\"certified_shareholder_structure\",\"front\":\"" + file + "\"}," + + "\"proof_of_legality\":{\"type\":\"proof_of_legality\",\"front\":\"" + file + "\"}," + + "\"proof_of_principal_address\":{\"type\":\"proof_of_address\",\"front\":\"" + file + "\"}," + + "\"additional_document1\":{\"front\":\"" + file + "\"}," + + "\"additional_document2\":{\"front\":\"" + file + "\"}," + + "\"additional_document3\":{\"front\":\"" + file + "\"}," + + "\"tax_verification\":{\"type\":\"ein_letter\",\"front\":\"" + file + "\"}," + + "\"financial_verification\":{\"type\":\"financial_statement\",\"front\":\"" + file + "\"}," + + "\"financial_statements\":{\"type\":\"financial_statements\",\"front\":\"" + file + "\"}," + + "\"certified_authorised_signatory\":{\"type\":\"power_of_attorney\",\"front\":\"" + file + "\"}," + + "\"proof_of_residential_address\":{\"type\":\"proof_of_address\",\"front\":\"" + file + "\"}," + + "\"proof_of_registration\":{\"type\":\"extract_from_trade_register\",\"front\":\"" + file + "\"}}"), + JsonParser.parseString(json), json); + assertEquals(documents, serializer.fromJson(json, OnboardSubEntityDocuments.class)); + } + + private static OnboardSubEntityDocuments eeaSoleTraderRepresentativeDocuments() { + return OnboardSubEntityDocuments.builder() + .identityVerification(Document.builder() + .type(DocumentType.PASSPORT) + .front("file_identityverificationaaaaaa") + .build()) + .proofOfResidentialAddress(ProofOfResidentialAddress.builder() + .type(ProofOfResidentialAddressType.PROOF_OF_ADDRESS) + .front("file_proofofresidentialaddressa") + .build()) + .proofOfRegistration(ProofOfRegistration.builder() + .type(ProofOfRegistrationType.EXTRACT_FROM_TRADE_REGISTER) + .front("file_proofofregistrationaaaaaaa") + .build()) + .build(); + } } diff --git a/src/test/java/com/checkout/accounts/files/AccountsFilesSerializationTest.java b/src/test/java/com/checkout/accounts/files/AccountsFilesSerializationTest.java new file mode 100644 index 00000000..42671af7 --- /dev/null +++ b/src/test/java/com/checkout/accounts/files/AccountsFilesSerializationTest.java @@ -0,0 +1,121 @@ +package com.checkout.accounts.files; + +import com.checkout.GsonSerializer; +import com.checkout.accounts.files.entities.FilePurpose; +import com.checkout.accounts.files.request.FileUploadRequest; +import com.checkout.accounts.files.response.FileDetailsResponse; +import com.checkout.accounts.files.response.FileUploadResponse; +import com.google.gson.JsonParser; +import org.junit.jupiter.api.Test; + +import java.time.Instant; +import java.util.Arrays; +import java.util.Collections; +import java.util.EnumMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class AccountsFilesSerializationTest { + + private final GsonSerializer serializer = new GsonSerializer(); + + // ------------------------------------------------------------------------ + // FileUploadResponse (PlatformsFileUploadResponse) + // Built from the spec's per-field example values, including the upload link. + // ------------------------------------------------------------------------ + + @Test + void shouldDeserializeFileUploadResponseSwaggerExample() { + final String upload = "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"; + final String self = "https://files.checkout.com/files/file_6lbss42ezvoufcb2beo76rvwly"; + + final FileUploadResponse response = serializer.fromJson("{" + + "\"id\":\"file_6lbss42ezvoufcb2beo76rvwly\"," + + "\"maximum_size_in_bytes\":4194304," + + "\"document_types_for_purpose\":[\"image/jpeg\",\"image/png\",\"image/jpg\"]," + + "\"_links\":{\"upload\":{\"href\":\"" + upload + "\"},\"self\":{\"href\":\"" + self + "\"}}}", + FileUploadResponse.class); + + assertEquals("file_6lbss42ezvoufcb2beo76rvwly", response.getId()); + assertEquals(Long.valueOf(4194304L), response.getMaximumSizeInBytes()); + assertEquals(Arrays.asList("image/jpeg", "image/png", "image/jpg"), response.getDocumentTypesForPurpose()); + assertEquals(2, response.getLinks().size()); + assertEquals(upload, response.getLink("upload").getHref()); + assertEquals(self, response.getSelfLink().getHref()); + } + + // ------------------------------------------------------------------------ + // FileDetailsResponse (PlatformsFileRetrieveResponse) + // uploaded_on uses the spec's seven fractional digits with a +00:00 offset. + // ------------------------------------------------------------------------ + + @Test + void shouldDeserializeFileDetailsResponseSwaggerExample() { + final String download = "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"; + final String self = "https://files.checkout.com/files/file_6lbss42ezvoufcb2beo76rvwly"; + + final FileDetailsResponse response = serializer.fromJson("{" + + "\"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\":\"" + download + "\"},\"self\":{\"href\":\"" + self + "\"}}}", + FileDetailsResponse.class); + + assertEquals("file_6lbss42ezvoufcb2beo76rvwly", response.getId()); + assertEquals("invalid", response.getStatus()); + assertEquals(Collections.singletonList("InvalidMimeType"), response.getStatusReasons()); + assertEquals(Long.valueOf(1024L), response.getSize()); + assertEquals("application/pdf", response.getMimeType()); + assertEquals(Instant.parse("2020-12-01T15:01:01Z"), response.getUploadedOn()); + assertEquals(FilePurpose.IDENTITY_VERIFICATION, response.getPurpose()); + assertEquals(2, response.getLinks().size()); + assertEquals(download, response.getLink("download").getHref()); + assertEquals(self, response.getSelfLink().getHref()); + } + + // ------------------------------------------------------------------------ + // FileUploadRequest and FilePurpose + // ------------------------------------------------------------------------ + + @Test + void shouldSerializeFileUploadRequestPurposeOnly() { + final FileUploadRequest request = FileUploadRequest.builder().purpose(FilePurpose.IDENTITY_VERIFICATION).build(); + + assertEquals(JsonParser.parseString("{\"purpose\":\"identity_verification\"}"), + JsonParser.parseString(serializer.toJson(request))); + } + + @Test + void shouldSerializeEveryFilePurposeWireValue() { + final Map expected = new EnumMap<>(FilePurpose.class); + expected.put(FilePurpose.ADDITIONAL_DOCUMENT, "additional_document"); + expected.put(FilePurpose.ARTICLES_OF_ASSOCIATION, "articles_of_association"); + expected.put(FilePurpose.BANK_VERIFICATION, "bank_verification"); + expected.put(FilePurpose.CERTIFIED_AUTHORISED_SIGNATORY, "certified_authorised_signatory"); + expected.put(FilePurpose.COMPANY_OWNERSHIP, "company_ownership"); + expected.put(FilePurpose.COMPANY_VERIFICATION, "company_verification"); + expected.put(FilePurpose.FINANCIAL_VERIFICATION, "financial_verification"); + expected.put(FilePurpose.IDENTITY_VERIFICATION, "identity_verification"); + expected.put(FilePurpose.PROOF_OF_LEGALITY, "proof_of_legality"); + expected.put(FilePurpose.PROOF_OF_PRINCIPAL_ADDRESS, "proof_of_principal_address"); + expected.put(FilePurpose.SHAREHOLDER_STRUCTURE, "shareholder_structure"); + expected.put(FilePurpose.TAX_VERIFICATION, "tax_verification"); + expected.put(FilePurpose.PROOF_OF_RESIDENTIAL_ADDRESS, "proof_of_residential_address"); + expected.put(FilePurpose.PROOF_OF_REGISTRATION, "proof_of_registration"); + expected.put(FilePurpose.DISPUTE_EVIDENCE, "dispute_evidence"); + + assertEquals(FilePurpose.values().length, expected.size(), "every value must be asserted"); + expected.forEach((purpose, wire) -> { + assertEquals("\"" + wire + "\"", serializer.toJson(purpose), purpose.name()); + assertEquals(purpose, serializer.fromJson("\"" + wire + "\"", FilePurpose.class), wire); + }); + } +}