diff --git a/checkout_sdk/accounts/accounts.py b/checkout_sdk/accounts/accounts.py index 5225b14d..c65896ce 100644 --- a/checkout_sdk/accounts/accounts.py +++ b/checkout_sdk/accounts/accounts.py @@ -7,12 +7,15 @@ class ScheduleFrequency(str, Enum): + """How often funds are paid out to a sub-entity: the recurrence.frequency of a payout schedule.""" WEEKLY = 'weekly' DAILY = 'daily' MONTHLY = 'monthly' class DaySchedule(str, Enum): + """The days of the week a weekly payout can take place on (by_day). For ISV (SaaS seller) + sub-entities, only monday to friday are accepted.""" MONDAY = 'monday' TUESDAY = 'tuesday' WEDNESDAY = 'wednesday' @@ -23,6 +26,9 @@ class DaySchedule(str, Enum): class BusinessType(str, Enum): + """The legal type of the company (company.business_type). The union of the values every variant + accepts; each variant accepts a subset, and the sole trader variants accept + individual_or_sole_proprietorship only.""" INDIVIDUAL_OR_SOLE_PROPRIETORSHIP = 'individual_or_sole_proprietorship' GENERAL_PARTNERSHIP = 'general_partnership' LIMITED_PARTNERSHIP = 'limited_partnership' @@ -45,6 +51,9 @@ class BusinessType(str, Enum): class EntityRoles(str, Enum): + """The roles of a representative within the company (representatives[].roles). Each variant + accepts a subset: director on GB Company Full (3.0) only; legal_representative on the EEA + Company variants only; ubo only on the sole trader variants.""" UBO = 'ubo' LEGAL_REPRESENTATIVE = 'legal_representative' AUTHORISED_SIGNATORY = 'authorised_signatory' @@ -53,6 +62,8 @@ class EntityRoles(str, Enum): class CompanyPosition(str, Enum): + """The position of a representative within the company (representatives[].company_position), on + EEA, GB and US Company Full (3.0) and US ISV Seller Company (3.0).""" CEO = 'ceo' CFO = 'cfo' COO = 'coo' @@ -67,6 +78,8 @@ class CompanyPosition(str, Enum): class NationalIdType(str, Enum): + """The classification of a representative's national identification number + (individual.national_id_type), US ISV Seller variants (3.0) only.""" SSN = 'ssn' ITIN = 'itin' PASSPORT = 'passport' @@ -77,320 +90,880 @@ class NationalIdType(str, Enum): class EntityEmailAddresses: + """Email addresses for this sub-entity.""" + # The main email address for this sub-entity. + # [Required] in every variant that takes email_addresses. + # Format: email primary: str + # 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 + pci_compliance_contact: str class Invitee: + """The details of the user responsible for onboarding the sub-entity.""" + # The email of the user responsible for onboarding the sub-entity. The full onboarding variants + # describe it as the main email address for this sub-entity, but it is the invitee's address. + # [Required] in the hosted onboarding invite request; [Optional] in the Full and Lite onboarding + # variants; not part of the US ISV Seller variants. + # Format: email email: str class ContactDetails: + """Contact details of the sub-entity.""" + # 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. + # On v3.0 country_code is required and is the ISO 3166-1 alpha-2 country where the number is + # registered (for example 'FR'), not the dialling code; v2.0 takes number only. 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 phone: 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. email_addresses: EntityEmailAddresses + # The details of the user responsible for onboarding the sub-entity. + # [Required] in the hosted onboarding invite request, where it is the only contact detail; [Optional] + # in the Full and Lite onboarding variants; not part of the US ISV Seller variants. invitee: Invitee class Profile: - urls: list - mccs: list + """Information about the profile of the sub-entity, primarily regarding the products and services + offered.""" + # A collection of website URLs the sub-entity accepts payments on (str items). + # [Required] + # max 100 items; each item Format: uri, ^(http|https):\/\/\S{2,293}$, min 4 characters, max 300 + # characters + urls: list # str + # The merchant category codes that most closely describe the business (str items). + # [Required] + # min 1 item, max 5 items; each item ^[0-9]{4}$ + mccs: list # str + # The default holding currency's three-letter ISO 4217 code. + # [Required] for every v3.0 variant; [Optional] for the v2.0 Full variants; not part of the v2.0 + # Lite variants. + # Format: iso-4217. On the US ISV Seller variants (3.0), USD only. default_holding_currency: Currency - holding_currencies: list + # The currencies in which incoming funds are held (Currency items). + # [Required] for every v3.0 variant; [Optional] for the v2.0 Full variants; not part of the v2.0 + # Lite variants. + # min 1 item on v3.0. Enum per variant: + # GB (3.0): AED, AUD, CAD, CHF, CZK, DKK, EUR, GBP, HKD, JPY, KWD, NOK, NZD, PLN, RON, SEK, SGD, + # USD, ZAR + # EEA (3.0) and EEA Company Full (2.0): the GB (3.0) list without KWD + # US and US ISV Seller (3.0): USD + holding_currencies: list # Currency class EntityDocument: + """Deprecated: not defined by any Accounts API onboarding schema. Referenced only by + Company.document and EntityFinancialDocuments, both deprecated; retained so existing code keeps + working.""" + # Deprecated: see the class docstring. file_id: str + # Deprecated: see the class docstring. type: str class EntityIdentificationDocument: + """The document to use to confirm an individual's identity (identity_verification): on a + representative (Accounts API v3.0), or at the top level of the v2.0 sole trader variants.""" + # The type of document used for identity verification. + # [Required] type: DocumentType + # The ID of the front side of the document as represented within Checkout.com systems. + # [Required] + # ^file_[a-z2-7]{26}$ + # 31 characters front: str + # The ID of the back side of the document as represented within Checkout.com systems. + # [Optional] + # ^file_[a-z2-7]{26}$ + # 31 characters back: str class EntityIdentification: + """The identification of a representative on the Accounts API v2.0 US Company variants.""" + # Social Security Number (SSN), or Individual Taxpayer Identification Number (ITIN) for non-US + # citizens. + # [Required] + # ^\d{9}$ + # 9 characters national_id_number: str + # Deprecated: not defined by the Accounts API, the identification object carries + # national_id_number only. Retained so existing code keeps working; the API does not read it. document: EntityIdentificationDocument class DateOfBirth: + """The date of birth of the person according to the Gregorian calendar.""" + # The calendar day of the month they were born. + # [Required] + # min 1, max 31 day: int + # The month of the year they were born. + # [Required] + # min 1, max 12 month: int + # The year they were born. + # [Required] + # min 1900, max 2999 year: int class PlaceOfBirth: + """The place of birth of the person.""" + # The country code (iso-3166-1 alpha-2). + # [Required] + # Format: iso-3166-1-alpha-2 country: Country class CompanyVerificationType(str, Enum): + """The document types accepted as company verification. articles_of_association is accepted on + the US Company (2.0) variants only; articles of association sent as their own document use + ArticlesOfAssociationType instead.""" INCORPORATION_DOCUMENT = 'incorporation_document' ARTICLES_OF_ASSOCIATION = 'articles_of_association' class CompanyVerification: + """The document to use to confirm the company's identity (certified by a power of attorney within + the last 3 months).""" + # The type of document used for company verification. + # [Required] type: CompanyVerificationType + # The ID of the front side of the document as represented within Checkout.com systems. + # [Required] + # ^file_[a-z2-7]{26}$ + # 31 characters front: str class TaxVerificationType(str, Enum): + """The document type accepted as tax verification: an IRS-issued Employer Identification Number + letter.""" EIN_LETTER = 'ein_letter' class TaxVerification: + """IRS-issued Employer Identification Number document used to verify the entity's tax + identification (US variants).""" + # The type of IRS-issued document used for tax verification. + # [Required] type: TaxVerificationType + # The ID of the front side of the document as represented within Checkout.com systems. + # [Required] + # ^file_[a-z2-7]{26}$ + # 31 characters front: str class ArticlesOfAssociationType(str, Enum): + """The document types accepted as memorandum or articles of association.""" MEMORANDUM_OF_ASSOCIATION = "memorandum_of_association" ARTICLES_OF_ASSOCIATION = "articles_of_association" class ArticlesOfAssociation: + """Memorandum or Articles of Association document.""" + # The type of document used. + # [Required] type: ArticlesOfAssociationType + # The ID of the front side of the document as represented within Checkout.com systems. + # [Required] + # ^file_[a-z2-7]{26}$ + # 31 characters front: str class BankVerificationType(str, Enum): + """The document type accepted as bank verification.""" BANK_STATEMENT = 'bank_statement' class BankVerification: + """A document showing transactions from the last 3 months.""" + # The type of document being used as bank verification. + # [Required] type: BankVerificationType + # The ID of the front side of the document as represented within Checkout.com systems. + # [Required] + # ^file_[a-z2-7]{26}$ + # 31 characters front: str class ShareholderStructureType(str, Enum): + """The document type accepted as a certified shareholder structure.""" CERTIFIED_SHAREHOLDER_STRUCTURE = 'certified_shareholder_structure' class ShareholderStructure: + """Shareholder structure chart (including % of shares) certified by a competent authority + individual and dated within the last 3 months.""" + # The type of document. + # [Required] type: ShareholderStructureType + # The ID of the front side of the document as represented within Checkout.com systems. + # [Required] + # ^file_[a-z2-7]{26}$ + # 31 characters front: str class ProofOfLegalityType(str, Enum): + """The document type accepted as proof of legality.""" PROOF_OF_LEGALITY = 'proof_of_legality' class ProofOfLegality: + """A regulatory licence document required for the company to operate (when applicable).""" + # The type of document used for proof of legality. + # [Required] type: ProofOfLegalityType + # The ID of the front side of the document as represented within Checkout.com systems. + # [Required] + # ^file_[a-z2-7]{26}$ + # 31 characters front: str class ProofOfPrincipalAddressType(str, Enum): + """The document type accepted as proof of the company's principal place of business. Carries the + same proof_of_address value as ProofOfResidentialAddressType, but the API defines the two as + separate enums on separate documents.""" PROOF_OF_ADDRESS = 'proof_of_address' class ProofOfPrincipalAddress: + """Proof of the company's principal place of business.""" + # The type of document being used as address verification. + # [Required] type: ProofOfPrincipalAddressType + # The ID of the front side of the document as represented within Checkout.com systems. + # [Required] + # ^file_[a-z2-7]{26}$ + # 31 characters front: str class AdditionalDocument: + """Additional space for documents to be provided when requested. Carries a file ID only; the API + defines no document type for it.""" + # The ID of the front side of the document as represented within Checkout.com systems. + # [Required] + # ^file_[a-z2-7]{26}$ + # 31 characters front: str class FinancialVerificationType(str, Enum): + """The document type accepted as financial verification. Note the singular financial_statement; + FinancialStatementsType is a different enum.""" FINANCIAL_STATEMENT = 'financial_statement' class FinancialVerification: + """Financial statement document. Becomes mandatory depending on the answer provided for + annual_processing_volume; the sub-entity's status changes to requirements_due when it is + needed.""" + # The type of the file. + # [Required] type: FinancialVerificationType + # The ID of the front side of the document as represented within Checkout.com systems. + # [Required] + # ^file_[a-z2-7]{26}$ + # 31 characters front: str class FinancialStatementsType(str, Enum): + """The document type accepted as financial statements (US ISV Seller variants). Note the plural + financial_statements; FinancialVerificationType is a different enum.""" FINANCIAL_STATEMENTS = 'financial_statements' class FinancialStatements: + """Audited or management-prepared financial statements (when applicable). US ISV Seller variants + only.""" + # The type of document. + # [Required] type: FinancialStatementsType + # The ID of the front side of the document as represented within Checkout.com systems. + # [Required] + # ^file_[a-z2-7]{26}$ + # 31 characters front: str class OnboardSubEntityDocuments: + """The top-level request documents (OnboardEntityRequest.documents). The API ignores keys it does + not recognise here rather than rejecting them, so a misplaced document is dropped silently. The + representative's own documents go on EntityRepresentative.documents (RepresentativeDocuments).""" + # The document to use to confirm the individual's identity. + # [Required] for the six sole trader variants of Accounts API v2.0 (EEA, GB and US, Full and + # Lite), the only variants that take it at this level. On v3.0 it belongs on the representative. identity_verification: EntityIdentificationDocument + # The document to use to confirm the company's identity (certified by a power of attorney within + # the last 3 months). + # [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. company_verification: CompanyVerification + # Memorandum or Articles of Association document. + # [Required] for EEA and GB Company Full (3.0); [Optional] for US Company Full (3.0) and the US ISV + # Seller variants. articles_of_association: ArticlesOfAssociation + # A document showing transactions from the last 3 months. + # [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). bank_verification: BankVerification + # Shareholder structure chart (including % of shares) certified by a competent authority + # individual and dated within the last 3 months. + # [Required] for EEA and GB Company Full (3.0); [Optional] for US Company Full (3.0) and US ISV + # Seller Company (3.0). shareholder_structure: ShareholderStructure + # A regulatory licence document required for the company to operate (when applicable). + # [Optional] (EEA, GB and US Company Full (3.0) and the US ISV Seller variants) proof_of_legality: ProofOfLegality + # Proof of the company's principal place of business. + # [Optional] (EEA, GB and US Company Full (3.0) and the US ISV Seller variants) proof_of_principal_address: ProofOfPrincipalAddress + # Additional space for documents to be provided when requested. + # [Optional] (EEA, GB and US Company and Sole Trader Full (3.0); not the US ISV Seller variants) additional_document1: AdditionalDocument + # Additional space for documents to be provided when requested. + # [Optional] (EEA, GB and US Company and Sole Trader Full (3.0); not the US ISV Seller variants) additional_document2: AdditionalDocument + # Additional space for documents to be provided when requested. + # [Optional] (EEA, GB and US Company and Sole Trader Full (3.0); not the US ISV Seller variants) additional_document3: AdditionalDocument + # IRS-issued Employer Identification Number document used to verify the entity's tax + # identification. + # [Optional] (US Company variants and the US ISV Seller variants only) tax_verification: TaxVerification + # Financial statement document. Becomes mandatory depending on the answer provided for + # annual_processing_volume. + # [Optional] (EEA Company Full and Lite (2.0) only) financial_verification: FinancialVerification + # Audited or management-prepared financial statements (when applicable). + # [Optional] (US ISV Seller variants only) financial_statements: FinancialStatements class CertifiedAuthorisedSignatoryType(str, Enum): + """The document type accepted as a representative's certified authorised signatory document.""" POWER_OF_ATTORNEY = 'power_of_attorney' class CertifiedAuthorisedSignatory: + """Certified authorised signatory document. Required when the legal representative or other role + owner is not registered on the certificate of incorporation. Representative documents only, + EEA, GB and US Company Full (3.0) and US ISV Seller Company (3.0).""" + # The type of document. + # [Required] type: CertifiedAuthorisedSignatoryType + # The ID of the front side of the document as represented within Checkout.com systems. + # [Required] + # ^file_[a-z2-7]{26}$ + # 31 characters front: str class ProofOfResidentialAddressType(str, Enum): + """The document type accepted as a representative's proof of residential address (EEA Sole Trader + Full (3.0)). Carries the same proof_of_address value as ProofOfPrincipalAddressType, but the API + defines the two as separate enums on separate documents.""" PROOF_OF_ADDRESS = 'proof_of_address' class ProofOfResidentialAddress: + """Proof of residential address of the representative. Representative documents only, EEA Sole + Trader Full (3.0).""" + # The type of document being used as address verification. + # [Required] type: ProofOfResidentialAddressType + # The ID of the front side of the document as represented within Checkout.com systems. + # [Required] + # ^file_[a-z2-7]{26}$ + # 31 characters front: str class ProofOfRegistrationType(str, Enum): + """The document types accepted as a sole trader's proof of registration (EEA Sole Trader Full + (3.0)).""" EXTRACT_FROM_TRADE_REGISTER = 'extract_from_trade_register' OTHER = 'other' class ProofOfRegistration: + """Proof of the sole trader's registration, for example an extract from a trade register. + Representative documents only, EEA Sole Trader Full (3.0).""" + # The type of document being used as proof of registration. + # [Required] type: ProofOfRegistrationType + # The ID of the front side of the document as represented within Checkout.com systems. + # [Required] + # ^file_[a-z2-7]{26}$ + # 31 characters front: str -# The representative-level documents union (Accounts API v3.0) is a distinct, smaller shape than the -# entity-level OnboardSubEntityDocuments — per shared/swagger-latest.json it carries only these fields. class RepresentativeDocuments: + """Verification documents for an individual representative, sent as + company.representatives[].documents (Accounts API v3.0). + + These four are the only keys any variant defines, and which apply depends on the onboarding + variant: EEA Sole Trader Full (3.0) requires identity_verification, proof_of_residential_address + and proof_of_registration; GB and US Sole Trader Full (3.0) require identity_verification; the + EEA, GB and US Company Full (3.0) variants (person of interest) and US ISV Seller Company (3.0) + accept identity_verification and certified_authorised_signatory, both optional; US ISV Seller + Sole Trader (3.0) accepts identity_verification, optional. + + The API validates this object strictly (additionalProperties false), rejecting a key it does not + recognise rather than ignoring it, only on the EEA, GB and US Company Full (3.0) person of + interest and the EEA, GB and US Sole Trader Full (3.0) variants. It is not strict on the US ISV + Seller variants (3.0) nor on v2.0. + + The v2.0 company representatives use this class too, with identity_verification only. + + Leave an attribute unset rather than assigning None: an attribute set to None is sent as null. + """ + # The document to use to confirm the individual's identity. + # [Optional] (required for the sole trader full variants) identity_verification: EntityIdentificationDocument + # Certified authorised signatory document. Required when the legal representative or other role + # owner is not registered on the certificate of incorporation. + # [Optional] (EEA, GB and US Company Full (3.0) and US ISV Seller Company (3.0) only) certified_authorised_signatory: CertifiedAuthorisedSignatory + # Proof of residential address of the representative. + # [Optional] (required for EEA Sole Trader Full (3.0), and only valid there) proof_of_residential_address: ProofOfResidentialAddress + # Proof of the sole trader's registration, for example an extract from a trade register. + # [Optional] (required for EEA Sole Trader Full (3.0), and only valid there) proof_of_registration: ProofOfRegistration class Citizenship: + """A citizenship or legal-status record (US ISV Seller variants).""" + # The type of citizenship or legal status (for example citizenship or residency). + # [Optional] type: str + # The two-letter ISO 3166-1 alpha-2 country code. + # [Required] + # Format: iso-3166-1-alpha-2 country: Country class RepresentativeIndividual: + """The personal details of a company representative (company.representatives[].individual), + Accounts API v3.0.""" + # The representative's first name. + # [Required] + # min 2 characters, max 50 characters first_name: str + # The representative's middle name. Required if it appears in official documents. + # [Optional] + # min 2 characters, max 50 characters middle_name: str + # The representative's last name. + # [Required] + # min 2 characters, max 50 characters last_name: str + # The date of birth of the person according to the Gregorian calendar. + # [Required] date_of_birth: DateOfBirth + # The place of birth of the person. + # [Required] place_of_birth: PlaceOfBirth + # The list of citizenships or legal statuses for the representative (Citizenship items). + # [Required] for the US ISV Seller variants only; not part of the other v3.0 schemas, leave unset + # for them. citizenships: list # Citizenship + # 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. national_id_type: NationalIdType + # The representative's national identification number. + # [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 national_id_type 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. national_id_number: str + # The representative's personal email address. + # [Required] for the US ISV Seller variants; [Optional] for the other v3.0 variants. + # Format: email email_address: str + # The representative's phone number. + # [Required] for the US ISV Seller variants; [Optional] for the other v3.0 variants. phone: Phone + # The representative's address. + # [Required] address: Address class EntityRepresentative: + """A representative of the sub-entity. One class covers every shape the Accounts API defines: + the v3.0 person of interest (individual, roles, company_position, ownership_percentage, + documents), the v3.0 controlling company of EEA and GB Company Full (company, + ownership_percentage), and the v2.0 company representative (the flat person fields, roles, + documents and, on the US variants, identification).""" # v3.0 (Accounts API v3.0) + # The representative's id. + # [Optional] + # ^rep_[a-z0-9]{26}$ + # 30 characters id: str + # Information about the individual representing the sub-entity. + # [Required] for every v3.0 person of interest. individual: RepresentativeIndividual + # The individual's roles within the company (EntityRoles items). For sole traders, must be ubo + # only. + # [Required] for every variant except EEA and US Company Lite (2.0), where it is [Optional]. roles: list # accounts.EntityRoles + # The position of the representative within the company (required for the control_person role). + # [Optional] (EEA, GB and US Company Full (3.0) and US ISV Seller Company (3.0)) company_position: 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 ownership_percentage: int + # Verification documents for the individual representative. See RepresentativeDocuments: on the + # EEA, GB and US Company Full (3.0) person of interest and the EEA, GB and US Sole Trader Full + # (3.0) variants the API validates this object strictly and rejects any key the variant does not + # define; on the US ISV Seller variants (3.0) and v2.0 it is not strict. + # [Required] for the EEA, GB and US Sole Trader Full (3.0) variants and EEA Company Full (2.0); + # [Optional] otherwise. documents: RepresentativeDocuments - # v2.0 only — deprecated; use `individual` for v3.0 + # 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 attributes here, all [Required]: legal_name, trading_name and + # registered_address. Leave the other Company attributes unset. + company: 'Company' + # v2.0 only, deprecated; use `individual` for v3.0 + # The representative's first name. + # [Required] (v2.0) + # min 2 characters, max 50 characters first_name: str + # The representative's middle name. Required if it appears in official documents. + # [Optional] + # min 2 characters, max 50 characters middle_name: str + # The representative's last name. + # [Required] (v2.0) + # min 2 characters, max 50 characters last_name: str + # The representative's address. + # [Required] (v2.0) address: Address + # The representative's identification. US Company (2.0) only. + # [Required] for US Company Full (2.0); [Optional] for US Company Lite (2.0). identification: EntityIdentification + # The representative's phone number. + # [Optional] phone: Phone + # The date of birth of the person according to the Gregorian calendar. + # [Required] for the v2.0 Full variants; [Optional] for the v2.0 Lite variants. date_of_birth: DateOfBirth + # The place of birth of the person. + # [Required] for EEA Company Full (2.0); [Optional] for EEA Company Lite (2.0). Not part of the + # other v2.0 variants. place_of_birth: PlaceOfBirth class EntityFinancialDocuments: + """Deprecated: not defined by any Accounts API schema. financial_details carries the three amounts + and the currency only. Retained so existing code keeps working.""" + # Deprecated: see the class docstring. bank_statement: EntityDocument + # Deprecated: see the class docstring. financial_statement: EntityDocument class EntityFinancialDetails: + """Seller financial questions (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).""" + # 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 annual_processing_volume: int + # 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 average_transaction_value: int + # 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 highest_transaction_value: int + # Deprecated: not defined by any Accounts API schema; the API does not read it. Supporting + # documents go on the top-level request documents (OnboardSubEntityDocuments) instead. documents: EntityFinancialDocuments + # 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. currency: Currency class DateOfIncorporation: + """The date the company was incorporated, or the date the sole trader started trading.""" + # The day of the month the company was incorporated. + # [Optional] + # min 1, max 31 day: int + # The month the company was incorporated. + # [Required] + # min 1, max 12 month: int + # The year the company was incorporated. + # [Required] + # min 1500, max 2999 year: int class Company: + """Information about the company represented by the sub-entity: on every company and v3.0 sole + trader variant, and as the controlling company of an EntityRepresentative (where only + legal_name, trading_name and registered_address apply).""" + # 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, 8 characters, matching one of the three alternatives of + # the spec's pattern, ^(A|B|C)$: + # A: ((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}) + # B: ((IP|SP|RS)[A-Z\d]{6}) + # C: (SL\d{5}[\dA]) + # 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. business_registration_number: str + # The legal type of the company. Must be 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). business_type: BusinessType + # 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 legal_name: str + # The trading name of the sub-entity, also referred to as 'doing business as'. + # [Required] + # min 2 characters, max 300 characters trading_name: str + # The collection of additional trading names for the sub-entity. + # [Optional] (US ISV Seller variants only) additional_trading_names: list # str + # Indicates whether the sub-entity is a registered legal entity. Must be False for US ISV Seller + # Sole Trader (3.0). + # [Required] for US ISV Seller Sole Trader (3.0); not part of the other variants. is_registered_company: bool + # 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). date_of_incorporation: 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 regulatory_licence_number: str + # The primary location where business is performed. + # [Required] for every company and v3.0 sole trader variant. principal_address: Address + # The registered address of the company. + # [Required] for every company variant and the controlling company; not part of the sole trader + # variants. registered_address: Address + # Information about the representatives of this company (EntityRepresentative items). + # [Required] + # min 1 item; max 1 item for the sole trader variants (the individual themselves, with roles + # [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) representatives: list # EntityRepresentative + # Deprecated: not defined by any Accounts API company schema. Retained so existing code keeps + # working; the API does not read it. document: EntityDocument + # 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. financial_details: EntityFinancialDetails class Identification: + """The identification of the individual on the Accounts API v2.0 US Sole Trader variants.""" + # Social Security Number (SSN), or Individual Taxpayer Identification Number (ITIN) for non-US + # citizens. + # [Required] + # ^\d{9}$ + # 9 characters national_id_number: str + # Deprecated: not defined by the Accounts API, the identification object carries + # national_id_number only. Retained so existing code keeps working; the API does not read it. document: EntityIdentificationDocument class Individual: + """The top-level individual of the Accounts API v2.0 sole trader variants.""" + # The individual's first name. + # [Required] + # min 2 characters, max 50 characters first_name: str + # The individual's middle name. Required if it appears in official documents. + # [Optional] + # min 2 characters, max 50 characters middle_name: str + # The individual's last name. + # [Required] + # min 2 characters, max 50 characters last_name: str + # The trading name of the sub-entity, also referred to as 'doing business as'. + # [Required] + # min 2 characters, max 300 characters trading_name: str + # Deprecated: not defined by any Accounts API schema. Retained so existing code keeps working; the + # API does not read it. national_tax_id: str + # The registered address of the sole trader's business. + # [Required] registered_address: Address + # 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]. date_of_birth: 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. place_of_birth: 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). 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). financial_details: EntityFinancialDetails class ProcessingDetailsAch: + """ACH payment processing details (processing_details.payments.ach), US ISV Seller variants (3.0) + only.""" + # The estimated annual ACH processing volume in minor units without decimals. + # [Required] + # min 0 annual_ach_volume: int + # The expected average ACH transaction size in minor units without decimals. + # [Required] + # min 0 average_ach_transaction_size: int + # The estimated monthly volume of ACH credit transactions (for example, refunds issued to + # customers) in minor units without decimals. + # [Required] + # min 0 estimated_monthly_credit_volume: int + # The average value of an ACH credit transaction (for example, a refund) in minor units without + # decimals. + # [Required] + # min 0 average_credit_amount: int class ProcessingDetailsPayments: + """Payment method-specific processing details (processing_details.payments), US ISV Seller + variants (3.0) only.""" + # ACH payment processing details. + # [Required] ach: ProcessingDetailsAch class ProcessingDetails: + """Information about the sub-entity's expected processing (processing_details). Part of every + Accounts API v3.0 variant; not part of v2.0.""" + # The country code (iso-3166-1 alpha-2) where the settlement bank account is located. + # [Required] for EEA, GB and US Company and Sole Trader Full (3.0); not part of the US ISV Seller + # variants. + # Format: iso-3166-1-alpha-2 + # [a-zA-Z]{2} + # 2 characters settlement_country: str + # Target country codes (iso-3166-1 alpha-2) with more than 10% expected volume processing with + # Checkout.com (str items). + # [Required] + # min 1 item, max 10 items; each item Format: iso-3166-1-alpha-2, [a-zA-Z]{2}, 2 characters target_countries: list # str + # The estimated annual processing volume. In minor units without decimals. + # [Required] + # min 0 annual_processing_volume: int + # The expected average transaction value. In minor units without decimals. + # [Required] + # min 0 average_transaction_value: int + # The average time in days between accepting payment and fulfilling the order. + # [Required] for the US ISV Seller variants (3.0); not part of the other variants. + # min 0 average_order_fulfillment_time: int + # The expected highest transaction value. In minor units without decimals. + # [Required] for EEA, GB and US Company and Sole Trader Full (3.0); not part of the US ISV Seller + # variants. + # min 0 highest_transaction_value: int + # The currency used for the processing details provided. + # [Required] + # Enum per variant: GBP on the GB variants, EUR on the EEA variants, USD on the US and US ISV + # Seller variants. currency: Currency + # Payment method-specific processing details. + # [Required] for the US ISV Seller variants (3.0); not part of the other variants. payments: ProcessingDetailsPayments class AdditionalInfo: + """Deprecated: not defined by any Accounts API onboarding schema. Referenced only by + OnboardEntityRequest.additional_info; retained so existing code keeps working.""" + # Deprecated: see the class docstring. field1: str + # Deprecated: see the class docstring. field2: str + # Deprecated: see the class docstring. field3: str class AgreedTerms: + """Details of the person (or sole trader) who agreed to the terms and conditions on behalf of the + sub-entity, captured as evidence of consent to Checkout.com onboarding (agreed_terms). US ISV + Seller variants (3.0) only.""" + # Date and time the terms were agreed in RFC 3339 or ISO 8601 format. + # [Required] + # Format: date-time date: str + # IP address (IPv4 or IPv6) of the person at the time they agreed the terms. + # [Required] ip_address: str + # First and last name of the person who agreed to the terms. + # [Required] name: str + # Email address of the person who agreed to the terms. + # [Required] + # Format: email email: str + # Identifier of the terms version that was agreed. + # [Required] version: str class SchemaVersionHeader: + """The Accept header that selects the Accounts API payload version, for example + 'application/json;schema_version=3.0'. Built by AccountsClient from its schema_version argument.""" + # The Accept header value: application/json with a schema_version parameter. accept: str def get_header_mappings(self) -> Dict[str, str]: @@ -400,119 +973,290 @@ def get_header_mappings(self) -> Dict[str, str]: class OnboardEntityRequest: + """The request body of POST /accounts/entities (onboard a sub-entity) and PUT + /accounts/entities/{id} (update a sub-entity). One class covers every variant the API defines: + the Accounts API v3.0 and v2.0 company and sole trader variants (EEA, GB and US, Full and Lite), + the US ISV Seller variants (3.0), and the hosted onboarding invite request, which takes only + reference, is_draft and contact_details (with invitee). Select the version with the + schema_version argument of the client method. Leave unset the attributes the chosen variant does + not define.""" + # A unique reference you can later use to identify the sub-entity. Immutable after creation. + # [Required] + # min 1 character, max 50 characters reference: str + # Specifies whether the sub-entity details are in draft. Marking a sub-entity as a draft allows + # its details to be updated without triggering due diligence checks. On the US ISV Seller + # variants, POST always creates the sub-entity in Draft regardless of this field. + # [Required] in the hosted onboarding invite request; [Optional] in the other variants. is_draft: bool + # Information about the profile of the sub-entity, primarily regarding the products and services + # offered. + # [Required] for every variant except the hosted onboarding invite request, which does not take + # it. profile: Profile + # Contact details of this sub-entity. + # [Required] for every variant except EEA Company Full (3.0), where it is [Optional]. In the + # hosted onboarding invite request it carries invitee only. contact_details: ContactDetails + # Information about the company represented by the sub-entity, or about the sole trader's + # business on the v3.0 sole trader variants. + # [Required] for every company variant and every v3.0 sole trader variant, US ISV Seller + # included; not part of the v2.0 sole trader variants (they use individual) nor the hosted + # onboarding invite request. company: Company + # Information about the sub-entity's expected processing. + # [Required] for every v3.0 variant; not part of v2.0 nor the hosted onboarding invite request. processing_details: ProcessingDetails + # Details of the person who agreed to the terms and conditions on behalf of the sub-entity. + # [Required] for the US ISV Seller variants (3.0); not part of the other variants. agreed_terms: AgreedTerms + # The identifier of a seller category set up for your platform. Seller categories define the + # pricing, capabilities and risk profile applied to sub-entities, and are configured during your + # platform's onboarding with Checkout.com; contact your account manager for the list of available + # identifiers. + # [Required] for the US ISV Seller variants (3.0); not part of the other variants. seller_category: str + # The documents used to support the verification of the company or business details. + # [Required] for EEA, GB and US Company Full (3.0), EEA, GB and US Sole Trader Full (3.0), EEA + # Company Full (2.0) and EEA Sole Trader Full (2.0); [Optional] for the other variants, US ISV + # Seller included. Not part of the hosted onboarding invite request. documents: OnboardSubEntityDocuments + # Deprecated: not defined by any Accounts API onboarding schema. Retained so existing code keeps + # working; the API does not document reading it. additional_info: AdditionalInfo - # v2.0 only — deprecated; a v3.0 sole trader is onboarded as a `company` with representatives + # v2.0 only, deprecated; a v3.0 sole trader is onboarded as a `company` with representatives. + # Information about the individual represented by the sub-entity. + # [Required] for the six v2.0 sole trader variants (EEA, GB and US, Full and Lite); not part of + # the other variants. individual: Individual class InstrumentDocument: + """A legal document used to verify the bank account (document): on a bank_account + PaymentInstrumentRequest, and on the deprecated AccountsPaymentInstrument.""" + # The document type. Enum: bank_statement. + # [Optional] (defaults to bank_statement) type: str + # The file ID of the uploaded document. The document must have been uploaded for the purpose of + # bank_verification. + # [Optional] file_id: str class InstrumentDetails: - pass + """Details of the payment instrument being created (instrument_details). Base class: use + InstrumentDetailsFasterPayments, InstrumentDetailsSepa or InstrumentDetailsAch for a bank_account + instrument, and InstrumentDetailsCardToken for a card_token instrument.""" class InstrumentDetailsFasterPayments(InstrumentDetails): + """Faster Payments bank account details of a bank_account payment instrument.""" + # The alphanumeric value that identifies the account. + # [Required] account_number: str + # The code that identifies the bank. + # [Required] bank_code: str class InstrumentDetailsSepa(InstrumentDetails): + """SEPA bank account details of a bank_account payment instrument.""" + # The account's International Bank Account Number (IBAN). + # [Required] + # min 5 characters, max 34 characters iban: str + # An 8 or 11 character code that identifies the bank or bank branch. + # [Required] + # Format: ISO 9362:2009 swift_bic: str class InstrumentDetailsCardToken(InstrumentDetails): + """Card details of a card_token payment instrument.""" + # The token that identifies the card. + # [Required] token: str class InstrumentAccountType(str, Enum): + """The type of bank account of an ACH payment instrument (instrument_details.account_type).""" SAVINGS = 'savings' CHECKING = 'checking' class InstrumentDetailsAch(InstrumentDetails): + """ACH bank account details of a bank_account payment instrument.""" + # The alphanumeric value that identifies the account. + # [Required] account_number: str + # The 9-digit American Bankers Association (ABA) routing number that identifies the financial + # institution. + # [Required] + # ^[0-9]{9}$ routing_number: str + # The type of bank account. + # [Required] account_type: InstrumentAccountType class BankDetails: + """Deprecated: part of the deprecated AccountsPaymentInstrument (AccountsPaymentInstrument.bank) + only; retained so existing code keeps working. Not the shared common.common.BankDetails.""" + # Deprecated: see the class docstring. name: str + # Deprecated: see the class docstring. branch: str + # Deprecated: see the class docstring. address: Address class AccountsAccountHolder: + """Deprecated: the account holder of the deprecated AccountsPaymentInstrument + (AccountsPaymentInstrument.account_holder) only; retained so existing code keeps working. Use + AccountsCorporateAccountHolder or AccountsIndividualAccountHolder.""" + # Deprecated: see the class docstring. type: AccountHolderType + # Deprecated: see the class docstring. tax_id: str + # Deprecated: see the class docstring. date_of_birth: DateOfBirth + # Deprecated: see the class docstring. country_of_birth: Country + # Deprecated: see the class docstring. residential_status: ResidentialStatusType + # Deprecated: see the class docstring. billing_address: Address + # Deprecated: see the class docstring. phone: Phone + # Deprecated: see the class docstring. identification: AccountHolderIdentification + # Deprecated: see the class docstring. email: str class AccountsCorporateAccountHolder(AccountsAccountHolder): + """Deprecated: a corporate account holder of the deprecated AccountsPaymentInstrument; see + AccountsAccountHolder.""" + # Deprecated: see the class docstring. company_name: str class AccountsIndividualAccountHolder(AccountsAccountHolder): + """Deprecated: an individual account holder of the deprecated AccountsPaymentInstrument; see + AccountsAccountHolder.""" + # Deprecated: see the class docstring. first_name: str + # Deprecated: see the class docstring. last_name: str class AccountsPaymentInstrument: + """Deprecated: the request body of AccountsClient.create_payment_instrument (POST + /accounts/entities/{id}/instruments), itself deprecated in favour of add_payment_instrument. The + API reference does not describe this endpoint. Use PaymentInstrumentRequest with + add_payment_instrument instead; retained so existing code keeps working.""" + # Deprecated: see the class docstring. Always bank_account. type = InstrumentType.BANK_ACCOUNT + # Deprecated: see the class docstring. label: str + # Deprecated: see the class docstring. account_type: AccountType + # Deprecated: see the class docstring. account_number: str + # Deprecated: see the class docstring. bank_code: str + # Deprecated: see the class docstring. branch_code: str + # Deprecated: see the class docstring. iban: str + # Deprecated: see the class docstring. bban: str + # Deprecated: see the class docstring. swift_bic: str + # Deprecated: see the class docstring. currency: Currency + # Deprecated: see the class docstring. country: Country + # Deprecated: see the class docstring. document: InstrumentDocument + # Deprecated: see the class docstring. account_holder: AccountsAccountHolder + # Deprecated: see the class docstring. bank: BankDetails class PaymentInstrumentRequest: + """The request body of POST /accounts/entities/{id}/payment-instruments (add a payment + instrument), PlatformsPaymentInstrumentCreate. Two variants, selected by type: bank_account and + card_token.""" + # A reference that you can use to identify the payment instrument. + # [Required] + # min 1 character, max 50 characters label: str + # The instrument type. Enum: bank_account, card_token. + # [Required] type: InstrumentType + # The account's currency, as a three-letter ISO 4217 currency code. + # [Required] + # Format: ISO 4217 + # 3 characters currency: Currency + # The account's country, as a two-letter ISO country code. + # [Required] for bank_account; not part of card_token. + # Format: ISO 3166-1 country: Country + # Deprecated: specifies whether the payment instrument should be set as the default payout + # destination. For ad-hoc payouts, the payment instrument is explicitly specified in the payout + # request; for scheduled payouts, the first payment instrument created for a given currency is + # used for that currency's payout schedule. To change it, update the payout schedule. + # [Optional] (bank_account only) default: bool + # A legal document used to verify the bank account. + # [Required] for bank_account; not part of card_token. document: InstrumentDocument + # Details of the payment instrument being created: InstrumentDetailsFasterPayments, + # InstrumentDetailsSepa or InstrumentDetailsAch for bank_account; InstrumentDetailsCardToken for + # card_token. + # [Required] instrument_details: InstrumentDetails class Headers: + """The headers object of PlatformsPaymentInstrumentUpdate (UpdatePaymentInstrumentRequest.headers). + The API reference models it inside the request body, with the key if-match, but the API reads the + ETag only from the If-Match HTTP header. AccountsClient.update_payment_instrument sends it as that + header.""" + # The payment instrument ETag value, as returned in the ETag header of the GET. Sent as the If-Match + # HTTP header; the update fails with 428 when it is missing and 412 when it does not match. + # [Required] if_match: str class UpdatePaymentInstrumentRequest: + """The request body of PATCH /accounts/entities/{entityId}/payment-instruments/{id} (update a + payment instrument), PlatformsPaymentInstrumentUpdate.""" + # A reference that you can use to identify the payment instrument. + # [Optional] + # min 1 character, max 50 characters label: str + # Deprecated: specifies whether the payment instrument should be set as the default payout + # destination. For scheduled payouts, the first payment instrument created for a given currency + # is used for that currency's payout schedule; to change it, update the payout schedule. + # [Optional] default: bool + # The payment instrument ETag, sent as the If-Match HTTP header. + # [Required] by the API: the update fails with 428 Precondition Required without it. headers: Headers class ScheduleRequest: + """Information about how often the payout schedule takes place (recurrence). Base class, selected + by frequency: use ScheduleFrequencyDailyRequest, ScheduleFrequencyWeeklyRequest or + ScheduleFrequencyMonthlyRequest.""" + # Used to indicate how often funds should be paid out to a sub-entity. Enum: daily, weekly, + # monthly. For ISV (SaaS seller) sub-entities, the payout is based on the sub-entity's available + # balance as of 00:00 in the sub-entity's time zone. + # [Required] frequency: ScheduleFrequency def __init__(self, frequency_p: ScheduleFrequency): @@ -520,15 +1264,19 @@ def __init__(self, frequency_p: ScheduleFrequency): class ScheduleFrequencyDailyRequest(ScheduleRequest): - # For ISV (SaaS seller) sub-entities, a daily schedule runs on working days only - # (Monday to Friday); payouts do not take place on weekends. + """A daily payout schedule (frequency daily). For ISV (SaaS seller) sub-entities, a daily schedule + runs on working days only (Monday to Friday); payouts do not take place on weekends.""" def __init__(self): super().__init__(ScheduleFrequency.DAILY) class ScheduleFrequencyMonthlyRequest(ScheduleRequest): + """A monthly payout schedule (frequency monthly).""" + # The day or days of the month the payout should take place (int items). + # [Required] + # each item min 1, max 28. # For ISV (SaaS seller) sub-entities, by_month_day accepts only the combinations - # [1], [15], [1, 15] or [1, 16], in any order. + # [1], [15], [1, 15] or [1, 16], in any order (min 1 item, max 2 items). by_month_day: list # int def __init__(self): @@ -536,6 +1284,10 @@ def __init__(self): class ScheduleFrequencyWeeklyRequest(ScheduleRequest): + """A weekly payout schedule (frequency weekly).""" + # The day or days of the week the payout should take place (DaySchedule items). + # [Required] + # Enum: monday, tuesday, wednesday, thursday, friday, saturday, sunday. # For ISV (SaaS seller) sub-entities, by_day accepts working days only # (Monday to Friday); payouts set to take place on weekends are rejected. by_day: list # DaySchedule @@ -545,50 +1297,100 @@ def __init__(self): class UpdateScheduleRequest: + """The payout schedule for one currency, in PUT /accounts/entities/{id}/payout-schedules. The + client sends it keyed by the currency's three-letter ISO 4217 code. One class covers both + variants the API defines: Standard and SaaS seller (ISV).""" + # Indicates whether the payout schedule is enabled. + # [Required] for ISV (SaaS seller) sub-entities; [Optional] otherwise. enabled: bool + # The minimum available balance required for a payout to take place; below it, Checkout.com does + # not send the payout instruction. For ISV (SaaS seller) sub-entities, in the minor units of the + # schedule's currency, and defaults to 0 if you do not set it. + # [Optional] threshold: int # The amount, in the minor units of the schedule's currency, to retain in the - # sub-entity's available balance. ISV (SaaS seller) sub-entities only. Min 0. + # sub-entity's available balance. ISV (SaaS seller) sub-entities only. Checkout.com pays out only + # the funds above it, and generates no payout otherwise. Defaults to 0 if you do not set it. + # [Optional] (ISV (SaaS seller) sub-entities only) + # min 0 balance_minimum: int # Indicates whether to carry forward any balance below the configured minimum # to the next payout. ISV (SaaS seller) sub-entities only. + # Defaults to False if you do not set it. + # [Optional] (ISV (SaaS seller) sub-entities only) carry_forward_enabled: bool # The ID of the platforms payment instrument used as the payout destination. + # For ISV (SaaS seller) sub-entities, if included it must reference a verified payment + # instrument, otherwise the request fails. + # [Optional] payment_instrument_id: str + # Information about how often the schedule takes place. + # [Required] for ISV (SaaS seller) sub-entities; [Optional] otherwise. recurrence: ScheduleRequest class PaymentInstrumentsQuery: + """The query parameters of GET /accounts/entities/{id}/payment-instruments.""" + # The status of the sub-entity's payment instrument: its stage of verification, and whether it + # can be used for payouts. Enum: pending, verified, unverified. + # [Optional] status: str class ReserveRuleType(str, Enum): + """The type of a reserve rule (ReserveRuleRequest.type).""" ROLLING = 'rolling' class HoldingDuration: + """The length of time the collateral balance will be reserved for.""" + # The number of weeks the collateral balance is reserved for. + # [Required] + # min 2, max 104 weeks: int class RollingReserveRule: + """The rolling reserve rule details (rolling).""" + # The percentage of captured funds that will be reserved as a collateral balance. + # [Required] + # min 0, max 100 percentage: float + # The length of time the collateral balance will be reserved for. + # [Required] holding_duration: HoldingDuration class ReserveRuleRequest: + """The request body of POST /accounts/entities/{id}/reserve-rules (ReserveRuleCreateRequest) and + PUT /accounts/entities/{entityId}/reserve-rules/{id} (ReserveRuleUpdateRequest, sent with the + If-Match header from the etag argument).""" + # The reserve rule type. Enum: rolling. + # [Required] type: ReserveRuleType + # The rolling reserve rule details. + # [Required] rolling: RollingReserveRule + # The date and time the reserve rule will come into effect. Must be at least 15 minutes in the + # future. + # [Required] on create; not part of the update request. + # Format: date-time valid_from: str class FilePurpose(str, Enum): + """The purpose of a sub-entity file upload (POST /entities/{entity_id}/files). The fourteen values + the endpoint accepts (PlatformsFileUpload), plus two that it does not, noted below.""" ADDITIONAL_DOCUMENT = 'additional_document' ARTICLES_OF_ASSOCIATION = 'articles_of_association' BANK_VERIFICATION = 'bank_verification' CERTIFIED_AUTHORISED_SIGNATORY = 'certified_authorised_signatory' COMPANY_OWNERSHIP = 'company_ownership' + # Not an onboarding upload purpose: not among the values PlatformsFileUpload defines. Use + # IDENTITY_VERIFICATION. IDENTIFICATION = 'identification' IDENTITY_VERIFICATION = 'identity_verification' + # Not an onboarding upload purpose: POST /entities/{entity_id}/files does not accept it. DISPUTE_EVIDENCE = 'dispute_evidence' COMPANY_VERIFICATION = 'company_verification' FINANCIAL_VERIFICATION = 'financial_verification' @@ -601,14 +1403,29 @@ class FilePurpose(str, Enum): class EntityFileRequest: + """The request body of POST /entities/{entity_id}/files (PlatformsFileUpload).""" + # The purpose of the file upload: the onboarding document the file is for. + # [Required] purpose: FilePurpose class EntityRequirementUpdateRequest: + """The request body of PUT /accounts/entities/{id}/requirements/{requirementId} (resolve a + requirement). The shape of value is defined by the requirement's _schema, returned from GET + /accounts/entities/{id}/requirements/{requirementId}.""" + # The response to the requirement. The expected shape depends on the requirement and is defined + # by the JSON Schema returned in the requirement details response. Common shapes include a file + # reference (for document uploads), a primitive value, or a structured object. One of: object, + # array, string, number, boolean. + # [Required] value: object class EtagHeader: + """The If-Match header of PUT /accounts/entities/{entityId}/reserve-rules/{id}. Built by + AccountsClient.update_reserve_rule from its etag argument.""" + # Identifies a specific version of a reserve rule to update. + # [Required] etag: str def get_header_mappings(self) -> Dict[str, str]: diff --git a/checkout_sdk/accounts/accounts_client.py b/checkout_sdk/accounts/accounts_client.py index 24b8ad2d..9bc58dfc 100644 --- a/checkout_sdk/accounts/accounts_client.py +++ b/checkout_sdk/accounts/accounts_client.py @@ -43,6 +43,20 @@ def __build_schema_version_headers(schema_version: str): return headers def upload_file(self, file_request: FileRequest): + """Upload a file to the Files API (POST /files on the Files host), as a multipart request. + + The Files host POST /files is not described in the API reference: the reference's POST /files + is the disputes upload on the API host, whose purpose mentions only dispute_evidence and + arbitration_evidence. For onboarding documents, set the purpose to one of the PlatformsFileUpload + purposes the reference lists for the sub-entity upload, POST /entities/{entity_id}/files + (FilePurpose in checkout_sdk.accounts.accounts). + + Args: + file_request: The path to the file and its purpose. + + Returns: + ResponseWrapper with the file ID, which document front and back attributes take. + """ return self.__files_client.submit_file( self.__FILES_PATH, self._sdk_authorization(), @@ -97,6 +111,8 @@ def update_payment_instrument(self, entity_id: str, instrument_id: str, update_payment_instrument_request: UpdatePaymentInstrumentRequest): + # The API reads the ETag only from the If-Match HTTP header; without it the update fails with + # 428 Precondition Required. So the request's headers are sent as HTTP headers. return self._api_client.patch( self.build_path(self.__ACCOUNTS_PATH, self.__ENTITIES_PATH, @@ -104,7 +120,8 @@ def update_payment_instrument(self, self.__PAYMENT_INSTRUMENTS_PATH, instrument_id), self._sdk_authorization(), - update_payment_instrument_request + update_payment_instrument_request, + headers=getattr(update_payment_instrument_request, 'headers', None) ) def query_payment_instruments(self, entity_id: str, query: PaymentInstrumentsQuery = None): @@ -182,12 +199,35 @@ def resolve_entity_requirement(self, entity_id: str, requirement_id: str, self._sdk_authorization(), request) def upload_entity_file(self, entity_id: str, entity_file_request: EntityFileRequest): + """Create a file upload for a sub-entity (POST /entities/{entity_id}/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. + + Args: + entity_id: The ID of the sub-entity. + entity_file_request: The purpose of the file upload. + + Returns: + ResponseWrapper with the file ID, the maximum size allowed, the MIME types allowed for the + purpose, and the upload link. + """ return self.__files_client.post( self.build_path(self.__ENTITIES_PATH, entity_id, self.__FILES_PATH), self._sdk_authorization(), entity_file_request) def retrieve_entity_file(self, entity_id: str, file_id: str): + """Retrieve the details of a sub-entity's file (GET /entities/{entity_id}/files/{file_id} on the + Files host). + + Args: + entity_id: The ID of the sub-entity. + file_id: The ID of the file. + + Returns: + ResponseWrapper with the file's status, size, MIME type, upload date and purpose. + """ return self.__files_client.get( self.build_path(self.__ENTITIES_PATH, entity_id, self.__FILES_PATH, file_id), self._sdk_authorization()) diff --git a/checkout_sdk/common/enums.py b/checkout_sdk/common/enums.py index d27cb910..641b2ac1 100644 --- a/checkout_sdk/common/enums.py +++ b/checkout_sdk/common/enums.py @@ -512,7 +512,8 @@ class InstrumentType(str, Enum): SEPA = 'sepa' ACH = 'ach' BACS = 'bacs' - # Previous API (ABC) only - the current API's instrument type does not declare this value. + # A card token payment instrument for a sub-entity (PlatformsPaymentInstrument.type, the Accounts + # payment instruments). CARD_TOKEN = 'card_token' @@ -550,7 +551,7 @@ class AchInstrumentAccountType(str, Enum): # SEPA mandate type. Used by both RequestSepaV4Source.mandate_type and -# StoreSepaInstrumentRequest.instrument_data.type — same enum, two callsites. +# StoreSepaInstrumentRequest.instrument_data.type: same enum, two callsites. class SepaMandateType(str, Enum): CORE = 'Core' B2B = 'B2B' diff --git a/checkout_sdk/files/files.py b/checkout_sdk/files/files.py index a93bd8e3..26283228 100644 --- a/checkout_sdk/files/files.py +++ b/checkout_sdk/files/files.py @@ -1,3 +1,16 @@ class FileRequest: + """A file to upload to the Files API (POST /files), sent as a multipart request. The returned ID + is what document front and back attributes take.""" + # The path to the file to upload (JPEG, PNG or PDF). + # [Required] file: str + # The purpose of the file upload. For onboarding documents, one of the FilePurpose values from + # checkout_sdk.accounts.accounts (for example 'identity_verification'); for disputes, + # 'dispute_evidence'. + # AccountsClient.upload_file sends this request to POST /files on the Files host, which the API + # reference does not describe. The reference's POST /files is the disputes upload on the API host + # (DisputesClient.upload_file), whose purpose mentions only dispute_evidence and + # arbitration_evidence. The onboarding values are the PlatformsFileUpload purposes the reference + # lists for the sub-entity upload, POST /entities/{entity_id}/files (FilePurpose). + # [Required] purpose: str diff --git a/checkout_sdk/issuing/controls.py b/checkout_sdk/issuing/controls.py index 588be6f5..9f0a2e24 100644 --- a/checkout_sdk/issuing/controls.py +++ b/checkout_sdk/issuing/controls.py @@ -75,7 +75,7 @@ def __init__(self): # Parallel hierarchy for controls declared INLINE on VirtualCardRequest.controls. # The standalone POST /issuing/controls endpoint requires target_id (a separate -# card to attach the control to). The inline variant does NOT — the card being +# card to attach the control to). The inline variant does NOT: the card being # created is the implicit target. Reusing CardControlRequest here would let # callers set target_id on the wire, which the API ignores or rejects. These # classes prevent that misuse at the type level. diff --git a/tests/accounts/accounts_client_test.py b/tests/accounts/accounts_client_test.py index 57492792..df0b7f54 100644 --- a/tests/accounts/accounts_client_test.py +++ b/tests/accounts/accounts_client_test.py @@ -2,9 +2,10 @@ from tests._assertions import assert_api_call from checkout_sdk.accounts.accounts import OnboardEntityRequest, AccountsPaymentInstrument, UpdateScheduleRequest, \ - PaymentInstrumentRequest, PaymentInstrumentsQuery, UpdatePaymentInstrumentRequest, ReserveRuleRequest, \ + PaymentInstrumentRequest, PaymentInstrumentsQuery, UpdatePaymentInstrumentRequest, ReserveRuleRequest, Headers, \ EntityFileRequest, FilePurpose, EntityRequirementUpdateRequest from checkout_sdk.accounts.accounts_client import AccountsClient +from checkout_sdk.api_client import ApiClient from checkout_sdk.common.enums import Currency from checkout_sdk.files.files import FileRequest @@ -58,6 +59,19 @@ def test_should_update_payment_instrument(self, mocker, client: AccountsClient): assert client.update_payment_instrument('entity_id', 'instrument_id', body) == 'response' assert_api_call(mock, 'accounts/entities/entity_id/payment-instruments/instrument_id', body) + assert mock.call_args.kwargs['headers'] is None + + def test_should_send_update_payment_instrument_etag_as_if_match_header(self, mocker, client: AccountsClient): + mock = mocker.patch('checkout_sdk.api_client.ApiClient.patch', return_value='response') + body = UpdatePaymentInstrumentRequest() + body.headers = Headers() + body.headers.if_match = '"Y3Y9MCZydj0w"' + + assert client.update_payment_instrument('entity_id', 'instrument_id', body) == 'response' + assert_api_call(mock, 'accounts/entities/entity_id/payment-instruments/instrument_id', body) + assert mock.call_args.kwargs['headers'] is body.headers + # The HTTP layer turns the attribute into the If-Match header. + assert ApiClient.__new__(ApiClient)._process_custom_headers(body.headers) == {'If-Match': '"Y3Y9MCZydj0w"'} def test_should_query_payment_instruments(self, mocker, client: AccountsClient): mock = mocker.patch('checkout_sdk.api_client.ApiClient.get', return_value='response') @@ -139,7 +153,7 @@ def test_should_update_reserve_rule(self, mocker, client: AccountsClient): def test_should_upload_entity_file(self, mocker, client: AccountsClient): mock = mocker.patch('checkout_sdk.api_client.ApiClient.post', return_value='response') body = EntityFileRequest() - body.purpose = FilePurpose.IDENTIFICATION + body.purpose = FilePurpose.IDENTITY_VERIFICATION assert client.upload_entity_file('entity_id', body) == 'response' assert_api_call(mock, 'entities/entity_id/files', body) diff --git a/tests/accounts/accounts_integration_test.py b/tests/accounts/accounts_integration_test.py index 64bd6987..15df7eb2 100644 --- a/tests/accounts/accounts_integration_test.py +++ b/tests/accounts/accounts_integration_test.py @@ -8,16 +8,19 @@ from checkout_sdk import CheckoutSdk from checkout_sdk.accounts.accounts import OnboardEntityRequest, ContactDetails, Profile, Individual, \ - DateOfBirth, Identification, EntityEmailAddresses, Company, EntityRepresentative, PaymentInstrumentRequest, \ + DateOfBirth, Identification, EntityIdentification, EntityEmailAddresses, Company, EntityRepresentative, \ + PaymentInstrumentRequest, \ InstrumentDocument, InstrumentDetailsFasterPayments, ReserveRuleRequest, RollingReserveRule, \ HoldingDuration, EntityFileRequest, FilePurpose, RepresentativeIndividual, PlaceOfBirth, EntityRoles, \ - CompanyPosition, BusinessType, DateOfIncorporation, ProcessingDetails, ProcessingDetailsPayments, \ - ProcessingDetailsAch + BusinessType, DateOfIncorporation, ProcessingDetails, ProcessingDetailsPayments, \ + ProcessingDetailsAch, RepresentativeDocuments, EntityIdentificationDocument, CertifiedAuthorisedSignatory, \ + CertifiedAuthorisedSignatoryType, InstrumentDetailsAch, InstrumentAccountType, UpdatePaymentInstrumentRequest, \ + Headers from checkout_sdk.common.common import Phone -from checkout_sdk.common.enums import Currency, Country, InstrumentType +from checkout_sdk.common.enums import Currency, Country, InstrumentType, DocumentType from checkout_sdk.files.files import FileRequest from checkout_sdk.oauth_scopes import OAuthScopes -from tests.checkout_test_utils import assert_response, phone, address, new_uuid, get_project_root, random_email +from tests.checkout_test_utils import assert_response, address, new_uuid, get_project_root, random_email @pytest.fixture(scope='class') @@ -48,7 +51,7 @@ def test_should_create_get_and_update_onboard_entity(accounts_checkout_api): email_addresses = EntityEmailAddresses() email_addresses.primary = random_email() onboard_entity_request.contact_details = ContactDetails() - onboard_entity_request.contact_details.phone = phone() + onboard_entity_request.contact_details.phone = build_v2_phone() onboard_entity_request.contact_details.email_addresses = email_addresses onboard_entity_request.profile = Profile() onboard_entity_request.profile.urls = ['https://www.superheroexample.com'] @@ -58,13 +61,12 @@ def test_should_create_get_and_update_onboard_entity(accounts_checkout_api): onboard_entity_request.individual.last_name = 'Wayne' onboard_entity_request.individual.trading_name = "Batman's Super Hero Masks" onboard_entity_request.individual.registered_address = address() - onboard_entity_request.individual.national_tax_id = 'TAX123456' onboard_entity_request.individual.date_of_birth = DateOfBirth() onboard_entity_request.individual.date_of_birth.day = 5 onboard_entity_request.individual.date_of_birth.month = 6 onboard_entity_request.individual.date_of_birth.year = 1996 onboard_entity_request.individual.identification = Identification() - onboard_entity_request.individual.identification.national_id_number = 'AB123456C' + onboard_entity_request.individual.identification.national_id_number = '123456789' # v2.0 payload (top-level individual) — pin to schema_version 2.0 (SDK now defaults to 3.0) create_entity_response = accounts_checkout_api.accounts.create_entity(onboard_entity_request, '2.0') @@ -83,8 +85,7 @@ def test_should_create_get_and_update_onboard_entity(accounts_checkout_api): 'individual', 'individual.first_name', 'individual.last_name', - 'individual.trading_name', - 'individual.national_tax_id') + 'individual.trading_name') onboard_entity_request.individual.first_name = 'John' @@ -96,77 +97,65 @@ def test_should_create_get_and_update_onboard_entity(accounts_checkout_api): assert create_entity_response.id == update_response.id -@pytest.mark.skip(reason='Schema 3.0 onboarding pending sandbox account currency-scope confirmation') def test_should_onboard_company_v3(accounts_checkout_api): - entity_request = OnboardEntityRequest() - entity_request.reference = new_uuid()[:14] + entity_request = build_company_v3_request() - # v3.0: phone.country_code is an ISO 3166-1 alpha-2 code (finding 3), not a dialing code - entity_request.contact_details = ContactDetails() - v3_phone = Phone() - v3_phone.country_code = 'GB' - v3_phone.number = '2072343000' - entity_request.contact_details.phone = v3_phone - entity_request.contact_details.email_addresses = EntityEmailAddresses() - entity_request.contact_details.email_addresses.primary = random_email() + # default schema_version is 3.0 + create_response = accounts_checkout_api.accounts.create_entity(entity_request) + assert_response(create_response, 'id', 'reference') - # v3.0: profile currencies are validated against the platform scope (finding 4) - entity_request.profile = Profile() - entity_request.profile.urls = ['https://www.superheroexample.com'] - entity_request.profile.mccs = ['0742'] - entity_request.profile.default_holding_currency = Currency.USD - entity_request.profile.holding_currencies = [Currency.USD] + get_response = accounts_checkout_api.accounts.get_entity(create_response.id) + assert_response(get_response, 'id', 'reference', 'company', 'company.representatives') - entity_request.company = Company() - entity_request.company.legal_name = 'Super Hero Masks Inc.' - entity_request.company.trading_name = 'Super Hero Masks' - entity_request.company.business_registration_number = '01234567' - entity_request.company.business_type = BusinessType.LIMITED_COMPANY - entity_request.company.principal_address = address() - entity_request.company.registered_address = address() - entity_request.company.date_of_incorporation = DateOfIncorporation() - entity_request.company.date_of_incorporation.day = 1 - entity_request.company.date_of_incorporation.month = 6 - entity_request.company.date_of_incorporation.year = 2010 - # v3.0: representative is a "person of interest" with a nested individual + roles (finding 2) - representative = EntityRepresentative() - representative.roles = [EntityRoles.UBO, EntityRoles.AUTHORISED_SIGNATORY] - representative.company_position = CompanyPosition.CEO - representative.ownership_percentage = 100 - representative.individual = RepresentativeIndividual() - representative.individual.first_name = 'John' - representative.individual.last_name = 'Doe' - representative.individual.national_id_number = 'AB123456C' - representative.individual.email_address = random_email() - representative.individual.address = address() - representative.individual.date_of_birth = DateOfBirth() - representative.individual.date_of_birth.day = 5 - representative.individual.date_of_birth.month = 6 - representative.individual.date_of_birth.year = 1996 - representative.individual.place_of_birth = PlaceOfBirth() - representative.individual.place_of_birth.country = Country.GB - entity_request.company.representatives = [representative] +# 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 +# accounts_v3_serialization_test, since this platform rejects them. +def test_should_onboard_entity_with_representative_documents(accounts_checkout_api): + identity_file = upload_file(accounts_checkout_api, 'identity_verification') + signatory_file = upload_file(accounts_checkout_api, 'certified_authorised_signatory') - # v3.0: processing currency reflects the sub-entity region and can differ from profile scope (finding 4) - entity_request.processing_details = ProcessingDetails() - entity_request.processing_details.settlement_country = 'GB' - entity_request.processing_details.target_countries = ['GB'] - entity_request.processing_details.annual_processing_volume = 1000000 - entity_request.processing_details.average_transaction_value = 5000 - entity_request.processing_details.highest_transaction_value = 25000 - entity_request.processing_details.currency = Currency.GBP - entity_request.processing_details.payments = ProcessingDetailsPayments() - entity_request.processing_details.payments.ach = ProcessingDetailsAch() - entity_request.processing_details.payments.ach.annual_ach_volume = 1000000 - entity_request.processing_details.payments.ach.average_ach_transaction_size = 5000 + identity = EntityIdentificationDocument() + identity.type = DocumentType.PASSPORT + identity.front = identity_file.id + signatory = CertifiedAuthorisedSignatory() + signatory.type = CertifiedAuthorisedSignatoryType.POWER_OF_ATTORNEY + signatory.front = signatory_file.id + documents = RepresentativeDocuments() + documents.identity_verification = identity + documents.certified_authorised_signatory = signatory + + entity_request = build_company_v3_request() + entity_request.company.representatives[0].documents = documents - # default schema_version is 3.0 create_response = accounts_checkout_api.accounts.create_entity(entity_request) - assert_response(create_response, 'id', 'reference') + assert_response(create_response, 'id') + # The documents are linked on the representative, not dropped: the API echoes them back. get_response = accounts_checkout_api.accounts.get_entity(create_response.id) - assert_response(get_response, 'id', 'reference', 'company', 'company.representatives') + linked = get_response.company.representatives[0].documents + assert linked.identity_verification.type == 'passport' + assert linked.identity_verification.front == identity_file.id + assert linked.certified_authorised_signatory.type == 'power_of_attorney' + assert linked.certified_authorised_signatory.front == signatory_file.id + + +# 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. +def test_should_upload_representative_proof_files(accounts_checkout_api): + entity_id = accounts_checkout_api.accounts.create_entity(build_company_v3_request()).id + + for purpose in (FilePurpose.PROOF_OF_RESIDENTIAL_ADDRESS, FilePurpose.PROOF_OF_REGISTRATION): + request = EntityFileRequest() + request.purpose = purpose + upload_response = accounts_checkout_api.accounts.upload_entity_file(entity_id, request) + assert_response(upload_response, 'id', '_links') + + retrieve_response = accounts_checkout_api.accounts.retrieve_entity_file(entity_id, upload_response.id) + assert_response(retrieve_response, 'id', 'purpose') + assert retrieve_response.purpose == purpose.value def test_should_upload_file(accounts_checkout_api): @@ -184,7 +173,7 @@ def test_should_create_and_retrieve_payment_instrument(accounts_checkout_api): entity_request = OnboardEntityRequest() entity_request.reference = new_uuid()[:14] entity_request.contact_details = ContactDetails() - entity_request.contact_details.phone = phone() + entity_request.contact_details.phone = build_v2_phone() entity_request.contact_details.email_addresses = EntityEmailAddresses() entity_request.contact_details.email_addresses.primary = random_email() entity_request.profile = Profile() @@ -200,8 +189,8 @@ def test_should_create_and_retrieve_payment_instrument(accounts_checkout_api): representative.first_name = 'John' representative.last_name = 'Doe' representative.address = address() - representative.identification = Identification() - representative.identification.national_id_number = 'AB123456C' + representative.identification = EntityIdentification() + representative.identification.national_id_number = '123456789' entity_request.company.representatives = [representative] # v2.0 payload (flat representative) — pin to schema_version 2.0 @@ -345,18 +334,12 @@ def test_update_reserve_rule_should_return_valid_response(accounts_checkout_api) assert response.id == create_response.id -@pytest.mark.skip( - reason='sandbox rejects POST accounts/entities with 422 for the individual v2 ' - 'entity this test builds. The company v3 path still passes - see ' - 'test_should_onboard_company_v3. Unrelated to the instruments work; needs ' - 'an accounts-owned fix to the entity payload. Same breakage as ' - 'checkout-sdk-ruby.' -) def test_should_upload_entity_file_and_retrieve(accounts_checkout_api): - entity_id = create_test_entity(accounts_checkout_api) + # A schema 3.0 entity: the sandbox rejects the schema 2.0 one create_test_entity builds. + entity_id = accounts_checkout_api.accounts.create_entity(build_company_v3_request()).id request = EntityFileRequest() - request.purpose = FilePurpose.IDENTIFICATION + request.purpose = FilePurpose.IDENTITY_VERIFICATION upload_response = accounts_checkout_api.accounts.upload_entity_file(entity_id, request) @@ -371,16 +354,114 @@ def test_should_upload_entity_file_and_retrieve(accounts_checkout_api): assert retrieve_response.id == file_id +def test_should_update_payment_instrument_with_etag(accounts_checkout_api): + # The update only succeeds when the ETag reaches the API as the If-Match HTTP header: a request + # without it fails with 428, and one with a stale ETag with 412. + entity_id = accounts_checkout_api.accounts.create_entity(build_company_v3_request()).id + file = upload_file(accounts_checkout_api) + + instrument_request = PaymentInstrumentRequest() + instrument_request.label = 'Main account' + instrument_request.type = InstrumentType.BANK_ACCOUNT + instrument_request.currency = Currency.USD + instrument_request.country = Country.US + instrument_request.document = InstrumentDocument() + instrument_request.document.type = 'bank_statement' + instrument_request.document.file_id = file.id + instrument_request.instrument_details = InstrumentDetailsAch() + instrument_request.instrument_details.account_number = '123456789' + instrument_request.instrument_details.routing_number = '026009593' + # The sandbox rejects checking (instrument_details_account_type_invalid), although the spec lists it. + instrument_request.instrument_details.account_type = InstrumentAccountType.SAVINGS + instrument_id = accounts_checkout_api.accounts.add_payment_instrument(entity_id, instrument_request).id + + details = accounts_checkout_api.accounts.retrieve_payment_instrument_details(entity_id, instrument_id) + etag = {k.lower(): v for k, v in details.http_metadata.headers.items()}['etag'] + + update_request = UpdatePaymentInstrumentRequest() + update_request.label = 'Renamed account' + update_request.headers = Headers() + update_request.headers.if_match = etag + update_response = accounts_checkout_api.accounts.update_payment_instrument(entity_id, instrument_id, + update_request) + + assert update_response.id == instrument_id + updated = accounts_checkout_api.accounts.retrieve_payment_instrument_details(entity_id, instrument_id) + assert updated.label == 'Renamed account' + + # Common methods -def upload_file(api): +def upload_file(api, purpose='bank_verification'): request = FileRequest() request.file = os.path.join(get_project_root(), 'tests', 'resources', 'checkout.jpeg') - request.purpose = 'bank_verification' + request.purpose = purpose response = api.accounts.upload_file(request) assert_response(response, 'id', '_links') return response +# A schema 3.0 company request the sandbox platform accepts: every currency sits inside its USD-only +# currency scope, including the processing details currency. +def build_company_v3_request(): + entity_request = OnboardEntityRequest() + entity_request.reference = new_uuid()[:14] + + entity_request.contact_details = ContactDetails() + v3_phone = Phone() + v3_phone.country_code = 'GB' + v3_phone.number = '2072343000' + entity_request.contact_details.phone = v3_phone + entity_request.contact_details.email_addresses = EntityEmailAddresses() + entity_request.contact_details.email_addresses.primary = random_email() + + entity_request.profile = Profile() + entity_request.profile.urls = ['https://www.example-test-entity.com'] + entity_request.profile.mccs = ['0742'] + entity_request.profile.default_holding_currency = Currency.USD + entity_request.profile.holding_currencies = [Currency.USD] + + entity_request.company = Company() + entity_request.company.legal_name = 'Test Sub-Entity Company Inc.' + entity_request.company.trading_name = 'Test Sub-Entity Trading' + entity_request.company.business_registration_number = '01234567' + entity_request.company.business_type = BusinessType.LIMITED_COMPANY + entity_request.company.principal_address = address() + entity_request.company.registered_address = address() + entity_request.company.date_of_incorporation = DateOfIncorporation() + entity_request.company.date_of_incorporation.day = 1 + entity_request.company.date_of_incorporation.month = 6 + entity_request.company.date_of_incorporation.year = 2010 + + representative = EntityRepresentative() + representative.roles = [EntityRoles.UBO, EntityRoles.AUTHORISED_SIGNATORY, EntityRoles.DIRECTOR, + EntityRoles.CONTROL_PERSON] + representative.individual = RepresentativeIndividual() + representative.individual.first_name = 'John' + representative.individual.last_name = 'Representative' + representative.individual.address = address() + representative.individual.date_of_birth = DateOfBirth() + representative.individual.date_of_birth.day = 5 + representative.individual.date_of_birth.month = 6 + representative.individual.date_of_birth.year = 1996 + representative.individual.place_of_birth = PlaceOfBirth() + representative.individual.place_of_birth.country = Country.GB + entity_request.company.representatives = [representative] + + entity_request.processing_details = ProcessingDetails() + entity_request.processing_details.target_countries = ['GB'] + entity_request.processing_details.annual_processing_volume = 1000000 + entity_request.processing_details.average_transaction_value = 5000 + entity_request.processing_details.average_order_fulfillment_time = 3 + entity_request.processing_details.currency = Currency.USD + entity_request.processing_details.payments = ProcessingDetailsPayments() + entity_request.processing_details.payments.ach = ProcessingDetailsAch() + entity_request.processing_details.payments.ach.annual_ach_volume = 1000000 + entity_request.processing_details.payments.ach.average_ach_transaction_size = 5000 + entity_request.processing_details.payments.ach.estimated_monthly_credit_volume = 100000 + entity_request.processing_details.payments.ach.average_credit_amount = 5000 + return entity_request + + def create_test_entity(api): entity_request = OnboardEntityRequest() entity_request.reference = new_uuid()[:15] @@ -407,12 +488,20 @@ def create_test_entity(api): def build_contact_details(): contact_details = ContactDetails() - contact_details.phone = phone() + contact_details.phone = build_v2_phone() contact_details.email_addresses = EntityEmailAddresses() contact_details.email_addresses.primary = random_email() return contact_details +# The v2.0 contact phone takes a number only, with no country_code. The value fits the EEA and GB +# pattern (^[1-9][0-9]{7,15}$) and the US one (^[2-9]{1}[0-9]{9,15}$). +def build_v2_phone(): + v2_phone = Phone() + v2_phone.number = '2072343000' + return v2_phone + + def build_profile(): profile = Profile() profile.urls = ['https://www.superheroexample.com'] diff --git a/tests/accounts/accounts_v3_serialization_test.py b/tests/accounts/accounts_v3_serialization_test.py index 8905824c..4e9122f0 100644 --- a/tests/accounts/accounts_v3_serialization_test.py +++ b/tests/accounts/accounts_v3_serialization_test.py @@ -1,6 +1,8 @@ import json +from checkout_sdk.checkout_response import ResponseWrapper from checkout_sdk.json_serializer import JsonSerializer +from checkout_sdk.common.common import Address, Phone from checkout_sdk.common.enums import Country, Currency, DocumentType from checkout_sdk.accounts.accounts import ( ProcessingDetails, ProcessingDetailsPayments, ProcessingDetailsAch, @@ -15,6 +17,8 @@ TaxVerification, TaxVerificationType, FinancialVerification, FinancialVerificationType, RepresentativeDocuments, CertifiedAuthorisedSignatory, CertifiedAuthorisedSignatoryType, ProofOfResidentialAddress, ProofOfResidentialAddressType, ProofOfRegistration, ProofOfRegistrationType, + FilePurpose, EntityEmailAddresses, ContactDetails, Invitee, Profile, DateOfBirth, PlaceOfBirth, + EntityFinancialDetails, Individual, Identification, EntityIdentification, EntityFileRequest, ) @@ -24,6 +28,9 @@ def _serialize(obj): class TestAccountsV3Serialization: + # The US ISV Seller (3.0) processing details: USD only, with average_order_fulfillment_time and + # payments.ach, and without the settlement_country and highest_transaction_value the other + # v3.0 variants take. def test_serializes_processing_details_with_payments(self): ach = ProcessingDetailsAch() ach.annual_ach_volume = 1000000 @@ -36,20 +43,16 @@ def test_serializes_processing_details_with_payments(self): details.annual_processing_volume = 1000000 details.average_transaction_value = 5000 details.average_order_fulfillment_time = 3 - details.highest_transaction_value = 25000 - details.currency = Currency.GBP - details.settlement_country = 'GB' - details.target_countries = ['GB'] + details.currency = Currency.USD + details.target_countries = ['US'] details.payments = payments assert _serialize(details) == { 'annual_processing_volume': 1000000, 'average_transaction_value': 5000, 'average_order_fulfillment_time': 3, - 'highest_transaction_value': 25000, - 'currency': 'GBP', - 'settlement_country': 'GB', - 'target_countries': ['GB'], + 'currency': 'USD', + 'target_countries': ['US'], 'payments': { 'ach': { 'annual_ach_volume': 1000000, @@ -60,6 +63,21 @@ def test_serializes_processing_details_with_payments(self): }, } + # The US ISV Seller variants (3.0) require pci_compliance_contact next to primary. + def test_serializes_entity_email_addresses_with_pci_compliance_contact(self): + email_addresses = EntityEmailAddresses() + email_addresses.primary = 'admin@superhero1234.com' + email_addresses.pci_compliance_contact = 'pci@superhero1234.com' + + body = json.dumps(email_addresses, cls=JsonSerializer) + + assert json.loads(body) == { + 'primary': 'admin@superhero1234.com', + 'pci_compliance_contact': 'pci@superhero1234.com', + } + # Key-level check on the raw body, so a naming change cannot pass silently. + assert '"pci_compliance_contact": "pci@superhero1234.com"' in body + def test_serializes_agreed_terms(self): agreed_terms = AgreedTerms() agreed_terms.date = '2026-07-20T10:00:00Z' @@ -76,27 +94,25 @@ def test_serializes_agreed_terms(self): 'version': '1.0', } + # US ISV Seller Sole Trader (3.0) is the only variant with is_registered_company, and it allows + # only false, together with business_type individual_or_sole_proprietorship. def test_serializes_company_v3_fields(self): date_of_incorporation = DateOfIncorporation() date_of_incorporation.day = 1 date_of_incorporation.month = 6 date_of_incorporation.year = 2010 company = Company() - company.legal_name = 'Super Hero Masks Inc.' company.trading_name = 'Super Hero Masks' - company.business_registration_number = '01234567' - company.business_type = BusinessType.LIMITED_COMPANY + company.business_type = BusinessType.INDIVIDUAL_OR_SOLE_PROPRIETORSHIP company.additional_trading_names = ['SHM'] - company.is_registered_company = True + company.is_registered_company = False company.date_of_incorporation = date_of_incorporation assert _serialize(company) == { - 'legal_name': 'Super Hero Masks Inc.', 'trading_name': 'Super Hero Masks', - 'business_registration_number': '01234567', - 'business_type': 'limited_company', + 'business_type': 'individual_or_sole_proprietorship', 'additional_trading_names': ['SHM'], - 'is_registered_company': True, + 'is_registered_company': False, 'date_of_incorporation': {'day': 1, 'month': 6, 'year': 2010}, } @@ -109,10 +125,10 @@ def test_serializes_representative_v3_fields(self): individual.last_name = 'Doe' individual.citizenships = [citizenship] individual.national_id_type = NationalIdType.SSN - individual.national_id_number = 'AB123456C' + individual.national_id_number = '123456789' individual.email_address = 'john@example.com' representative = EntityRepresentative() - representative.id = 'rep_00000000000000000000000000' + representative.id = 'rep_be6xo4i6ia8mq7vz27su1ma6li' representative.individual = individual representative.company_position = CompanyPosition.CEO representative.ownership_percentage = 100 @@ -120,13 +136,13 @@ def test_serializes_representative_v3_fields(self): EntityRoles.DIRECTOR, EntityRoles.CONTROL_PERSON] assert _serialize(representative) == { - 'id': 'rep_00000000000000000000000000', + 'id': 'rep_be6xo4i6ia8mq7vz27su1ma6li', 'individual': { 'first_name': 'John', 'last_name': 'Doe', 'citizenships': [{'type': 'citizenship', 'country': 'US'}], 'national_id_type': 'ssn', - 'national_id_number': 'AB123456C', + 'national_id_number': '123456789', 'email_address': 'john@example.com', }, 'company_position': 'ceo', @@ -137,17 +153,17 @@ def test_serializes_representative_v3_fields(self): def test_serializes_representative_documents(self): identity = EntityIdentificationDocument() identity.type = DocumentType.PASSPORT - identity.front = 'file_identity_front' - identity.back = 'file_identity_back' + identity.front = 'file_ebxawxm4fesbgqtwtiuikwdviu' + identity.back = 'file_3wb62nghcba73hmzz7rxfdpgtp' signatory = CertifiedAuthorisedSignatory() signatory.type = CertifiedAuthorisedSignatoryType.POWER_OF_ATTORNEY - signatory.front = 'file_signatory' + signatory.front = 'file_trjpkykozlhwurcfeie24lpp52' residential = ProofOfResidentialAddress() residential.type = ProofOfResidentialAddressType.PROOF_OF_ADDRESS - residential.front = 'file_residential' + residential.front = 'file_5dzmhwq66uettzacvm23zde6cj' registration = ProofOfRegistration() registration.type = ProofOfRegistrationType.EXTRACT_FROM_TRADE_REGISTER - registration.front = 'file_registration' + registration.front = 'file_hgf4rera4kdlmuv7nb4ehzkr5a' documents = RepresentativeDocuments() documents.identity_verification = identity documents.certified_authorised_signatory = signatory @@ -155,23 +171,132 @@ def test_serializes_representative_documents(self): documents.proof_of_registration = registration assert _serialize(documents) == { - 'identity_verification': {'type': 'passport', 'front': 'file_identity_front', 'back': 'file_identity_back'}, - 'certified_authorised_signatory': {'type': 'power_of_attorney', 'front': 'file_signatory'}, - 'proof_of_residential_address': {'type': 'proof_of_address', 'front': 'file_residential'}, - 'proof_of_registration': {'type': 'extract_from_trade_register', 'front': 'file_registration'}, + 'identity_verification': { + 'type': 'passport', + 'front': 'file_ebxawxm4fesbgqtwtiuikwdviu', + 'back': 'file_3wb62nghcba73hmzz7rxfdpgtp', + }, + 'certified_authorised_signatory': {'type': 'power_of_attorney', 'front': 'file_trjpkykozlhwurcfeie24lpp52'}, + 'proof_of_residential_address': {'type': 'proof_of_address', 'front': 'file_5dzmhwq66uettzacvm23zde6cj'}, + 'proof_of_registration': { + 'type': 'extract_from_trade_register', 'front': 'file_hgf4rera4kdlmuv7nb4ehzkr5a'}, + } + + # 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. + def test_serializes_eea_sole_trader_representative_documents(self): + identity = EntityIdentificationDocument() + identity.type = DocumentType.PASSPORT + identity.front = 'file_identityverificationaaaaaa' + residential = ProofOfResidentialAddress() + residential.type = ProofOfResidentialAddressType.PROOF_OF_ADDRESS + residential.front = 'file_proofofresidentialaddressa' + registration = ProofOfRegistration() + registration.type = ProofOfRegistrationType.EXTRACT_FROM_TRADE_REGISTER + registration.front = 'file_proofofregistrationaaaaaaa' + rep_documents = RepresentativeDocuments() + rep_documents.identity_verification = identity + rep_documents.proof_of_residential_address = residential + rep_documents.proof_of_registration = registration + individual = RepresentativeIndividual() + individual.first_name = 'Jane' + individual.last_name = 'Doe' + representative = EntityRepresentative() + representative.individual = individual + representative.roles = [EntityRoles.UBO] + representative.documents = rep_documents + company = Company() + company.business_type = BusinessType.INDIVIDUAL_OR_SOLE_PROPRIETORSHIP + company.representatives = [representative] + bank = BankVerification() + bank.type = BankVerificationType.BANK_STATEMENT + bank.front = 'file_bankverificationaaaaaaaaaa' + documents = OnboardSubEntityDocuments() + documents.bank_verification = bank + request = OnboardEntityRequest() + request.reference = 'ref_sole_trader' + request.company = company + request.documents = documents + + body = json.dumps(request, cls=JsonSerializer) + result = json.loads(body) + + assert result['company']['representatives'][0]['documents'] == { + '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'}, + } + assert result['documents'] == { + 'bank_verification': {'type': 'bank_statement', 'front': 'file_bankverificationaaaaaaaaaa'}} + # Key-level check on the raw body, so a naming change cannot pass silently. + assert '"proof_of_residential_address": {' in body + assert '"proof_of_registration": {' in body + + # No variant defines a key on company.representatives[].documents other than these four, and the + # EEA, GB and US Company Full (3.0) person of interest and Sole Trader Full (3.0) variants reject + # unknown keys (additionalProperties: false), so an attribute added here by mistake would fail + # the request on those variants. + def test_representative_documents_declares_only_the_keys_the_api_accepts(self): + assert list(RepresentativeDocuments.__annotations__) == [ + 'identity_verification', + 'certified_authorised_signatory', + 'proof_of_residential_address', + 'proof_of_registration', + ] + + # Leaving an attribute unset omits it; assigning None sends null, which the docstring on + # RepresentativeDocuments warns about. + def test_representative_documents_unset_attributes_are_omitted_and_none_is_sent(self): + registration = ProofOfRegistration() + registration.type = ProofOfRegistrationType.OTHER + registration.front = 'file_proofofregistrationaaaaaaa' + documents = RepresentativeDocuments() + documents.proof_of_registration = registration + + assert _serialize(documents) == { + 'proof_of_registration': {'type': 'other', 'front': 'file_proofofregistrationaaaaaaa'}} + + documents.identity_verification = None + assert _serialize(documents)['identity_verification'] is None + + # EEA and GB Company Full (3.0) allow a representative that is a company: + # { company: { legal_name, trading_name, registered_address }, ownership_percentage }. + def test_serializes_controlling_company_representative(self): + address = Address() + address.address_line1 = '1 Main Street' + address.city = 'London' + address.zip = 'W1T 4TJ' + address.country = Country.GB + company = Company() + company.legal_name = 'Parent Holdings Ltd' + company.trading_name = 'Parent Holdings' + company.registered_address = address + representative = EntityRepresentative() + representative.company = company + representative.ownership_percentage = 60 + + assert _serialize(representative) == { + 'company': { + 'legal_name': 'Parent Holdings Ltd', + 'trading_name': 'Parent Holdings', + 'registered_address': { + 'address_line1': '1 Main Street', 'city': 'London', 'zip': 'W1T 4TJ', 'country': 'GB'}, + }, + 'ownership_percentage': 60, } def test_serializes_financial_statements_document(self): financial_statements = FinancialStatements() financial_statements.type = FinancialStatementsType.FINANCIAL_STATEMENTS - financial_statements.front = 'file_00000000000000000000000000' + financial_statements.front = 'file_xwc7fyfsezfda35wxcimpsw6q2' documents = OnboardSubEntityDocuments() documents.financial_statements = financial_statements assert _serialize(documents) == { 'financial_statements': { 'type': 'financial_statements', - 'front': 'file_00000000000000000000000000', + 'front': 'file_xwc7fyfsezfda35wxcimpsw6q2', } } @@ -210,13 +335,13 @@ def test_serializes_full_onboard_entity_request_v3(self): representative.roles = [EntityRoles.UBO] company = Company() company.legal_name = 'Super Hero Masks Inc.' - company.business_type = BusinessType.LIMITED_COMPANY + company.business_type = BusinessType.PRIVATE_CORPORATION company.representatives = [representative] payments = ProcessingDetailsPayments() payments.ach = ProcessingDetailsAch() payments.ach.annual_ach_volume = 1000000 processing_details = ProcessingDetails() - processing_details.currency = Currency.GBP + processing_details.currency = Currency.USD processing_details.payments = payments request = OnboardEntityRequest() request.reference = 'ref_1' @@ -227,10 +352,10 @@ def test_serializes_full_onboard_entity_request_v3(self): assert result['reference'] == 'ref_1' assert result['company']['legal_name'] == 'Super Hero Masks Inc.' - assert result['company']['business_type'] == 'limited_company' + assert result['company']['business_type'] == 'private_corporation' assert result['company']['representatives'][0]['individual']['first_name'] == 'John' assert result['company']['representatives'][0]['roles'] == ['ubo'] - assert result['processing_details']['currency'] == 'GBP' + assert result['processing_details']['currency'] == 'USD' assert result['processing_details']['payments']['ach']['annual_ach_volume'] == 1000000 def test_serializes_all_thirteen_documents_fields(self): @@ -238,81 +363,108 @@ def test_serializes_all_thirteen_documents_fields(self): identity_verification = EntityIdentificationDocument() identity_verification.type = DocumentType.NATIONAL_IDENTITY_CARD - identity_verification.front = 'file_identity_front' - identity_verification.back = 'file_identity_back' + identity_verification.front = 'file_ebxawxm4fesbgqtwtiuikwdviu' + identity_verification.back = 'file_3wb62nghcba73hmzz7rxfdpgtp' documents.identity_verification = identity_verification company_verification = CompanyVerification() company_verification.type = CompanyVerificationType.INCORPORATION_DOCUMENT - company_verification.front = 'file_company_verification' + company_verification.front = 'file_std7uf52bx3hvqvoiiyne6fx6m' documents.company_verification = company_verification articles_of_association = ArticlesOfAssociation() articles_of_association.type = ArticlesOfAssociationType.ARTICLES_OF_ASSOCIATION - articles_of_association.front = 'file_articles_of_association' + articles_of_association.front = 'file_734v2ecg5yxaqrvomqqnpjxgqw' documents.articles_of_association = articles_of_association bank_verification = BankVerification() bank_verification.type = BankVerificationType.BANK_STATEMENT - bank_verification.front = 'file_bank_verification' + bank_verification.front = 'file_3aeeozugysd4ivxus6ytlwwh7a' documents.bank_verification = bank_verification shareholder_structure = ShareholderStructure() shareholder_structure.type = ShareholderStructureType.CERTIFIED_SHAREHOLDER_STRUCTURE - shareholder_structure.front = 'file_shareholder_structure' + shareholder_structure.front = 'file_xfkfxrkzawbxgl7tz7c27qdnz5' documents.shareholder_structure = shareholder_structure proof_of_legality = ProofOfLegality() proof_of_legality.type = ProofOfLegalityType.PROOF_OF_LEGALITY - proof_of_legality.front = 'file_proof_of_legality' + proof_of_legality.front = 'file_zhlrertry7amktnhskyj5g4hvb' documents.proof_of_legality = proof_of_legality proof_of_principal_address = ProofOfPrincipalAddress() proof_of_principal_address.type = ProofOfPrincipalAddressType.PROOF_OF_ADDRESS - proof_of_principal_address.front = 'file_proof_of_principal_address' + proof_of_principal_address.front = 'file_lk6ym6bhljxnvnvabzphglsllv' documents.proof_of_principal_address = proof_of_principal_address tax_verification = TaxVerification() tax_verification.type = TaxVerificationType.EIN_LETTER - tax_verification.front = 'file_tax_verification' + tax_verification.front = 'file_vexm5xyve2qwdmgzzk5fceyoxw' documents.tax_verification = tax_verification financial_verification = FinancialVerification() financial_verification.type = FinancialVerificationType.FINANCIAL_STATEMENT - financial_verification.front = 'file_financial_verification' + financial_verification.front = 'file_smwugkyjp2oyaaj4stcu2rzrrc' documents.financial_verification = financial_verification financial_statements = FinancialStatements() financial_statements.type = FinancialStatementsType.FINANCIAL_STATEMENTS - financial_statements.front = 'file_financial_statements' + financial_statements.front = 'file_sddb4dghum37xzptw3zdiegg3n' documents.financial_statements = financial_statements additional_document1 = AdditionalDocument() - additional_document1.front = 'file_additional_document1' + additional_document1.front = 'file_5hvmsac5bzbmg7lnypr4so2h43' documents.additional_document1 = additional_document1 additional_document2 = AdditionalDocument() - additional_document2.front = 'file_additional_document2' + additional_document2.front = 'file_mh2fyxtghcalxx77gsfb3gk77t' documents.additional_document2 = additional_document2 additional_document3 = AdditionalDocument() - additional_document3.front = 'file_additional_document3' + additional_document3.front = 'file_ok7jhmf4nzkcudzyh5v5q2kc6d' documents.additional_document3 = additional_document3 - result = _serialize(documents) + assert _serialize(documents) == { + 'identity_verification': { + 'type': 'national_identity_card', + 'front': 'file_ebxawxm4fesbgqtwtiuikwdviu', + 'back': 'file_3wb62nghcba73hmzz7rxfdpgtp', + }, + 'company_verification': {'type': 'incorporation_document', 'front': 'file_std7uf52bx3hvqvoiiyne6fx6m'}, + 'articles_of_association': { + 'type': 'articles_of_association', 'front': 'file_734v2ecg5yxaqrvomqqnpjxgqw'}, + 'bank_verification': {'type': 'bank_statement', 'front': 'file_3aeeozugysd4ivxus6ytlwwh7a'}, + 'shareholder_structure': { + 'type': 'certified_shareholder_structure', 'front': 'file_xfkfxrkzawbxgl7tz7c27qdnz5'}, + 'proof_of_legality': {'type': 'proof_of_legality', 'front': 'file_zhlrertry7amktnhskyj5g4hvb'}, + 'proof_of_principal_address': {'type': 'proof_of_address', 'front': 'file_lk6ym6bhljxnvnvabzphglsllv'}, + 'tax_verification': {'type': 'ein_letter', 'front': 'file_vexm5xyve2qwdmgzzk5fceyoxw'}, + 'financial_verification': {'type': 'financial_statement', 'front': 'file_smwugkyjp2oyaaj4stcu2rzrrc'}, + 'financial_statements': {'type': 'financial_statements', 'front': 'file_sddb4dghum37xzptw3zdiegg3n'}, + # The additional documents take a front only; the spec defines no type for them. + 'additional_document1': {'front': 'file_5hvmsac5bzbmg7lnypr4so2h43'}, + 'additional_document2': {'front': 'file_mh2fyxtghcalxx77gsfb3gk77t'}, + 'additional_document3': {'front': 'file_ok7jhmf4nzkcudzyh5v5q2kc6d'}, + } - expected_fields = [ - 'identity_verification', 'company_verification', 'articles_of_association', - 'bank_verification', 'shareholder_structure', 'proof_of_legality', - 'proof_of_principal_address', 'tax_verification', 'financial_verification', - 'financial_statements', 'additional_document1', 'additional_document2', - 'additional_document3', - ] - assert sorted(result.keys()) == sorted(expected_fields) - for field in expected_fields: - assert 'front' in result[field], f'{field} must serialize a front' - # identity_verification is the only document that carries a back - assert result['identity_verification']['back'] == 'file_identity_back' - assert result['articles_of_association'] == { - 'type': 'articles_of_association', 'front': 'file_articles_of_association' + # POST /entities/{entity_id}/files sends the purpose's value on the wire; the first fourteen are + # the PlatformsFileUpload enum, the last two are not accepted by that endpoint. + def test_file_purpose_enum_values(self): + assert {p.name: p.value for p in FilePurpose} == { + 'ADDITIONAL_DOCUMENT': 'additional_document', + 'ARTICLES_OF_ASSOCIATION': 'articles_of_association', + 'BANK_VERIFICATION': 'bank_verification', + 'CERTIFIED_AUTHORISED_SIGNATORY': 'certified_authorised_signatory', + 'COMPANY_OWNERSHIP': 'company_ownership', + 'IDENTITY_VERIFICATION': 'identity_verification', + 'COMPANY_VERIFICATION': 'company_verification', + 'FINANCIAL_VERIFICATION': 'financial_verification', + 'TAX_VERIFICATION': 'tax_verification', + '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', + 'IDENTIFICATION': 'identification', + 'DISPUTE_EVIDENCE': 'dispute_evidence', } def test_entity_roles_enum_values(self): @@ -336,3 +488,820 @@ def test_business_type_enum_values(self): 'government_agency', 'non_profit_entity', 'trust', 'club_or_society', 'regulated_financial_institution', 'cftc_registered_entity', 'sec_registered_entity', ] + + def test_document_type_enum_values(self): + assert [d.value for d in DocumentType] == [ + 'passport', 'national_identity_card', 'driving_license', 'citizen_card', 'residence_permit', + 'electoral_id', + ] + + def test_company_verification_type_enum_values(self): + assert [t.value for t in CompanyVerificationType] == ['incorporation_document', 'articles_of_association'] + + def test_articles_of_association_type_enum_values(self): + assert [t.value for t in ArticlesOfAssociationType] == ['memorandum_of_association', 'articles_of_association'] + + def test_tax_verification_type_enum_values(self): + assert [t.value for t in TaxVerificationType] == ['ein_letter'] + + def test_shareholder_structure_type_enum_values(self): + assert [t.value for t in ShareholderStructureType] == ['certified_shareholder_structure'] + + def test_proof_of_legality_type_enum_values(self): + assert [t.value for t in ProofOfLegalityType] == ['proof_of_legality'] + + def test_proof_of_principal_address_type_enum_values(self): + assert [t.value for t in ProofOfPrincipalAddressType] == ['proof_of_address'] + + def test_financial_verification_type_enum_values(self): + assert [t.value for t in FinancialVerificationType] == ['financial_statement'] + + def test_national_id_type_enum_values(self): + assert [t.value for t in NationalIdType] == [ + 'ssn', 'itin', 'passport', 'driving_license', 'national_id_card', 'residence_permit', 'other', + ] + + # US Company Full (3.0): phone with an ISO alpha-2 country_code, email_addresses and invitee. + def test_serializes_contact_details(self): + phone = Phone() + phone.country_code = 'US' + phone.number = '4155678900' + email_addresses = EntityEmailAddresses() + email_addresses.primary = 'admin@superhero1234.com' + invitee = Invitee() + invitee.email = 'invitee@superhero1234.com' + contact_details = ContactDetails() + contact_details.phone = phone + contact_details.email_addresses = email_addresses + contact_details.invitee = invitee + + assert _serialize(contact_details) == { + 'phone': {'country_code': 'US', 'number': '4155678900'}, + 'email_addresses': {'primary': 'admin@superhero1234.com'}, + 'invitee': {'email': 'invitee@superhero1234.com'}, + } + + # PlatformsHostedOnboardInviteRequest takes reference, is_draft and contact_details.invitee only. + def test_serializes_hosted_onboarding_invite_request(self): + invitee = Invitee() + invitee.email = 'invitee@superhero1234.com' + request = OnboardEntityRequest() + request.reference = 'superhero1234' + request.is_draft = True + request.contact_details = ContactDetails() + request.contact_details.invitee = invitee + + assert _serialize(request) == { + 'reference': 'superhero1234', + 'is_draft': True, + 'contact_details': {'invitee': {'email': 'invitee@superhero1234.com'}}, + } + + def test_serializes_company_eea_full_fields(self): + principal_address = Address() + principal_address.address_line1 = '12 Rue de Rivoli' + principal_address.city = 'Paris' + principal_address.zip = '75001' + principal_address.country = Country.FR + registered_address = Address() + registered_address.address_line1 = '8 Avenue de l\'Opera' + registered_address.city = 'Paris' + registered_address.zip = '75002' + registered_address.country = Country.FR + date_of_incorporation = DateOfIncorporation() + date_of_incorporation.month = 6 + date_of_incorporation.year = 2010 + date_of_birth = DateOfBirth() + date_of_birth.day = 5 + date_of_birth.month = 6 + date_of_birth.year = 1980 + place_of_birth = PlaceOfBirth() + place_of_birth.country = Country.FR + individual = RepresentativeIndividual() + individual.first_name = 'Marie' + individual.last_name = 'Dupont' + individual.date_of_birth = date_of_birth + individual.place_of_birth = place_of_birth + individual.address = principal_address + representative = EntityRepresentative() + representative.individual = individual + representative.roles = [EntityRoles.UBO, EntityRoles.DIRECTOR] + representative.ownership_percentage = 100 + company = Company() + company.legal_name = 'Super Hero Masques SAS' + company.trading_name = 'Super Hero Masques' + company.business_registration_number = '123456789' + company.business_type = BusinessType.LIMITED_COMPANY + company.date_of_incorporation = date_of_incorporation + company.regulatory_licence_number = 'FR-12345678' + company.principal_address = principal_address + company.registered_address = registered_address + company.representatives = [representative] + + assert _serialize(company) == { + 'legal_name': 'Super Hero Masques SAS', + 'trading_name': 'Super Hero Masques', + 'business_registration_number': '123456789', + 'business_type': 'limited_company', + 'date_of_incorporation': {'month': 6, 'year': 2010}, + 'regulatory_licence_number': 'FR-12345678', + 'principal_address': { + 'address_line1': '12 Rue de Rivoli', 'city': 'Paris', 'zip': '75001', 'country': 'FR'}, + 'registered_address': { + 'address_line1': '8 Avenue de l\'Opera', 'city': 'Paris', 'zip': '75002', 'country': 'FR'}, + 'representatives': [{ + 'individual': { + 'first_name': 'Marie', + 'last_name': 'Dupont', + 'date_of_birth': {'day': 5, 'month': 6, 'year': 1980}, + 'place_of_birth': {'country': 'FR'}, + 'address': { + 'address_line1': '12 Rue de Rivoli', 'city': 'Paris', 'zip': '75001', 'country': 'FR'}, + }, + 'roles': ['ubo', 'director'], + 'ownership_percentage': 100, + }], + } + + # EEA Company Full (2.0) company.financial_details, EUR only. + def test_serializes_company_financial_details_v2(self): + financial_details = EntityFinancialDetails() + financial_details.annual_processing_volume = 120000 + financial_details.average_transaction_value = 500 + financial_details.highest_transaction_value = 2500 + financial_details.currency = Currency.EUR + + assert _serialize(financial_details) == { + 'annual_processing_volume': 120000, + 'average_transaction_value': 500, + 'highest_transaction_value': 2500, + 'currency': 'EUR', + } + + # US Sole Trader Full (2.0) individual: identification and financial_details (USD), no place_of_birth. + def test_serializes_individual_v2_us_sole_trader(self): + registered_address = Address() + registered_address.address_line1 = '123 Main Street' + registered_address.city = 'San Francisco' + registered_address.state = 'CA' + registered_address.zip = '94105' + registered_address.country = Country.US + date_of_birth = DateOfBirth() + date_of_birth.day = 15 + date_of_birth.month = 1 + date_of_birth.year = 1990 + identification = Identification() + identification.national_id_number = '123456789' + financial_details = EntityFinancialDetails() + financial_details.annual_processing_volume = 120000 + financial_details.average_transaction_value = 500 + financial_details.highest_transaction_value = 2500 + financial_details.currency = Currency.USD + individual = Individual() + individual.first_name = 'Hannah' + individual.middle_name = 'Grace' + individual.last_name = 'Bret' + individual.trading_name = 'Hannah\'s Goods' + individual.registered_address = registered_address + individual.date_of_birth = date_of_birth + individual.identification = identification + individual.financial_details = financial_details + + assert _serialize(individual) == { + 'first_name': 'Hannah', + 'middle_name': 'Grace', + 'last_name': 'Bret', + 'trading_name': 'Hannah\'s Goods', + 'registered_address': { + 'address_line1': '123 Main Street', 'city': 'San Francisco', 'state': 'CA', 'zip': '94105', + 'country': 'US'}, + 'date_of_birth': {'day': 15, 'month': 1, 'year': 1990}, + 'identification': {'national_id_number': '123456789'}, + 'financial_details': { + 'annual_processing_volume': 120000, + 'average_transaction_value': 500, + 'highest_transaction_value': 2500, + 'currency': 'USD', + }, + } + + # EEA Sole Trader Full (2.0) individual: place_of_birth, no identification nor financial_details. + def test_serializes_individual_v2_eea_sole_trader(self): + registered_address = Address() + registered_address.address_line1 = '12 Rue de Rivoli' + registered_address.city = 'Paris' + registered_address.zip = '75001' + registered_address.country = Country.FR + date_of_birth = DateOfBirth() + date_of_birth.day = 5 + date_of_birth.month = 6 + date_of_birth.year = 1980 + place_of_birth = PlaceOfBirth() + place_of_birth.country = Country.FR + individual = Individual() + individual.first_name = 'Marie' + individual.middle_name = 'Claire' + individual.last_name = 'Dupont' + individual.trading_name = 'Masques Marie' + individual.registered_address = registered_address + individual.date_of_birth = date_of_birth + individual.place_of_birth = place_of_birth + + assert _serialize(individual) == { + 'first_name': 'Marie', + 'middle_name': 'Claire', + 'last_name': 'Dupont', + 'trading_name': 'Masques Marie', + 'registered_address': { + 'address_line1': '12 Rue de Rivoli', 'city': 'Paris', 'zip': '75001', 'country': 'FR'}, + 'date_of_birth': {'day': 5, 'month': 6, 'year': 1980}, + 'place_of_birth': {'country': 'FR'}, + } + + # US ISV Seller (3.0) is the variant that takes every RepresentativeIndividual attribute. + def test_serializes_representative_individual_all_fields(self): + date_of_birth = DateOfBirth() + date_of_birth.day = 15 + date_of_birth.month = 1 + date_of_birth.year = 1990 + place_of_birth = PlaceOfBirth() + place_of_birth.country = Country.US + citizenship = Citizenship() + citizenship.type = 'citizenship' + citizenship.country = Country.US + phone = Phone() + phone.country_code = 'US' + phone.number = '4155678901' + address = Address() + address.address_line1 = '123 Main Street' + address.city = 'San Francisco' + address.state = 'CA' + address.zip = '94105' + address.country = Country.US + individual = RepresentativeIndividual() + individual.first_name = 'Toby' + individual.middle_name = 'James' + individual.last_name = 'Arden' + individual.date_of_birth = date_of_birth + individual.place_of_birth = place_of_birth + individual.citizenships = [citizenship] + individual.national_id_type = NationalIdType.SSN + individual.national_id_number = '123456789' + individual.email_address = 'toby.arden@example.com' + individual.phone = phone + individual.address = address + + assert _serialize(individual) == { + 'first_name': 'Toby', + 'middle_name': 'James', + 'last_name': 'Arden', + 'date_of_birth': {'day': 15, 'month': 1, 'year': 1990}, + 'place_of_birth': {'country': 'US'}, + 'citizenships': [{'type': 'citizenship', 'country': 'US'}], + 'national_id_type': 'ssn', + 'national_id_number': '123456789', + 'email_address': 'toby.arden@example.com', + 'phone': {'country_code': 'US', 'number': '4155678901'}, + 'address': { + 'address_line1': '123 Main Street', 'city': 'San Francisco', 'state': 'CA', 'zip': '94105', + 'country': 'US'}, + } + + # US Company Full (2.0) flat representative: identification, phone with number only, no + # place_of_birth. + def test_serializes_representative_v2_us_company_fields(self): + date_of_birth = DateOfBirth() + date_of_birth.day = 15 + date_of_birth.month = 1 + date_of_birth.year = 1990 + phone = Phone() + phone.number = '4155678901' + address = Address() + address.address_line1 = '123 Main Street' + address.city = 'San Francisco' + address.state = 'CA' + address.zip = '94105' + address.country = Country.US + identification = EntityIdentification() + identification.national_id_number = '123456789' + representative = EntityRepresentative() + representative.first_name = 'Toby' + representative.middle_name = 'James' + representative.last_name = 'Arden' + representative.date_of_birth = date_of_birth + representative.phone = phone + representative.address = address + representative.identification = identification + representative.roles = [EntityRoles.UBO, EntityRoles.CONTROL_PERSON] + + assert _serialize(representative) == { + 'first_name': 'Toby', + 'middle_name': 'James', + 'last_name': 'Arden', + 'date_of_birth': {'day': 15, 'month': 1, 'year': 1990}, + 'phone': {'number': '4155678901'}, + 'address': { + 'address_line1': '123 Main Street', 'city': 'San Francisco', 'state': 'CA', 'zip': '94105', + 'country': 'US'}, + 'identification': {'national_id_number': '123456789'}, + 'roles': ['ubo', 'control_person'], + } + + # EEA Company Full (2.0) flat representative: place_of_birth, no identification. + def test_serializes_representative_v2_eea_company_fields(self): + date_of_birth = DateOfBirth() + date_of_birth.day = 5 + date_of_birth.month = 6 + date_of_birth.year = 1980 + place_of_birth = PlaceOfBirth() + place_of_birth.country = Country.FR + phone = Phone() + phone.number = '142681234' + address = Address() + address.address_line1 = '12 Rue de Rivoli' + address.city = 'Paris' + address.zip = '75001' + address.country = Country.FR + representative = EntityRepresentative() + representative.first_name = 'Marie' + representative.middle_name = 'Claire' + representative.last_name = 'Dupont' + representative.date_of_birth = date_of_birth + representative.place_of_birth = place_of_birth + representative.phone = phone + representative.address = address + representative.roles = [EntityRoles.LEGAL_REPRESENTATIVE] + + assert _serialize(representative) == { + 'first_name': 'Marie', + 'middle_name': 'Claire', + 'last_name': 'Dupont', + 'date_of_birth': {'day': 5, 'month': 6, 'year': 1980}, + 'place_of_birth': {'country': 'FR'}, + 'phone': {'number': '142681234'}, + 'address': {'address_line1': '12 Rue de Rivoli', 'city': 'Paris', 'zip': '75001', 'country': 'FR'}, + 'roles': ['legal_representative'], + } + + # individual.identification (US Sole Trader, 2.0) and company.representatives[].identification + # (US Company, 2.0) both carry a nine digit national_id_number only. + def test_serializes_identification(self): + identification = Identification() + identification.national_id_number = '123456789' + entity_identification = EntityIdentification() + entity_identification.national_id_number = '123456789' + + assert _serialize(identification) == {'national_id_number': '123456789'} + assert _serialize(entity_identification) == {'national_id_number': '123456789'} + + def test_serializes_entity_file_request(self): + request = EntityFileRequest() + request.purpose = FilePurpose.IDENTITY_VERIFICATION + + body = json.dumps(request, cls=JsonSerializer) + + assert json.loads(body) == {'purpose': 'identity_verification'} + # Key-level check on the raw body, so a naming change cannot pass silently. + assert '"purpose": "identity_verification"' in body + + # components.schemas["USISVSellerCompany3-0"].example in the swagger, built from the SDK classes. + def test_roundtrips_us_isv_seller_company_example(self): + agreed_terms = AgreedTerms() + agreed_terms.date = '2026-07-02T10:30:00.0000000+00:00' + agreed_terms.ip_address = '8.8.8.8' + agreed_terms.name = 'Toby Arden' + agreed_terms.email = 'toby.arden@example.com' + agreed_terms.version = 'cko-platform-terms-1.0.0' + ach = ProcessingDetailsAch() + ach.annual_ach_volume = 100000 + ach.average_ach_transaction_size = 5000 + ach.estimated_monthly_credit_volume = 50000 + ach.average_credit_amount = 2500 + processing_details = ProcessingDetails() + processing_details.annual_processing_volume = 1000 + processing_details.average_transaction_value = 2000 + processing_details.average_order_fulfillment_time = 3 + processing_details.target_countries = ['US'] + processing_details.currency = Currency.USD + processing_details.payments = ProcessingDetailsPayments() + processing_details.payments.ach = ach + contact_phone = Phone() + contact_phone.number = '4155678900' + contact_phone.country_code = 'US' + contact_details = ContactDetails() + contact_details.phone = contact_phone + contact_details.email_addresses = EntityEmailAddresses() + contact_details.email_addresses.primary = 'toby.arden@example.com' + contact_details.email_addresses.pci_compliance_contact = 'pci.contact@example.com' + profile = Profile() + profile.urls = ['https://www.isv-seller-example.com'] + profile.mccs = ['5551'] + profile.holding_currencies = [Currency.USD] + profile.default_holding_currency = Currency.USD + address = Address() + address.address_line1 = '123 Main Street' + address.city = 'San Francisco' + address.state = 'CA' + address.zip = '94105' + address.country = Country.US + date_of_incorporation = DateOfIncorporation() + date_of_incorporation.year = 2025 + date_of_incorporation.month = 10 + date_of_incorporation.day = 1 + + ubo = RepresentativeIndividual() + ubo.first_name = 'Toby' + ubo.last_name = 'Arden' + ubo.email_address = 'toby.arden@example.com' + ubo.national_id_type = NationalIdType.SSN + ubo.national_id_number = '123456789' + ubo.date_of_birth = DateOfBirth() + ubo.date_of_birth.day = 15 + ubo.date_of_birth.month = 1 + ubo.date_of_birth.year = 1990 + ubo.place_of_birth = PlaceOfBirth() + ubo.place_of_birth.country = Country.US + ubo.citizenships = [Citizenship()] + ubo.citizenships[0].country = Country.US + ubo.phone = Phone() + ubo.phone.country_code = 'US' + ubo.phone.number = '4155678901' + ubo.address = address + first = EntityRepresentative() + first.roles = [EntityRoles.UBO, EntityRoles.CONTROL_PERSON] + first.ownership_percentage = 25 + first.company_position = CompanyPosition.CEO + first.individual = ubo + + signatory = RepresentativeIndividual() + signatory.first_name = 'Alex' + signatory.last_name = 'Morgan' + signatory.email_address = 'alex.morgan@example.com' + signatory.national_id_type = NationalIdType.SSN + signatory.national_id_number = '987654321' + signatory.date_of_birth = DateOfBirth() + signatory.date_of_birth.day = 22 + signatory.date_of_birth.month = 6 + signatory.date_of_birth.year = 1985 + signatory.place_of_birth = PlaceOfBirth() + signatory.place_of_birth.country = Country.US + signatory.citizenships = [Citizenship()] + signatory.citizenships[0].country = Country.US + signatory.phone = Phone() + signatory.phone.country_code = 'US' + signatory.phone.number = '4155678902' + signatory.address = address + second = EntityRepresentative() + second.roles = [EntityRoles.AUTHORISED_SIGNATORY] + second.individual = signatory + + company = Company() + company.business_registration_number = '12-3456789' + company.business_type = BusinessType.PRIVATE_CORPORATION + company.legal_name = 'ISV Seller Example Inc' + company.trading_name = 'ISV Seller Example' + company.registered_address = address + company.principal_address = address + company.date_of_incorporation = date_of_incorporation + company.representatives = [first, second] + request = OnboardEntityRequest() + request.reference = 'isv-seller-example001' + request.agreed_terms = agreed_terms + request.seller_category = 'cat_retail_001' + request.processing_details = processing_details + request.contact_details = contact_details + request.profile = profile + request.company = company + + assert _serialize(request) == { + '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', + }, + }, + }, + ], + }, + } + + # components.schemas["USISVSellerSoleTrader3-0"].example in the swagger, built from the SDK classes. + def test_roundtrips_us_isv_seller_sole_trader_example(self): + agreed_terms = AgreedTerms() + agreed_terms.date = '2026-07-02T10:30:00.0000000+00:00' + agreed_terms.ip_address = '8.8.8.8' + agreed_terms.name = 'Hannah Bret' + agreed_terms.email = 'hannah.bret@example.com' + agreed_terms.version = 'cko-platform-terms-1.0.0' + ach = ProcessingDetailsAch() + ach.annual_ach_volume = 100000 + ach.average_ach_transaction_size = 5000 + ach.estimated_monthly_credit_volume = 50000 + ach.average_credit_amount = 2500 + processing_details = ProcessingDetails() + processing_details.annual_processing_volume = 1000 + processing_details.average_transaction_value = 2000 + processing_details.average_order_fulfillment_time = 3 + processing_details.target_countries = ['US'] + processing_details.currency = Currency.USD + processing_details.payments = ProcessingDetailsPayments() + processing_details.payments.ach = ach + contact_phone = Phone() + contact_phone.number = '4155678900' + contact_phone.country_code = 'US' + contact_details = ContactDetails() + contact_details.phone = contact_phone + contact_details.email_addresses = EntityEmailAddresses() + contact_details.email_addresses.primary = 'hannah.bret@example.com' + contact_details.email_addresses.pci_compliance_contact = 'pci.contact@example.com' + profile = Profile() + profile.urls = ['https://www.isv-sole-trader-example.com'] + profile.mccs = ['5551'] + profile.holding_currencies = [Currency.USD] + profile.default_holding_currency = Currency.USD + address = Address() + address.address_line1 = '123 Main Street' + address.city = 'San Francisco' + address.state = 'CA' + address.zip = '94105' + address.country = Country.US + date_of_incorporation = DateOfIncorporation() + date_of_incorporation.year = 2025 + date_of_incorporation.month = 10 + date_of_incorporation.day = 1 + + individual = RepresentativeIndividual() + individual.first_name = 'Hannah' + individual.last_name = 'Bret' + individual.email_address = 'hannah.bret@example.com' + individual.national_id_type = NationalIdType.SSN + individual.national_id_number = '123456789' + individual.date_of_birth = DateOfBirth() + individual.date_of_birth.day = 15 + individual.date_of_birth.month = 1 + individual.date_of_birth.year = 1990 + individual.place_of_birth = PlaceOfBirth() + individual.place_of_birth.country = Country.US + individual.citizenships = [Citizenship()] + individual.citizenships[0].country = Country.US + individual.phone = Phone() + individual.phone.country_code = 'US' + individual.phone.number = '4155678901' + individual.address = address + representative = EntityRepresentative() + representative.roles = [EntityRoles.UBO] + representative.ownership_percentage = 100 + representative.individual = individual + + company = Company() + company.business_type = BusinessType.INDIVIDUAL_OR_SOLE_PROPRIETORSHIP + company.is_registered_company = False + company.trading_name = 'Hannah\'s Goods' + company.date_of_incorporation = date_of_incorporation + company.principal_address = address + company.representatives = [representative] + request = OnboardEntityRequest() + request.reference = 'isv-sole-trader-example001' + request.agreed_terms = agreed_terms + request.seller_category = 'cat_retail_001' + request.processing_details = processing_details + request.contact_details = contact_details + request.profile = profile + request.company = company + + assert _serialize(request) == { + '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', + }, + }, + }, + ], + }, + } + + +class TestEntityFileResponseShape: + """Response-shape tests for POST and GET /entities/{entity_id}/files. + + Python has no typed response classes; ApiClient wraps the parsed JSON in ResponseWrapper, which + wraps nested dicts recursively. Every value is a field-level example from the swagger + (PlatformsFileUploadResponse and PlatformsFileRetrieveResponse). + """ + + def test_exposes_every_upload_response_field(self): + response = ResponseWrapper(None, { + 'id': 'file_6lbss42ezvoufcb2beo76rvwly', + 'maximum_size_in_bytes': 4194304, + 'document_types_for_purpose': ['image/jpeg', 'image/png', 'image/jpg'], + '_links': { + 'upload': { + 'href': 'https://s3.eu-west-1.amazonaws.com/mp-files-api-staging-prod/' + 'ent_ociwguf5a5fe3ndmpnvpnwsi3e/file_6lbss42ezvoufcb2beo76rvwly' + '?AWSAccessKeyId=ASIX4BFJOBCQFLAMPKU3&Expires=1661355993&x-amz-security-token=some_token', + }, + 'self': {'href': 'https://files.checkout.com/files/file_6lbss42ezvoufcb2beo76rvwly'}, + }, + }) + + assert response.id == 'file_6lbss42ezvoufcb2beo76rvwly' + assert response.maximum_size_in_bytes == 4194304 + assert response.document_types_for_purpose == ['image/jpeg', 'image/png', 'image/jpg'] + assert response._links.upload.href == ( + 'https://s3.eu-west-1.amazonaws.com/mp-files-api-staging-prod/ent_ociwguf5a5fe3ndmpnvpnwsi3e/' + 'file_6lbss42ezvoufcb2beo76rvwly?AWSAccessKeyId=ASIX4BFJOBCQFLAMPKU3&Expires=1661355993' + '&x-amz-security-token=some_token') + assert response._links.self.href == 'https://files.checkout.com/files/file_6lbss42ezvoufcb2beo76rvwly' + + def test_exposes_every_retrieve_response_field(self): + response = ResponseWrapper(None, { + 'id': 'file_6lbss42ezvoufcb2beo76rvwly', + 'status': 'invalid', + 'status_reasons': ['InvalidMimeType'], + 'size': 1024, + 'mime_type': 'application/pdf', + 'uploaded_on': '2020-12-01T15:01:01.0000000+00:00', + 'purpose': 'identity_verification', + '_links': { + 'download': { + 'href': 'https://s3.eu-west-1.amazonaws.com/mp-files-api-clean-prod/' + 'ent_ociwguf5a5fe3ndmpnvpnwsi3e/file_6lbss42ezvoufcb2beo76rvwly' + '?X-Amz-Expires=3600&x-amz-security-token=some_token', + }, + 'self': {'href': 'https://files.checkout.com/files/file_6lbss42ezvoufcb2beo76rvwly'}, + }, + }) + + assert response.id == 'file_6lbss42ezvoufcb2beo76rvwly' + assert response.status == 'invalid' + assert response.status_reasons == ['InvalidMimeType'] + assert response.size == 1024 + assert response.mime_type == 'application/pdf' + # The SDK does not parse response dates: the seven fractional digits reach the caller unchanged. + assert response.uploaded_on == '2020-12-01T15:01:01.0000000+00:00' + assert response.purpose == 'identity_verification' + assert response._links.download.href == ( + 'https://s3.eu-west-1.amazonaws.com/mp-files-api-clean-prod/ent_ociwguf5a5fe3ndmpnvpnwsi3e/' + 'file_6lbss42ezvoufcb2beo76rvwly?X-Amz-Expires=3600&x-amz-security-token=some_token') + assert response._links.self.href == 'https://files.checkout.com/files/file_6lbss42ezvoufcb2beo76rvwly'