Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
841 changes: 829 additions & 12 deletions checkout_sdk/accounts/accounts.py

Large diffs are not rendered by default.

42 changes: 41 additions & 1 deletion checkout_sdk/accounts/accounts_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -97,14 +111,17 @@ 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,
entity_id,
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):
Expand Down Expand Up @@ -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())
5 changes: 3 additions & 2 deletions checkout_sdk/common/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'


Expand Down Expand Up @@ -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'
Expand Down
13 changes: 13 additions & 0 deletions checkout_sdk/files/files.py
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion checkout_sdk/issuing/controls.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
18 changes: 16 additions & 2 deletions tests/accounts/accounts_client_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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)
Expand Down
Loading
Loading