diff --git a/Makefile b/Makefile index ec61cc7..fe07ae5 100644 --- a/Makefile +++ b/Makefile @@ -20,6 +20,7 @@ setup: $(MAKE) setup-dev ### Commands to run the tests ### +# base64 encode the credentials: base64 -i tests/test_creds.json test-gen-creds: @poetry run pyrevolut auth-manual --credentials-json tests/test_creds.json diff --git a/README.md b/README.md index 4e8e3f2..1b5f166 100644 --- a/README.md +++ b/README.md @@ -97,11 +97,11 @@ Upon completion, you will have a `.json` file that you can use to authenticate y The SDK currently supports the following APIs: -- [ ] Accounts - - [ ] Retrieve all accounts - - [ ] Retrieve an account - - [ ] Retrieve account's full bank details -- [ ] Cards +- [x] Accounts + - [x] Retrieve all accounts + - [x] Retrieve an account + - [x] Retrieve account's full bank details +- [ ] Cards (Live only) - [ ] Retrieve a list of cards - [ ] Create a card - [ ] Retrieve card details @@ -110,48 +110,48 @@ The SDK currently supports the following APIs: - [ ] Freeze a card - [ ] Unfreeze a card - [ ] Retrieve sensitive card details -- [ ] Counterparties - - [ ] Retrieve a list of counterparties - - [ ] Retrieve a counterparty - - [ ] Delete a counterparty - - [ ] Create a counterparty - - [ ] Validate an account name (CoP) +- [x] Counterparties + - [x] Retrieve a list of counterparties + - [x] Retrieve a counterparty + - [x] Delete a counterparty + - [x] Create a counterparty (Personal) + - [x] Create a counterparty (Business) + - [x] Validate an account name (CoP) - [ ] Foreign exchange - - [ ] Get an exchange rate + - [x] Get an exchange rate - [ ] Exchange money - [ ] Payment drafts - - [ ] Retrieve all payments drafts + - [x] Retrieve all payments drafts - [ ] Create a payment draft - - [ ] Retrieve a payment draft + - [x] Retrieve a payment draft - [ ] Delete a payment draft -- [ ] Payout links - - [ ] Retrieve a list of payout links - - [ ] Retrieve a payout link - - [ ] Create a payout link - - [ ] Cancel a payout link - - [ ] Get transfer reasons -- [ ] Simulations - - [ ] Simulate a transfer state update (Sandbox only) - - [ ] Simulate an account top-up (Sandbox only) -- [ ] Team members +- [x] Payout links + - [x] Retrieve a list of payout links + - [x] Retrieve a payout link + - [x] Create a payout link + - [x] Cancel a payout link +- [ ] Simulations (Sandbox only) + - [ ] Simulate a transfer state update + - [x] Simulate an account top-up +- [ ] Team members (Live only) - [ ] Retrieve a list of team members - [ ] Invite a new memebr to your business - [ ] Retrieve team roles -- [ ] Transactions - - [ ] Retrieve a list of transactions - - [ ] Retrieve a transaction -- [ ] Transfers - - [ ] Move money between your accounts - - [ ] Create a transfer to another account - - [ ] Get transfer reasons -- [ ] Webhooks (v2) - - [ ] Create a new webhook - - [ ] Retrieve a list of webhooks - - [ ] Retrieve a webhook - - [ ] Update a webhook - - [ ] Delete a webhook - - [ ] Rotate a webhook signing secret - - [ ] Retrieve a list of failed webhook events +- [x] Transactions + - [x] Retrieve a list of transactions + - [x] Retrieve a transaction +- [x] Transfers + - [x] Move money between your accounts + - [x] Create a transfer to another account + - [x] Get transfer reasons +- [x] Webhooks (v2) + - [x] Create a new webhook + - [x] Retrieve a list of webhooks + - [x] Retrieve a webhook + - [x] Update a webhook + - [x] Delete a webhook + - [x] Rotate a webhook signing secret + - [x] Retrieve a list of failed webhook events ## **Contributing** diff --git a/pyrevolut/api/__init__.py b/pyrevolut/api/__init__.py index 696b751..9607b97 100644 --- a/pyrevolut/api/__init__.py +++ b/pyrevolut/api/__init__.py @@ -12,3 +12,4 @@ from .transactions import * from .transfers import * from .webhooks import * +from .common import * diff --git a/pyrevolut/api/accounts/endpoint/asynchronous.py b/pyrevolut/api/accounts/endpoint/asynchronous.py index 35c9006..79236c5 100644 --- a/pyrevolut/api/accounts/endpoint/asynchronous.py +++ b/pyrevolut/api/accounts/endpoint/asynchronous.py @@ -100,4 +100,4 @@ async def get_full_bank_details( **kwargs, ) - return endpoint.Response(**response.json()).model_dump() + return endpoint.Response(**response.json()[0]).model_dump() diff --git a/pyrevolut/api/accounts/endpoint/synchronous.py b/pyrevolut/api/accounts/endpoint/synchronous.py index 9a58f58..68b041b 100644 --- a/pyrevolut/api/accounts/endpoint/synchronous.py +++ b/pyrevolut/api/accounts/endpoint/synchronous.py @@ -100,4 +100,4 @@ def get_full_bank_details( **kwargs, ) - return endpoint.Response(**response.json()).model_dump() + return endpoint.Response(**response.json()[0]).model_dump() diff --git a/pyrevolut/api/accounts/get/retrieve_all_accounts.py b/pyrevolut/api/accounts/get/retrieve_all_accounts.py index 608a3cf..7452111 100644 --- a/pyrevolut/api/accounts/get/retrieve_all_accounts.py +++ b/pyrevolut/api/accounts/get/retrieve_all_accounts.py @@ -8,7 +8,7 @@ class RetrieveAllAccounts: Get a list of all your accounts. """ - ROUTE = "/accounts" + ROUTE = "/1.0/accounts" class Params(BaseModel): """ diff --git a/pyrevolut/api/accounts/get/retrieve_an_account.py b/pyrevolut/api/accounts/get/retrieve_an_account.py index 3736855..220c2b8 100644 --- a/pyrevolut/api/accounts/get/retrieve_an_account.py +++ b/pyrevolut/api/accounts/get/retrieve_an_account.py @@ -8,7 +8,7 @@ class RetrieveAnAccount: Get the information about one of your accounts. Specify the account by its ID. """ - ROUTE = "/accounts/{account_id}" + ROUTE = "/1.0/accounts/{account_id}" class Params(BaseModel): """ diff --git a/pyrevolut/api/accounts/get/retrieve_full_bank_details.py b/pyrevolut/api/accounts/get/retrieve_full_bank_details.py index 5c93df1..0ecbda1 100644 --- a/pyrevolut/api/accounts/get/retrieve_full_bank_details.py +++ b/pyrevolut/api/accounts/get/retrieve_full_bank_details.py @@ -11,7 +11,7 @@ class RetrieveFullBankDetails: Get all the bank details of one of your accounts. Specify the account by its ID. """ - ROUTE = "/accounts/{account_id}/bank-details" + ROUTE = "/1.0/accounts/{account_id}/bank-details" class Params(BaseModel): """ @@ -31,19 +31,19 @@ class ModelBeneficiaryAddress(BaseModel): street_line1: Annotated[ str | None, Field(description="Street line 1 information."), - ] + ] = None street_line2: Annotated[ str | None, Field(description="Street line 2 information."), - ] + ] = None region: Annotated[ str | None, Field(description="The name of the region."), - ] + ] = None city: Annotated[ str | None, Field(description="The name of the city."), - ] + ] = None country: Annotated[ CountryAlpha2, Field(description="The country of the counterparty as the 2-letter ISO 3166 code."), @@ -65,32 +65,32 @@ class ModelEstimatedTime(BaseModel): min: Annotated[ int | None, Field(description="The minimum time estimate.", ge=0), - ] + ] = None max: Annotated[ int | None, Field(description="The maximum time estimate.", ge=0), - ] + ] = None iban: Annotated[ str | None, Field(description="The IBAN number."), - ] + ] = None bic: Annotated[ str | None, Field(description="The BIC number, also known as SWIFT code."), - ] + ] = None account_no: Annotated[ str | None, Field(description="The account number."), - ] + ] = None sort_code: Annotated[ str | None, Field(description="The sort code of the account."), - ] + ] = None routing_number: Annotated[ str | None, Field(description="The routing number of the account."), - ] + ] = None beneficiary: Annotated[ str, Field(description="The name of the counterparty."), @@ -102,15 +102,15 @@ class ModelEstimatedTime(BaseModel): bank_country: Annotated[ CountryAlpha2 | None, Field(description="The country of the bank as the 2-letter ISO 3166 code."), - ] + ] = None pooled: Annotated[ bool | None, Field(description="Indicates whether the account address is pooled or unique."), - ] + ] = None unique_reference: Annotated[ str | None, Field(description="The reference of the pooled account."), - ] + ] = None schemes: Annotated[ list[EnumPaymentScheme], Field(description="The schemes that are available for this currency account."), diff --git a/pyrevolut/api/cards/endpoint/asynchronous.py b/pyrevolut/api/cards/endpoint/asynchronous.py index 18843ce..fb17569 100644 --- a/pyrevolut/api/cards/endpoint/asynchronous.py +++ b/pyrevolut/api/cards/endpoint/asynchronous.py @@ -56,6 +56,7 @@ async def get_all_cards( list The list of all cards in your organisation. """ + self.__check_sandbox() endpoint = RetrieveListOfCards path = endpoint.ROUTE params = endpoint.Params( @@ -89,6 +90,7 @@ async def get_card( dict The details of the card. """ + self.__check_sandbox() endpoint = RetrieveCardDetails path = endpoint.ROUTE.format(card_id=card_id) params = endpoint.Params() @@ -120,6 +122,7 @@ async def get_card_sensitive_details( dict The sensitive details of the card. """ + self.__check_sandbox() endpoint = RetrieveSensitiveCardDetails path = endpoint.ROUTE.format(card_id=card_id) params = endpoint.Params() @@ -213,6 +216,7 @@ async def create_card( dict The details of the created card. """ + self.__check_sandbox() endpoint = CreateCard path = endpoint.ROUTE @@ -314,6 +318,7 @@ async def freeze_card( dict An empty dictionary. """ + self.__check_sandbox() endpoint = FreezeCard path = endpoint.ROUTE.format(card_id=card_id) body = endpoint.Body() @@ -348,6 +353,7 @@ async def unfreeze_card( dict An empty dictionary. """ + self.__check_sandbox() endpoint = UnfreezeCard path = endpoint.ROUTE.format(card_id=card_id) body = endpoint.Body() @@ -442,6 +448,7 @@ async def update_card( dict The updated details of the card. """ + self.__check_sandbox() endpoint = UpdateCardDetails path = endpoint.ROUTE.format(card_id=card_id) @@ -544,6 +551,7 @@ async def delete_card( dict An empty dictionary. """ + self.__check_sandbox() endpoint = TerminateCard path = endpoint.ROUTE.format(card_id=card_id) params = endpoint.Params() @@ -573,3 +581,15 @@ def __process_limit_model( elif amount == "null" and currency == "null": return "null" return None + + def __check_sandbox(self): + """ + Check if the sandbox is enabled. + + Raises + ------ + ValueError + If the sandbox is enabled. + """ + if self.client.sandbox: + raise ValueError("This feature is not available in Sandbox.") diff --git a/pyrevolut/api/cards/endpoint/synchronous.py b/pyrevolut/api/cards/endpoint/synchronous.py index ea0055e..3883a98 100644 --- a/pyrevolut/api/cards/endpoint/synchronous.py +++ b/pyrevolut/api/cards/endpoint/synchronous.py @@ -56,6 +56,7 @@ def get_all_cards( list The list of all cards in your organisation. """ + self.__check_sandbox() endpoint = RetrieveListOfCards path = endpoint.ROUTE params = endpoint.Params( @@ -89,6 +90,7 @@ def get_card( dict The details of the card. """ + self.__check_sandbox() endpoint = RetrieveCardDetails path = endpoint.ROUTE.format(card_id=card_id) params = endpoint.Params() @@ -120,6 +122,7 @@ def get_card_sensitive_details( dict The sensitive details of the card. """ + self.__check_sandbox() endpoint = RetrieveSensitiveCardDetails path = endpoint.ROUTE.format(card_id=card_id) params = endpoint.Params() @@ -213,6 +216,7 @@ def create_card( dict The details of the created card. """ + self.__check_sandbox() endpoint = CreateCard path = endpoint.ROUTE @@ -314,6 +318,7 @@ def freeze_card( dict An empty dictionary. """ + self.__check_sandbox() endpoint = FreezeCard path = endpoint.ROUTE.format(card_id=card_id) body = endpoint.Body() @@ -348,6 +353,7 @@ def unfreeze_card( dict An empty dictionary. """ + self.__check_sandbox() endpoint = UnfreezeCard path = endpoint.ROUTE.format(card_id=card_id) body = endpoint.Body() @@ -442,6 +448,7 @@ def update_card( dict The updated details of the card. """ + self.__check_sandbox() endpoint = UpdateCardDetails path = endpoint.ROUTE.format(card_id=card_id) @@ -544,6 +551,7 @@ def delete_card( dict An empty dictionary. """ + self.__check_sandbox() endpoint = TerminateCard path = endpoint.ROUTE.format(card_id=card_id) params = endpoint.Params() @@ -573,3 +581,15 @@ def __process_limit_model( elif amount == "null" and currency == "null": return "null" return None + + def __check_sandbox(self): + """ + Check if the sandbox is enabled. + + Raises + ------ + ValueError + If the sandbox is enabled. + """ + if self.client.sandbox: + raise ValueError("This feature is not available in Sandbox.") diff --git a/pyrevolut/api/cards/get/retrieve_card_details.py b/pyrevolut/api/cards/get/retrieve_card_details.py index 5e78ce2..073d66b 100644 --- a/pyrevolut/api/cards/get/retrieve_card_details.py +++ b/pyrevolut/api/cards/get/retrieve_card_details.py @@ -8,7 +8,7 @@ class RetrieveCardDetails: Get the details of a specific card, based on its ID. """ - ROUTE = "/cards/{card_id}" + ROUTE = "/1.0/cards/{card_id}" class Params(BaseModel): """ diff --git a/pyrevolut/api/cards/get/retrieve_list_of_cards.py b/pyrevolut/api/cards/get/retrieve_list_of_cards.py index d55989e..8d3193d 100644 --- a/pyrevolut/api/cards/get/retrieve_list_of_cards.py +++ b/pyrevolut/api/cards/get/retrieve_list_of_cards.py @@ -12,7 +12,7 @@ class RetrieveListOfCards: The results are paginated and sorted by the created_at date in reverse chronological order. """ - ROUTE = "/cards" + ROUTE = "/1.0/cards" class Params(BaseModel): """ diff --git a/pyrevolut/api/cards/get/retrieve_sensitive_card_details.py b/pyrevolut/api/cards/get/retrieve_sensitive_card_details.py index 93bb35c..0180859 100644 --- a/pyrevolut/api/cards/get/retrieve_sensitive_card_details.py +++ b/pyrevolut/api/cards/get/retrieve_sensitive_card_details.py @@ -11,7 +11,7 @@ class RetrieveSensitiveCardDetails: Requires the READ_SENSITIVE_CARD_DATA token scope. """ - ROUTE = "/cards/{card_id}/sensitive-details" + ROUTE = "/1.0/cards/{card_id}/sensitive-details" class Params(BaseModel): """ diff --git a/pyrevolut/api/cards/patch/update_card_details.py b/pyrevolut/api/cards/patch/update_card_details.py index 21d1949..d858eb3 100644 --- a/pyrevolut/api/cards/patch/update_card_details.py +++ b/pyrevolut/api/cards/patch/update_card_details.py @@ -12,7 +12,7 @@ class UpdateCardDetails: Updating a spending limit does not reset the spending counter. """ - ROUTE = "/cards/{card_id}" + ROUTE = "/1.0/cards/{card_id}" class Body(BaseModel): """ diff --git a/pyrevolut/api/cards/post/create_card.py b/pyrevolut/api/cards/post/create_card.py index 4aec956..c5dcdd0 100644 --- a/pyrevolut/api/cards/post/create_card.py +++ b/pyrevolut/api/cards/post/create_card.py @@ -15,7 +15,7 @@ class CreateCard: To create a physical card, use the Revolut Business app. """ - ROUTE = "/cards" + ROUTE = "/1.0/cards" class Body(BaseModel): """ diff --git a/pyrevolut/api/cards/post/freeze_card.py b/pyrevolut/api/cards/post/freeze_card.py index 2130e30..15315e0 100644 --- a/pyrevolut/api/cards/post/freeze_card.py +++ b/pyrevolut/api/cards/post/freeze_card.py @@ -10,7 +10,7 @@ class FreezeCard: and no content is returned in the response. """ - ROUTE = "/cards/{card_id}/freeze" + ROUTE = "/1.0/cards/{card_id}/freeze" class Body(BaseModel): """ diff --git a/pyrevolut/api/cards/post/unfreeze_card.py b/pyrevolut/api/cards/post/unfreeze_card.py index 31b74d2..a6d0ebd 100644 --- a/pyrevolut/api/cards/post/unfreeze_card.py +++ b/pyrevolut/api/cards/post/unfreeze_card.py @@ -10,7 +10,7 @@ class UnfreezeCard: and no content is returned in the response. """ - ROUTE = "/cards/{card_id}/unfreeze" + ROUTE = "/1.0/cards/{card_id}/unfreeze" class Body(BaseModel): """ diff --git a/pyrevolut/api/common/enums/transaction_state.py b/pyrevolut/api/common/enums/transaction_state.py index f6ef6dd..b6a18b2 100644 --- a/pyrevolut/api/common/enums/transaction_state.py +++ b/pyrevolut/api/common/enums/transaction_state.py @@ -23,6 +23,8 @@ class EnumTransactionState(StrEnum): reverted: The transaction was reverted. This can happen for a variety of reasons, for example, the receiver being inaccessible. + cancelled: + The transaction was cancelled. """ CREATED = "created" @@ -31,3 +33,4 @@ class EnumTransactionState(StrEnum): DECLINED = "declined" FAILED = "failed" REVERTED = "reverted" + CANCELLED = "cancelled" diff --git a/pyrevolut/api/common/enums/transaction_type.py b/pyrevolut/api/common/enums/transaction_type.py index 179ba78..7d1975a 100644 --- a/pyrevolut/api/common/enums/transaction_type.py +++ b/pyrevolut/api/common/enums/transaction_type.py @@ -18,3 +18,4 @@ class EnumTransactionType(StrEnum): TOPUP_RETURN = "topup_return" TAX = "tax" TAX_REFUND = "tax_refund" + TEMP_BLOCK = "temp_block" diff --git a/pyrevolut/api/common/enums/transfer_reason_code.py b/pyrevolut/api/common/enums/transfer_reason_code.py index eaf0408..483a8a7 100644 --- a/pyrevolut/api/common/enums/transfer_reason_code.py +++ b/pyrevolut/api/common/enums/transfer_reason_code.py @@ -33,3 +33,6 @@ class EnumTransferReasonCode(StrEnum): TRANSPORTATION = "transportation" TRAVEL = "travel" UTILITIES = "utilities" + DONATIONS = "donations" + FAMILY_SUPPORT = "family_support" + SALARY = "salary" diff --git a/pyrevolut/api/counterparties/delete/delete_counterparty.py b/pyrevolut/api/counterparties/delete/delete_counterparty.py index 77ca0f6..643dc7b 100644 --- a/pyrevolut/api/counterparties/delete/delete_counterparty.py +++ b/pyrevolut/api/counterparties/delete/delete_counterparty.py @@ -6,7 +6,7 @@ class DeleteCounterparty: When a counterparty is deleted, you cannot make any payments to the counterparty. """ - ROUTE = "/counterparty/{counterparty_id}" + ROUTE = "/1.0/counterparty/{counterparty_id}" class Params(BaseModel): """ diff --git a/pyrevolut/api/counterparties/endpoint/asynchronous.py b/pyrevolut/api/counterparties/endpoint/asynchronous.py index fe4c2bf..7a6470d 100644 --- a/pyrevolut/api/counterparties/endpoint/asynchronous.py +++ b/pyrevolut/api/counterparties/endpoint/asynchronous.py @@ -114,7 +114,7 @@ async def get_counterparty( **kwargs, ) - return endpoint.Response(**response.json()) + return endpoint.Response(**response.json()).model_dump() async def create_counterparty( self, @@ -221,7 +221,9 @@ async def create_counterparty( individual_name=endpoint.Body.ModelIndividualName( first_name=individual_first_name, last_name=individual_last_name, - ), + ) + if individual_first_name is not None or individual_last_name is not None + else None, bank_country=bank_country, currency=currency, revtag=revtag, @@ -240,7 +242,9 @@ async def create_counterparty( city=address_city, country=address_country, postcode=address_postcode, - ), + ) + if address_country is not None and address_postcode is not None + else None, ) response = await self.client.post( @@ -249,7 +253,7 @@ async def create_counterparty( **kwargs, ) - return endpoint.Response(**response.json()) + return endpoint.Response(**response.json()).model_dump() async def validate_account_name( self, @@ -309,7 +313,9 @@ async def validate_account_name( individual_name=endpoint.Body.ModelIndividualName( first_name=individual_first_name, last_name=individual_last_name, - ), + ) + if individual_first_name is not None or individual_last_name is not None + else None, ) response = await self.client.post( @@ -318,7 +324,7 @@ async def validate_account_name( **kwargs, ) - return endpoint.Response(**response.json()) + return endpoint.Response(**response.json()).model_dump() async def delete_counterparty( self, diff --git a/pyrevolut/api/counterparties/endpoint/synchronous.py b/pyrevolut/api/counterparties/endpoint/synchronous.py index 3f8795c..952d92c 100644 --- a/pyrevolut/api/counterparties/endpoint/synchronous.py +++ b/pyrevolut/api/counterparties/endpoint/synchronous.py @@ -114,7 +114,7 @@ def get_counterparty( **kwargs, ) - return endpoint.Response(**response.json()) + return endpoint.Response(**response.json()).model_dump() def create_counterparty( self, @@ -221,7 +221,9 @@ def create_counterparty( individual_name=endpoint.Body.ModelIndividualName( first_name=individual_first_name, last_name=individual_last_name, - ), + ) + if individual_first_name is not None or individual_last_name is not None + else None, bank_country=bank_country, currency=currency, revtag=revtag, @@ -240,7 +242,9 @@ def create_counterparty( city=address_city, country=address_country, postcode=address_postcode, - ), + ) + if address_country is not None and address_postcode is not None + else None, ) response = self.client.post( @@ -249,7 +253,7 @@ def create_counterparty( **kwargs, ) - return endpoint.Response(**response.json()) + return endpoint.Response(**response.json()).model_dump() def validate_account_name( self, @@ -309,7 +313,9 @@ def validate_account_name( individual_name=endpoint.Body.ModelIndividualName( first_name=individual_first_name, last_name=individual_last_name, - ), + ) + if individual_first_name is not None or individual_last_name is not None + else None, ) response = self.client.post( @@ -318,7 +324,7 @@ def validate_account_name( **kwargs, ) - return endpoint.Response(**response.json()) + return endpoint.Response(**response.json()).model_dump() def delete_counterparty( self, diff --git a/pyrevolut/api/counterparties/get/retrieve_counterparty.py b/pyrevolut/api/counterparties/get/retrieve_counterparty.py index 5912bd1..b554691 100644 --- a/pyrevolut/api/counterparties/get/retrieve_counterparty.py +++ b/pyrevolut/api/counterparties/get/retrieve_counterparty.py @@ -6,7 +6,7 @@ class RetrieveCounterparty: """Get the information about a specific counterparty by ID.""" - ROUTE = "/counterparties/{counterparty_id}" + ROUTE = "/1.0/counterparty/{counterparty_id}" class Params(BaseModel): """ diff --git a/pyrevolut/api/counterparties/get/retrieve_list_of_counterparties.py b/pyrevolut/api/counterparties/get/retrieve_list_of_counterparties.py index dd5f7a6..a35b89f 100644 --- a/pyrevolut/api/counterparties/get/retrieve_list_of_counterparties.py +++ b/pyrevolut/api/counterparties/get/retrieve_list_of_counterparties.py @@ -17,7 +17,7 @@ class RetrieveListOfCounterparties: created_at date of the last counterparty returned in the previous response. """ - ROUTE = "/counterparties" + ROUTE = "/1.0/counterparties" class Params(BaseModel): """ diff --git a/pyrevolut/api/counterparties/post/create_counterparty.py b/pyrevolut/api/counterparties/post/create_counterparty.py index 3d66397..d8cef59 100644 --- a/pyrevolut/api/counterparties/post/create_counterparty.py +++ b/pyrevolut/api/counterparties/post/create_counterparty.py @@ -22,7 +22,7 @@ class CreateCounterparty: Test User 9 & john9pvki """ - ROUTE = "/counterparty" + ROUTE = "/1.0/counterparty" class Body(BaseModel): """Request body for the endpoint.""" @@ -217,10 +217,10 @@ def check_inputs(self) -> "CreateCounterparty.Body": ), "revtag is not required when profile_type is not specified." # Individual name check - if self.company_name is None: - assert ( - self.individual_name is not None - ), "individual_name is required when company_name is not specified." + # if self.company_name is None: + # assert ( + # self.individual_name is not None + # ), "individual_name is required when company_name is not specified." # Sort code check if self.currency == "GBP": diff --git a/pyrevolut/api/counterparties/post/validate_account_name.py b/pyrevolut/api/counterparties/post/validate_account_name.py index a4a524e..d190348 100644 --- a/pyrevolut/api/counterparties/post/validate_account_name.py +++ b/pyrevolut/api/counterparties/post/validate_account_name.py @@ -31,7 +31,7 @@ class ValidateAccountName: This functionality is only available to UK-based businesses. """ - ROUTE = "/account-name-validation" + ROUTE = "/1.0/account-name-validation" class Body(BaseModel): """ @@ -200,16 +200,3 @@ class ModelReason(BaseModel): description="The name of the individual counterparty. Use when company_name is not specified.", ), ] = None - - @model_validator(mode="after") - def check_inputs(self) -> "ValidateAccountName.Body": - """ - Ensure that either the individual_name or company_name is provided. - """ - if not self.company_name and not self.individual_name: - raise ValueError("You must provide either the company_name or individual_name.") - if self.company_name and self.individual_name: - raise ValueError( - "You must provide either the company_name or individual_name, not both." - ) - return self diff --git a/pyrevolut/api/foreign_exchange/endpoint/asynchronous.py b/pyrevolut/api/foreign_exchange/endpoint/asynchronous.py index e610c5a..3195aa7 100644 --- a/pyrevolut/api/foreign_exchange/endpoint/asynchronous.py +++ b/pyrevolut/api/foreign_exchange/endpoint/asynchronous.py @@ -52,7 +52,7 @@ async def get_exchange_rate( **kwargs, ) - return endpoint.Response(**response.json()) + return endpoint.Response(**response.json()).model_dump() async def exchange_money( self, @@ -132,4 +132,4 @@ async def exchange_money( **kwargs, ) - return endpoint.Response(**response.json()) + return endpoint.Response(**response.json()).model_dump() diff --git a/pyrevolut/api/foreign_exchange/endpoint/synchronous.py b/pyrevolut/api/foreign_exchange/endpoint/synchronous.py index 64a4c11..97ab2ea 100644 --- a/pyrevolut/api/foreign_exchange/endpoint/synchronous.py +++ b/pyrevolut/api/foreign_exchange/endpoint/synchronous.py @@ -52,7 +52,7 @@ def get_exchange_rate( **kwargs, ) - return endpoint.Response(**response.json()) + return endpoint.Response(**response.json()).model_dump() def exchange_money( self, @@ -132,4 +132,4 @@ def exchange_money( **kwargs, ) - return endpoint.Response(**response.json()) + return endpoint.Response(**response.json()).model_dump() diff --git a/pyrevolut/api/foreign_exchange/get/get_exchange_rate.py b/pyrevolut/api/foreign_exchange/get/get_exchange_rate.py index 5db753f..8b68057 100644 --- a/pyrevolut/api/foreign_exchange/get/get_exchange_rate.py +++ b/pyrevolut/api/foreign_exchange/get/get_exchange_rate.py @@ -1,7 +1,7 @@ from typing import Annotated from decimal import Decimal -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, ConfigDict from pydantic_extra_types.currency_code import Currency from pyrevolut.api.foreign_exchange.resources import ResourceForeignExchange @@ -12,13 +12,18 @@ class GetExchangeRate: Get the sell exchange rate between two currencies. """ - ROUTE = "/rate" + ROUTE = "/1.0/rate" class Params(BaseModel): """ Query parameters for the endpoint. """ + model_config = ConfigDict( + populate_by_name=True, + from_attributes=True, + ) + from_: Annotated[ Currency, Field( diff --git a/pyrevolut/api/foreign_exchange/post/exchange_money.py b/pyrevolut/api/foreign_exchange/post/exchange_money.py index 0270fdf..3b60735 100644 --- a/pyrevolut/api/foreign_exchange/post/exchange_money.py +++ b/pyrevolut/api/foreign_exchange/post/exchange_money.py @@ -2,7 +2,7 @@ from uuid import UUID from decimal import Decimal -from pydantic import BaseModel, Field, model_validator +from pydantic import BaseModel, Field, model_validator, ConfigDict from pydantic_extra_types.currency_code import Currency from pyrevolut.utils import DateTime @@ -24,13 +24,18 @@ class ExchangeMoney: Specify the amount in the to object. """ - ROUTE = "/exchange" + ROUTE = "/1.0/exchange" class Body(BaseModel): """ The request body for the endpoint. """ + model_config = ConfigDict( + populate_by_name=True, + from_attributes=True, + ) + class ModelFrom(BaseModel): """The details of the currency to exchange from.""" diff --git a/pyrevolut/api/foreign_exchange/resources/foreign_exchange.py b/pyrevolut/api/foreign_exchange/resources/foreign_exchange.py index 3cb500c..ce4ae12 100644 --- a/pyrevolut/api/foreign_exchange/resources/foreign_exchange.py +++ b/pyrevolut/api/foreign_exchange/resources/foreign_exchange.py @@ -1,7 +1,7 @@ from typing import Annotated from decimal import Decimal -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, ConfigDict from pyrevolut.utils import DateTime from pyrevolut.api.common import ModelBaseAmount @@ -12,6 +12,11 @@ class ResourceForeignExchange(BaseModel): Foreign Exchange resource model. """ + model_config = ConfigDict( + populate_by_name=True, + from_attributes=True, + ) + class ModelFrom(ModelBaseAmount): """ The money to sell. diff --git a/pyrevolut/api/payment_drafts/delete/delete_payment_draft.py b/pyrevolut/api/payment_drafts/delete/delete_payment_draft.py index 4ce40d1..e0ca13e 100644 --- a/pyrevolut/api/payment_drafts/delete/delete_payment_draft.py +++ b/pyrevolut/api/payment_drafts/delete/delete_payment_draft.py @@ -7,7 +7,7 @@ class DeletePaymentDraft: You can delete a payment draft only if it isn't processed. """ - ROUTE = "/payment-drafts/{payment_draft_id}" + ROUTE = "/1.0/payment-drafts/{payment_draft_id}" class Params(BaseModel): """ diff --git a/pyrevolut/api/payment_drafts/endpoint/asynchronous.py b/pyrevolut/api/payment_drafts/endpoint/asynchronous.py index 150607e..d6c801d 100644 --- a/pyrevolut/api/payment_drafts/endpoint/asynchronous.py +++ b/pyrevolut/api/payment_drafts/endpoint/asynchronous.py @@ -47,7 +47,7 @@ async def get_all_payment_drafts( **kwargs, ) - return endpoint.Response(**response.json()) + return endpoint.Response(**response.json()).model_dump() async def get_payment_draft( self, @@ -77,7 +77,7 @@ async def get_payment_draft( **kwargs, ) - return endpoint.Response(**response.json()) + return endpoint.Response(**response.json()).model_dump() async def create_payment_draft( self, @@ -176,7 +176,7 @@ async def create_payment_draft( **kwargs, ) - return endpoint.Response(**response.json()) + return endpoint.Response(**response.json()).model_dump() async def delete_payment_draft( self, @@ -207,4 +207,4 @@ async def delete_payment_draft( **kwargs, ) - return endpoint.Response(**response.json()) + return endpoint.Response(**response.json()).model_dump() diff --git a/pyrevolut/api/payment_drafts/endpoint/synchronous.py b/pyrevolut/api/payment_drafts/endpoint/synchronous.py index a64a34e..878d103 100644 --- a/pyrevolut/api/payment_drafts/endpoint/synchronous.py +++ b/pyrevolut/api/payment_drafts/endpoint/synchronous.py @@ -47,7 +47,7 @@ def get_all_payment_drafts( **kwargs, ) - return endpoint.Response(**response.json()) + return endpoint.Response(**response.json()).model_dump() def get_payment_draft( self, @@ -77,7 +77,7 @@ def get_payment_draft( **kwargs, ) - return endpoint.Response(**response.json()) + return endpoint.Response(**response.json()).model_dump() def create_payment_draft( self, @@ -176,7 +176,7 @@ def create_payment_draft( **kwargs, ) - return endpoint.Response(**response.json()) + return endpoint.Response(**response.json()).model_dump() def delete_payment_draft( self, @@ -207,4 +207,4 @@ def delete_payment_draft( **kwargs, ) - return endpoint.Response(**response.json()) + return endpoint.Response(**response.json()).model_dump() diff --git a/pyrevolut/api/payment_drafts/get/retrieve_all_payment_drafts.py b/pyrevolut/api/payment_drafts/get/retrieve_all_payment_drafts.py index 8cf3ca5..9f875b8 100644 --- a/pyrevolut/api/payment_drafts/get/retrieve_all_payment_drafts.py +++ b/pyrevolut/api/payment_drafts/get/retrieve_all_payment_drafts.py @@ -11,7 +11,7 @@ class RetrieveAllPaymentDrafts: Get a list of all the payment drafts that aren't processed. """ - ROUTE = "/payment-drafts" + ROUTE = "/1.0/payment-drafts" class Params(BaseModel): """ diff --git a/pyrevolut/api/payment_drafts/get/retrieve_payment_draft.py b/pyrevolut/api/payment_drafts/get/retrieve_payment_draft.py index cf6293d..56e4d6a 100644 --- a/pyrevolut/api/payment_drafts/get/retrieve_payment_draft.py +++ b/pyrevolut/api/payment_drafts/get/retrieve_payment_draft.py @@ -2,7 +2,7 @@ from uuid import UUID from decimal import Decimal -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, ConfigDict from pydantic_extra_types.currency_code import Currency from pyrevolut.api.common import ModelBaseAmount, EnumPaymentDraftState @@ -14,7 +14,7 @@ class RetrievePaymentDraft: Get the information about a specific payment draft by ID. """ - ROUTE = "/payment-drafts/{payment_draft_id}" + ROUTE = "/1.0/payment-drafts/{payment_draft_id}" class Params(BaseModel): """ @@ -75,6 +75,11 @@ class ModelReceiver(BaseModel): class ModelCurrentChargeOptions(BaseModel): """The explanation of conversion process""" + model_config = ConfigDict( + populate_by_name=True, + from_attributes=True, + ) + class ModelFrom(ModelBaseAmount): """The source of the conversion""" diff --git a/pyrevolut/api/payment_drafts/post/create_payment_draft.py b/pyrevolut/api/payment_drafts/post/create_payment_draft.py index 690d6ec..3ffaa3a 100644 --- a/pyrevolut/api/payment_drafts/post/create_payment_draft.py +++ b/pyrevolut/api/payment_drafts/post/create_payment_draft.py @@ -13,7 +13,7 @@ class CreatePaymentDraft: Create a payment draft. """ - ROUTE = "/payment-drafts" + ROUTE = "/1.0/payment-drafts" class Body(BaseModel): """ diff --git a/pyrevolut/api/payout_links/endpoint/synchronous.py b/pyrevolut/api/payout_links/endpoint/synchronous.py index baf814b..67db77f 100644 --- a/pyrevolut/api/payout_links/endpoint/synchronous.py +++ b/pyrevolut/api/payout_links/endpoint/synchronous.py @@ -176,6 +176,8 @@ def create_payout_link( A reference for the payment. payout_methods : list[EnumPayoutLinkPaymentMethod] The payout methods that the recipient can use to claim the payment. + If not provided, the default value is + [EnumPayoutLinkPaymentMethod.REVOLUT, EnumPayoutLinkPaymentMethod.BANK_ACCOUNT]. save_counterparty : bool, optional Indicates whether to save the recipient as your counterparty upon link claim. If false then the counterparty will not show up on your counterparties list, diff --git a/pyrevolut/api/payout_links/get/retrieve_list_of_payout_links.py b/pyrevolut/api/payout_links/get/retrieve_list_of_payout_links.py index c07e702..2f07aa2 100644 --- a/pyrevolut/api/payout_links/get/retrieve_list_of_payout_links.py +++ b/pyrevolut/api/payout_links/get/retrieve_list_of_payout_links.py @@ -22,7 +22,7 @@ class RetrieveListOfPayoutLinks: This feature is available in the UK and the EEA. """ - ROUTE = "/payout-links" + ROUTE = "/1.0/payout-links" class Params(BaseModel): """ diff --git a/pyrevolut/api/payout_links/get/retrieve_payout_link.py b/pyrevolut/api/payout_links/get/retrieve_payout_link.py index 3c3da91..f93b518 100644 --- a/pyrevolut/api/payout_links/get/retrieve_payout_link.py +++ b/pyrevolut/api/payout_links/get/retrieve_payout_link.py @@ -12,7 +12,7 @@ class RetrievePayoutLink: This feature is available in the UK and the EEA. """ - ROUTE = "/payout-links/{payout_link_id}" + ROUTE = "/1.0/payout-links/{payout_link_id}" class Params(BaseModel): """ diff --git a/pyrevolut/api/payout_links/post/cancel_payout_link.py b/pyrevolut/api/payout_links/post/cancel_payout_link.py index 491e198..6848748 100644 --- a/pyrevolut/api/payout_links/post/cancel_payout_link.py +++ b/pyrevolut/api/payout_links/post/cancel_payout_link.py @@ -12,7 +12,7 @@ class CancelPayoutLink: This feature is available in the UK and the EEA. """ - ROUTE = "/payout-links/{payout_link_id}/cancel" + ROUTE = "/1.0/payout-links/{payout_link_id}/cancel" class Body(BaseModel): """ diff --git a/pyrevolut/api/payout_links/post/create_payout_link.py b/pyrevolut/api/payout_links/post/create_payout_link.py index 7890513..f863e96 100644 --- a/pyrevolut/api/payout_links/post/create_payout_link.py +++ b/pyrevolut/api/payout_links/post/create_payout_link.py @@ -22,7 +22,7 @@ class CreatePayoutLink: This feature is available in the UK and the EEA. """ - ROUTE = "/payout-links" + ROUTE = "/1.0/payout-links" class Body(BaseModel): """ diff --git a/pyrevolut/api/payout_links/resources/payout_link.py b/pyrevolut/api/payout_links/resources/payout_link.py index a3cc613..9e9d949 100644 --- a/pyrevolut/api/payout_links/resources/payout_link.py +++ b/pyrevolut/api/payout_links/resources/payout_link.py @@ -5,7 +5,7 @@ from pydantic import BaseModel, Field, HttpUrl from pydantic_extra_types.currency_code import Currency -from pyrevolut.utils import DateTime, Date +from pyrevolut.utils import DateTime from pyrevolut.api.common import ( EnumPayoutLinkState, EnumPayoutLinkPaymentMethod, @@ -88,8 +88,8 @@ class ResourcePayoutLink(BaseModel): Field(description="The ID of the request, provided by the sender.", max_length=40), ] expiry_date: Annotated[ - Date, - Field(description="The date the payout link expires in ISO 8601 format."), + DateTime, + Field(description="The datetime the payout link expires in ISO 8601 format."), ] payout_methods: Annotated[ list[EnumPayoutLinkPaymentMethod], diff --git a/pyrevolut/api/simulations/endpoint/asynchronous.py b/pyrevolut/api/simulations/endpoint/asynchronous.py index 991702d..c36f3e8 100644 --- a/pyrevolut/api/simulations/endpoint/asynchronous.py +++ b/pyrevolut/api/simulations/endpoint/asynchronous.py @@ -69,6 +69,7 @@ async def simulate_account_topup( dict The top-up transaction information. """ + self.__check_sandbox() endpoint = SimulateAccountTopup path = endpoint.ROUTE body = endpoint.Body( @@ -122,6 +123,7 @@ async def simulate_transfer_state_update( dict The updated transfer information. """ + self.__check_sandbox() endpoint = SimulateTransferStateUpdate path = endpoint.ROUTE.format(transfer_id=transfer_id, action=action) body = endpoint.Body() @@ -133,3 +135,15 @@ async def simulate_transfer_state_update( ) return endpoint.Response(**response.json()).model_dump() + + def __check_sandbox(self): + """ + Check if the sandbox is enabled. + + Raises + ------ + ValueError + If the sandbox is enabled. + """ + if not self.client.sandbox: + raise ValueError("This feature is only available in the Sandbox.") diff --git a/pyrevolut/api/simulations/endpoint/synchronous.py b/pyrevolut/api/simulations/endpoint/synchronous.py index 89d7e6e..89763ca 100644 --- a/pyrevolut/api/simulations/endpoint/synchronous.py +++ b/pyrevolut/api/simulations/endpoint/synchronous.py @@ -69,6 +69,7 @@ def simulate_account_topup( dict The top-up transaction information. """ + self.__check_sandbox() endpoint = SimulateAccountTopup path = endpoint.ROUTE body = endpoint.Body( @@ -122,6 +123,7 @@ def simulate_transfer_state_update( dict The updated transfer information. """ + self.__check_sandbox() endpoint = SimulateTransferStateUpdate path = endpoint.ROUTE.format(transfer_id=transfer_id, action=action) body = endpoint.Body() @@ -133,3 +135,15 @@ def simulate_transfer_state_update( ) return endpoint.Response(**response.json()).model_dump() + + def __check_sandbox(self): + """ + Check if the sandbox is enabled. + + Raises + ------ + ValueError + If the sandbox is enabled. + """ + if not self.client.sandbox: + raise ValueError("This feature is only available in the Sandbox.") diff --git a/pyrevolut/api/simulations/post/simulate_account_topup.py b/pyrevolut/api/simulations/post/simulate_account_topup.py index 29ec970..38ada30 100644 --- a/pyrevolut/api/simulations/post/simulate_account_topup.py +++ b/pyrevolut/api/simulations/post/simulate_account_topup.py @@ -17,7 +17,7 @@ class SimulateAccountTopup: and need to add more. """ - ROUTE = "/sandbox/topup" + ROUTE = "/1.0/sandbox/topup" class Body(BaseModel): """ diff --git a/pyrevolut/api/simulations/post/simulate_transfer_state_update.py b/pyrevolut/api/simulations/post/simulate_transfer_state_update.py index bd9a643..7c697b9 100644 --- a/pyrevolut/api/simulations/post/simulate_transfer_state_update.py +++ b/pyrevolut/api/simulations/post/simulate_transfer_state_update.py @@ -17,7 +17,7 @@ class SimulateTransferStateUpdate: The resulting state is final and cannot be changed. """ - ROUTE = "/sandbox/transactions/{transfer_id}/{action}" + ROUTE = "/1.0/sandbox/transactions/{transfer_id}/{action}" class Body(BaseModel): """ diff --git a/pyrevolut/api/team_members/endpoint/asynchronous.py b/pyrevolut/api/team_members/endpoint/asynchronous.py index 21e9199..91047a9 100644 --- a/pyrevolut/api/team_members/endpoint/asynchronous.py +++ b/pyrevolut/api/team_members/endpoint/asynchronous.py @@ -54,6 +54,7 @@ async def get_team_members( list The list of all team members in your organisation. """ + self.__check_sandbox() endpoint = RetrieveListOfTeamMembers path = endpoint.ROUTE params = endpoint.Params( @@ -103,6 +104,7 @@ async def get_team_roles( list The list of all team roles in your organisation. """ + self.__check_sandbox() endpoint = RetrieveTeamRoles path = endpoint.ROUTE params = endpoint.Params( @@ -149,6 +151,7 @@ async def invite_team_member( dict The response model. """ + self.__check_sandbox() endpoint = InviteTeamMember path = endpoint.ROUTE body = endpoint.Body( @@ -163,3 +166,15 @@ async def invite_team_member( ) return endpoint.Response(**response.json()).model_dump() + + def __check_sandbox(self): + """ + Check if the sandbox is enabled. + + Raises + ------ + ValueError + If the sandbox is enabled. + """ + if self.client.sandbox: + raise ValueError("This feature is not available in Sandbox.") diff --git a/pyrevolut/api/team_members/endpoint/synchronous.py b/pyrevolut/api/team_members/endpoint/synchronous.py index 9bc19b0..bd32ec7 100644 --- a/pyrevolut/api/team_members/endpoint/synchronous.py +++ b/pyrevolut/api/team_members/endpoint/synchronous.py @@ -54,6 +54,7 @@ def get_team_members( list The list of all team members in your organisation. """ + self.__check_sandbox() endpoint = RetrieveListOfTeamMembers path = endpoint.ROUTE params = endpoint.Params( @@ -103,6 +104,7 @@ def get_team_roles( list The list of all team roles in your organisation. """ + self.__check_sandbox() endpoint = RetrieveTeamRoles path = endpoint.ROUTE params = endpoint.Params( @@ -149,6 +151,7 @@ def invite_team_member( dict The response model. """ + self.__check_sandbox() endpoint = InviteTeamMember path = endpoint.ROUTE body = endpoint.Body( @@ -163,3 +166,15 @@ def invite_team_member( ) return endpoint.Response(**response.json()).model_dump() + + def __check_sandbox(self): + """ + Check if the sandbox is enabled. + + Raises + ------ + ValueError + If the sandbox is enabled. + """ + if self.client.sandbox: + raise ValueError("This feature is not available in Sandbox.") diff --git a/pyrevolut/api/team_members/get/retrieve_list_of_team_members.py b/pyrevolut/api/team_members/get/retrieve_list_of_team_members.py index b03b2f6..01136b0 100644 --- a/pyrevolut/api/team_members/get/retrieve_list_of_team_members.py +++ b/pyrevolut/api/team_members/get/retrieve_list_of_team_members.py @@ -20,7 +20,7 @@ class RetrieveListOfTeamMembers: This feature is not available in Sandbox. """ - ROUTE = "/team-members" + ROUTE = "/1.0/team-members" class Params(BaseModel): """The parameters of the request.""" diff --git a/pyrevolut/api/team_members/get/retrieve_team_roles.py b/pyrevolut/api/team_members/get/retrieve_team_roles.py index 80e3758..c8edbef 100644 --- a/pyrevolut/api/team_members/get/retrieve_team_roles.py +++ b/pyrevolut/api/team_members/get/retrieve_team_roles.py @@ -17,7 +17,7 @@ class RetrieveTeamRoles: This feature is not available in Sandbox. """ - ROUTE = "/roles" + ROUTE = "/1.0/roles" class Params(BaseModel): """ diff --git a/pyrevolut/api/team_members/post/invite_team_member.py b/pyrevolut/api/team_members/post/invite_team_member.py index 07f70d7..39ded42 100644 --- a/pyrevolut/api/team_members/post/invite_team_member.py +++ b/pyrevolut/api/team_members/post/invite_team_member.py @@ -21,7 +21,7 @@ class InviteTeamMember: This feature is not available in Sandbox. """ - ROUTE = "/team-members" + ROUTE = "/1.0/team-members" class Body(BaseModel): """ diff --git a/pyrevolut/api/transactions/get/retrieve_list_of_transactions.py b/pyrevolut/api/transactions/get/retrieve_list_of_transactions.py index 2a352d5..bf5451e 100644 --- a/pyrevolut/api/transactions/get/retrieve_list_of_transactions.py +++ b/pyrevolut/api/transactions/get/retrieve_list_of_transactions.py @@ -1,7 +1,7 @@ from typing import Annotated from uuid import UUID -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, ConfigDict from pyrevolut.utils import DateTime, Date from pyrevolut.api.common import EnumTransactionType @@ -27,13 +27,18 @@ class RetrieveListOfTransactions: plans can only access information older than 90 days within 5 minutes of the first authorisation. """ - ROUTE = "/transactions" + ROUTE = "/1.0/transactions" class Params(BaseModel): """ The query parameters for the request. """ + model_config = ConfigDict( + populate_by_name=True, + from_attributes=True, + ) + from_: Annotated[ DateTime | Date | None, Field( diff --git a/pyrevolut/api/transactions/get/retrieve_transaction.py b/pyrevolut/api/transactions/get/retrieve_transaction.py index a60b6ac..51ce353 100644 --- a/pyrevolut/api/transactions/get/retrieve_transaction.py +++ b/pyrevolut/api/transactions/get/retrieve_transaction.py @@ -23,7 +23,7 @@ class RetrieveTransaction: /transaction/{request_id}?id_type=request_id """ - ROUTE = "/transactions/{id}" + ROUTE = "/1.0/transaction/{id}" class Params(BaseModel): """ diff --git a/pyrevolut/api/transactions/resources/transaction.py b/pyrevolut/api/transactions/resources/transaction.py index b88dcb7..034e71c 100644 --- a/pyrevolut/api/transactions/resources/transaction.py +++ b/pyrevolut/api/transactions/resources/transaction.py @@ -4,7 +4,7 @@ from pydantic import BaseModel, Field from pydantic_extra_types.currency_code import Currency -from pydantic_extra_types.country import CountryAlpha2 +from pydantic_extra_types.country import CountryAlpha2, CountryAlpha3 from pydantic_extra_types.phone_numbers import PhoneNumber from pyrevolut.utils import DateTime, Date @@ -23,7 +23,7 @@ class ModelMerchant(BaseModel): city: Annotated[str, Field(description="The city of the merchant.")] category_code: Annotated[str, Field(description="The category code of the merchant.")] country: Annotated[ - CountryAlpha2, + CountryAlpha2 | CountryAlpha3, Field(description="The country of the merchant as the 2-letter ISO 3166 code."), ] @@ -157,7 +157,7 @@ class ModelCard(BaseModel): merchant: Annotated[ ModelMerchant | None, Field(description="The information about the merchant (only for card transfers)."), - ] + ] = None reference: Annotated[ str | None, Field(description="The reference of the transaction."), diff --git a/pyrevolut/api/transfers/get/get_transfer_reasons.py b/pyrevolut/api/transfers/get/get_transfer_reasons.py index 677a04c..a64989e 100644 --- a/pyrevolut/api/transfers/get/get_transfer_reasons.py +++ b/pyrevolut/api/transfers/get/get_transfer_reasons.py @@ -18,7 +18,7 @@ class GetTransferReasons: field when making a transfer to a counterparty or creating a payout link. """ - ROUTE = "/transfer-reasons" + ROUTE = "/1.0/transfer-reasons" class Params(BaseModel): """ diff --git a/pyrevolut/api/transfers/post/create_transfer_to_another_account.py b/pyrevolut/api/transfers/post/create_transfer_to_another_account.py index a6f7a4d..b66be9c 100644 --- a/pyrevolut/api/transfers/post/create_transfer_to_another_account.py +++ b/pyrevolut/api/transfers/post/create_transfer_to_another_account.py @@ -29,7 +29,7 @@ class CreateTransferToAnotherAccount: leverage our Payment drafts (/payment-drafts) endpoint. """ - ROUTE = "/pay" + ROUTE = "/1.0/pay" class Body(BaseModel): """ diff --git a/pyrevolut/api/transfers/post/move_money_between_accounts.py b/pyrevolut/api/transfers/post/move_money_between_accounts.py index 65fb3d0..9773646 100644 --- a/pyrevolut/api/transfers/post/move_money_between_accounts.py +++ b/pyrevolut/api/transfers/post/move_money_between_accounts.py @@ -15,7 +15,7 @@ class MoveMoneyBetweenAccounts: The resulting transaction has the type transfer. """ - ROUTE = "/transfer" + ROUTE = "/1.0/transfer" class Body(BaseModel): """ diff --git a/pyrevolut/api/transfers/resources/transfer.py b/pyrevolut/api/transfers/resources/transfer.py index d4fba18..e040357 100644 --- a/pyrevolut/api/transfers/resources/transfer.py +++ b/pyrevolut/api/transfers/resources/transfer.py @@ -46,4 +46,4 @@ class ResourceTransfer(BaseModel): completed_at: Annotated[ DateTime | None, Field(description="The date and time the transaction was completed in ISO 8601 format."), - ] + ] = None diff --git a/pyrevolut/api/webhooks/delete/delete_webhook.py b/pyrevolut/api/webhooks/delete/delete_webhook.py index fc65eac..49edb88 100644 --- a/pyrevolut/api/webhooks/delete/delete_webhook.py +++ b/pyrevolut/api/webhooks/delete/delete_webhook.py @@ -8,7 +8,7 @@ class DeleteWebhook: A successful response does not get any content in return. """ - ROUTE = "/webhooks/{webhook_id}" + ROUTE = "/2.0/webhooks/{webhook_id}" class Params(BaseModel): """ diff --git a/pyrevolut/api/webhooks/get/retrieve_list_of_failed_webhooks.py b/pyrevolut/api/webhooks/get/retrieve_list_of_failed_webhooks.py index 5634b20..e0c1751 100644 --- a/pyrevolut/api/webhooks/get/retrieve_list_of_failed_webhooks.py +++ b/pyrevolut/api/webhooks/get/retrieve_list_of_failed_webhooks.py @@ -20,7 +20,7 @@ class RetrieveListOfFailedWebhooks: last event returned in the previous response. """ - ROUTE = "/webhooks/{webhook_id}/failed-events" + ROUTE = "/2.0/webhooks/{webhook_id}/failed-events" class Params(BaseModel): """ diff --git a/pyrevolut/api/webhooks/get/retrieve_list_of_webhooks.py b/pyrevolut/api/webhooks/get/retrieve_list_of_webhooks.py index eabf48d..e96c7ba 100644 --- a/pyrevolut/api/webhooks/get/retrieve_list_of_webhooks.py +++ b/pyrevolut/api/webhooks/get/retrieve_list_of_webhooks.py @@ -8,7 +8,7 @@ class RetrieveListOfWebhooks: Get the list of all your existing webhooks and their details. """ - ROUTE = "/webhooks" + ROUTE = "/2.0/webhooks" class Params(BaseModel): """ diff --git a/pyrevolut/api/webhooks/get/retrieve_webhook.py b/pyrevolut/api/webhooks/get/retrieve_webhook.py index c1d1d7e..f6a9fb8 100644 --- a/pyrevolut/api/webhooks/get/retrieve_webhook.py +++ b/pyrevolut/api/webhooks/get/retrieve_webhook.py @@ -10,7 +10,7 @@ class RetrieveWebhook: Get the information about a specific webhook by ID. """ - ROUTE = "/webhooks/{webhook_id}" + ROUTE = "/2.0/webhooks/{webhook_id}" class Params(BaseModel): """ diff --git a/pyrevolut/api/webhooks/patch/update_webhook.py b/pyrevolut/api/webhooks/patch/update_webhook.py index 9fac417..165aa4a 100644 --- a/pyrevolut/api/webhooks/patch/update_webhook.py +++ b/pyrevolut/api/webhooks/patch/update_webhook.py @@ -15,7 +15,7 @@ class UpdateWebhook: The fields that you don't specify are not updated. """ - ROUTE = "/webhooks/{webhook_id}" + ROUTE = "/2.0/webhooks/{webhook_id}" class Body(BaseModel): """ diff --git a/pyrevolut/api/webhooks/post/create_webhook.py b/pyrevolut/api/webhooks/post/create_webhook.py index 16c9033..25179db 100644 --- a/pyrevolut/api/webhooks/post/create_webhook.py +++ b/pyrevolut/api/webhooks/post/create_webhook.py @@ -13,7 +13,7 @@ class CreateWebhook: Only HTTPS URLs are supported. """ - ROUTE = "/webhooks" + ROUTE = "/2.0/webhooks" class Body(BaseModel): """ diff --git a/pyrevolut/api/webhooks/post/rotate_webhook_secret.py b/pyrevolut/api/webhooks/post/rotate_webhook_secret.py index 5a869a9..8df5ac2 100644 --- a/pyrevolut/api/webhooks/post/rotate_webhook_secret.py +++ b/pyrevolut/api/webhooks/post/rotate_webhook_secret.py @@ -11,7 +11,7 @@ class RotateWebhookSecret: Rotate a signing secret for a specific webhook. """ - ROUTE = "/webhooks/{webhook_id}/rotate-signing-secret" + ROUTE = "/2.0/webhooks/{webhook_id}/rotate-signing-secret" class Body(BaseModel): """ diff --git a/pyrevolut/client/base.py b/pyrevolut/client/base.py index c7a6b6c..1e6243e 100644 --- a/pyrevolut/client/base.py +++ b/pyrevolut/client/base.py @@ -36,6 +36,7 @@ def __init__( Parameters ---------- creds_loc : str, optional + The location of the credentials file, by default "credentials/creds.json" sandbox : bool, optional Whether to use the sandbox environment, by default True """ @@ -44,9 +45,9 @@ def __init__( # Set domain based on environment if self.sandbox: - self.domain = "https://sandbox-b2b.revolut.com/api/1.0/" + self.domain = "https://sandbox-b2b.revolut.com/api" else: - self.domain = "https://b2b.revolut.com/api/1.0/" + self.domain = "https://b2b.revolut.com/api" # Load the credentials self.__load_credentials() @@ -346,7 +347,7 @@ def __process_path(self, path: str) -> str: if "http" in path: return path - return self.__remove_leading_slash(path) + return self.__remove_leading_slash(path=path) def __remove_leading_slash(self, path: str) -> str: """Remove the leading slash from a path if it exists and @@ -385,17 +386,17 @@ def __replace_null_with_none(self, data: D) -> D: if isinstance(data, dict): for k, v in data.items(): if isinstance(v, dict): - self.__replace_null_with_none(data_dict=v, data_list=None) + self.__replace_null_with_none(data=v) elif isinstance(v, list): - self.__replace_null_with_none(data_dict=None, data_list=v) + self.__replace_null_with_none(data=v) elif v == "null": data[k] = None elif isinstance(data, list): for i in range(len(data)): if isinstance(data[i], dict): - self.__replace_null_with_none(data_dict=data[i], data_list=None) + self.__replace_null_with_none(data=data[i]) elif isinstance(data[i], list): - self.__replace_null_with_none(data_dict=None, data_list=data[i]) + self.__replace_null_with_none(data=data[i]) elif data[i] == "null": data[i] = None else: diff --git a/pyrevolut/utils/auth/creds.py b/pyrevolut/utils/auth/creds.py index 59cb3f1..2af0450 100644 --- a/pyrevolut/utils/auth/creds.py +++ b/pyrevolut/utils/auth/creds.py @@ -2,7 +2,7 @@ import json import pendulum -from pydantic import BaseModel, Field, SecretStr, field_serializer +from pydantic import BaseModel, Field, SecretStr, field_serializer, ConfigDict from pyrevolut.utils.datetime import DateTime @@ -10,9 +10,13 @@ class ModelCreds(BaseModel): """The model that represents the credentials JSON file.""" + model_config = ConfigDict(validate_assignment=True, extra="forbid") + class ModelCertificate(BaseModel): """The model that represents the certificate information""" + model_config = ConfigDict(validate_assignment=True, extra="forbid") + public: Annotated[ SecretStr, Field(description="The public certificate in base64 encoded format") ] @@ -31,6 +35,8 @@ def dump_secret(self, value: SecretStr) -> str: class ModelClientAssertJWT(BaseModel): """The model that represents the client assertion JWT information""" + model_config = ConfigDict(validate_assignment=True, extra="forbid") + jwt: Annotated[SecretStr, Field(description="The JWT assertion string")] expiration_dt: Annotated[DateTime, Field(description="The expiration datetime of the JWT")] @@ -42,6 +48,8 @@ def dump_secret(self, value: SecretStr) -> str: class ModelTokens(BaseModel): """The model that represents the tokens information""" + model_config = ConfigDict(validate_assignment=True, extra="forbid") + access_token: Annotated[SecretStr, Field(description="The access token")] refresh_token: Annotated[SecretStr, Field(description="The refresh token")] token_type: Annotated[str, Field(description="The token type")] diff --git a/tests/test_accounts.py b/tests/test_accounts.py index 5d29ad5..b66eee6 100644 --- a/tests/test_accounts.py +++ b/tests/test_accounts.py @@ -1,22 +1,99 @@ +import time +import asyncio import pytest +import random from pyrevolut.client import Client, AsyncClient +def test_sync_get_all_accounts(sync_client: Client): + """Test the sync `get_all_accounts` accounts method""" + # Get Accounts + accounts_all = sync_client.Accounts.get_all_accounts() + time.sleep(random.randint(1, 3)) + assert isinstance(accounts_all, list) + for account in accounts_all: + assert isinstance(account, dict) + + +def test_sync_get_account(sync_client: Client): + """Test the sync `get_account` accounts method""" + # Get Accounts + accounts_all = sync_client.Accounts.get_all_accounts() + time.sleep(random.randint(1, 3)) + assert isinstance(accounts_all, list) + for account in accounts_all: + assert isinstance(account, dict) + + # Get Account + for account in accounts_all: + account_id = account["id"] + account = sync_client.Accounts.get_account(account_id) + time.sleep(random.randint(1, 3)) + assert isinstance(account, dict) + assert account["id"] == account_id + + +def test_sync_get_full_bank_details(sync_client: Client): + """Test the sync `get_full_bank_details` accounts method""" + # Get Accounts + accounts_all = sync_client.Accounts.get_all_accounts() + time.sleep(random.randint(1, 3)) + assert isinstance(accounts_all, list) + for account in accounts_all: + assert isinstance(account, dict) + + # Get Full Bank Details + for account in accounts_all: + account_id = account["id"] + bank_details = sync_client.Accounts.get_full_bank_details(account_id) + time.sleep(random.randint(1, 3)) + assert isinstance(bank_details, dict) + + @pytest.mark.asyncio async def test_async_get_all_accounts(async_client: AsyncClient): """Test the async `get_all_accounts` accounts method""" # Get Accounts accounts_all = await async_client.Accounts.get_all_accounts() + await asyncio.sleep(random.randint(1, 3)) assert isinstance(accounts_all, list) for account in accounts_all: assert isinstance(account, dict) -def test_sync_get_all_accounts(sync_client: Client): - """Test the sync `get_all_accounts` accounts method""" +@pytest.mark.asyncio +async def test_async_get_account(async_client: AsyncClient): + """Test the async `get_account` accounts method""" # Get Accounts - accounts_all = sync_client.Accounts.get_all_accounts() + accounts_all = await async_client.Accounts.get_all_accounts() + await asyncio.sleep(random.randint(1, 3)) assert isinstance(accounts_all, list) for account in accounts_all: assert isinstance(account, dict) + + # Get Account + for account in accounts_all: + account_id = account["id"] + account = await async_client.Accounts.get_account(account_id) + await asyncio.sleep(random.randint(1, 3)) + assert isinstance(account, dict) + assert account["id"] == account_id + + +@pytest.mark.asyncio +async def test_async_get_full_bank_details(async_client: AsyncClient): + """Test the async `get_full_bank_details` accounts method""" + # Get Accounts + accounts_all = await async_client.Accounts.get_all_accounts() + await asyncio.sleep(random.randint(1, 3)) + assert isinstance(accounts_all, list) + for account in accounts_all: + assert isinstance(account, dict) + + # Get Full Bank Details + for account in accounts_all: + account_id = account["id"] + bank_details = await async_client.Accounts.get_full_bank_details(account_id) + await asyncio.sleep(random.randint(1, 3)) + assert isinstance(bank_details, dict) diff --git a/tests/test_card.py b/tests/test_card.py new file mode 100644 index 0000000..4af64c5 --- /dev/null +++ b/tests/test_card.py @@ -0,0 +1,174 @@ +import time +import asyncio +import pytest +import random + +from pyrevolut.client import Client, AsyncClient + + +def test_sync_get_all_cards(sync_client: Client): + """Test the sync `get_all_cards` cards method""" + # Get Cards (no params) + with pytest.raises(ValueError, match="This feature is not available in Sandbox."): + cards_all = sync_client.Cards.get_all_cards() + time.sleep(random.randint(1, 3)) + assert isinstance(cards_all, list) + for card in cards_all: + assert isinstance(card, dict) + + # Get Cards (with params) + with pytest.raises(ValueError, match="This feature is not available in Sandbox."): + cards_all = sync_client.Cards.get_all_cards( + created_before="2020-01-01", + limit=10, + ) + time.sleep(random.randint(1, 3)) + assert isinstance(cards_all, list) + assert len(cards_all) == 0 + + +def test_sync_get_card(sync_client: Client): + """Test the sync `get_card` cards method""" + with pytest.raises(ValueError, match="This feature is not available in Sandbox."): + # Get all cards + cards_all = sync_client.Cards.get_all_cards() + time.sleep(random.randint(1, 3)) + assert isinstance(cards_all, list) + assert len(cards_all) > 0 + + # Get Card + for card in cards_all: + card_id = card["id"] + card = sync_client.Cards.get_card(card_id=card_id) + time.sleep(random.randint(1, 3)) + assert isinstance(card, dict) + + +def test_get_card_sensitive_details(sync_client: Client): + """Test the sync `get_card_sensitive_details` cards method""" + with pytest.raises(ValueError, match="This feature is not available in Sandbox."): + # Get all cards + cards_all = sync_client.Cards.get_all_cards() + time.sleep(random.randint(1, 3)) + assert isinstance(cards_all, list) + assert len(cards_all) > 0 + + # Get Card + for card in cards_all: + card_id = card["id"] + card = sync_client.Cards.get_card_sensitive_details(card_id=card_id) + time.sleep(random.randint(1, 3)) + assert isinstance(card, dict) + + +def test_create_card(sync_client: Client): + """Test the sync `create_card` cards method""" + # TODO: Implement this test + + +def test_freeze_card(sync_client: Client): + """Test the sync `freeze_card` cards method""" + # TODO: Implement this test + + +def test_unfreeze_card(sync_client: Client): + """Test the sync `unfreeze_card` cards method""" + # TODO: Implement this test + + +def test_update_card(sync_client: Client): + """Test the sync `update_card` cards method""" + # TODO: Implement this test + + +def test_delete_card(sync_client: Client): + """Test the sync `delete_card` cards method""" + # TODO: Implement this test + + +@pytest.mark.asyncio +async def test_async_get_all_cards(async_client: AsyncClient): + """Test the async `get_all_cards` cards method""" + # Get Cards (no params) + with pytest.raises(ValueError, match="This feature is not available in Sandbox."): + cards_all = await async_client.Cards.get_all_cards() + await asyncio.sleep(random.randint(1, 3)) + assert isinstance(cards_all, list) + for card in cards_all: + assert isinstance(card, dict) + + # Get Cards (with params) + with pytest.raises(ValueError, match="This feature is not available in Sandbox."): + cards_all = await async_client.Cards.get_all_cards( + created_before="2020-01-01", + limit=10, + ) + await asyncio.sleep(random.randint(1, 3)) + assert isinstance(cards_all, list) + assert len(cards_all) == 0 + + +@pytest.mark.asyncio +async def test_async_get_card(async_client: AsyncClient): + """Test the async `get_card` cards method""" + with pytest.raises(ValueError, match="This feature is not available in Sandbox."): + # Get all cards + cards_all = await async_client.Cards.get_all_cards() + await asyncio.sleep(random.randint(1, 3)) + assert isinstance(cards_all, list) + assert len(cards_all) > 0 + + # Get Card + for card in cards_all: + card_id = card["id"] + card = await async_client.Cards.get_card(card_id=card_id) + await asyncio.sleep(random.randint(1, 3)) + assert isinstance(card, dict) + + +@pytest.mark.asyncio +async def test_async_get_card_sensitive_details(async_client: AsyncClient): + """Test the async `get_card_sensitive_details` cards method""" + with pytest.raises(ValueError, match="This feature is not available in Sandbox."): + # Get all cards + cards_all = await async_client.Cards.get_all_cards() + await asyncio.sleep(random.randint(1, 3)) + assert isinstance(cards_all, list) + assert len(cards_all) > 0 + + # Get Card + for card in cards_all: + card_id = card["id"] + card = await async_client.Cards.get_card_sensitive_details(card_id=card_id) + await asyncio.sleep(random.randint(1, 3)) + assert isinstance(card, dict) + + +@pytest.mark.asyncio +async def test_async_create_card(async_client: AsyncClient): + """Test the async `create_card` cards method""" + # TODO: Implement this test + + +@pytest.mark.asyncio +async def test_async_freeze_card(async_client: AsyncClient): + """Test the async `freeze_card` cards method""" + # TODO: Implement this test + + +@pytest.mark.asyncio +async def test_async_unfreeze_card(async_client: AsyncClient): + """Test the async `unfreeze_card` cards method""" + # TODO: Implement this test + + +@pytest.mark.asyncio +async def test_async_update_card(async_client: AsyncClient): + """Test the async `update_card` cards method""" + # TODO: Implement this test + + +@pytest.mark.asyncio +async def test_async_delete_card(async_client: AsyncClient): + """Test the async `delete_card` cards method""" + # TODO: Implement this test diff --git a/tests/test_counterparties.py b/tests/test_counterparties.py new file mode 100644 index 0000000..c1a3a85 --- /dev/null +++ b/tests/test_counterparties.py @@ -0,0 +1,289 @@ +import time +import asyncio +import pytest +import random + +from pyrevolut.client import Client, AsyncClient +from pyrevolut.api import EnumProfileType + + +def test_sync_get_all_counterparties(sync_client: Client): + """Test the sync `get_all_counterparties` counterparties method""" + # Get Counterparties (no params) + counterparties_all = sync_client.Counterparties.get_all_counterparties() + time.sleep(random.randint(1, 3)) + assert isinstance(counterparties_all, list) + for counterparty in counterparties_all: + assert isinstance(counterparty, dict) + + # Get Counterparties (with params) + counterparties_all = sync_client.Counterparties.get_all_counterparties( + name="Testing", + account_no="12345678", + created_before="2020-01-01", + limit=10, + ) + time.sleep(random.randint(1, 3)) + assert isinstance(counterparties_all, list) + assert len(counterparties_all) == 0 + + +def test_sync_get_counterparty(sync_client: Client): + """Test the sync `get_counterparty` counterparties method""" + # Get all counterparties + counterparties_all = sync_client.Counterparties.get_all_counterparties() + time.sleep(random.randint(1, 3)) + assert isinstance(counterparties_all, list) + assert len(counterparties_all) > 0 + + # Get Counterparty + for counterparty in counterparties_all: + counterparty_id = counterparty["id"] + counterparty = sync_client.Counterparties.get_counterparty(counterparty_id=counterparty_id) + time.sleep(random.randint(1, 3)) + assert isinstance(counterparty, dict) + + +def test_sync_validate_account_name(sync_client: Client): + """Test the sync `validate_account_name` counterparties method""" + # Validate UK individual counterparty (personal account) + response = sync_client.Counterparties.validate_account_name( + sort_code="54-01-05", + account_no="12345678", + individual_first_name="John", + individual_last_name="Doe", + ) + time.sleep(random.randint(1, 3)) + assert response["result_code"] == "cannot_be_checked" + + # Validate UK company counterparty (business account) + response = sync_client.Counterparties.validate_account_name( + sort_code="54-01-05", + account_no="12345678", + company_name="John Smith Co.", + ) + time.sleep(random.randint(1, 3)) + assert response["result_code"] == "cannot_be_checked" + + +def test_create_delete_counterparty(sync_client: Client): + """Test the sync `create_counterparty` and `delete_counterparty` counterparties methods""" + + counterparty_ids = [] + + # Create Personal Revolut Counterparty + counterparty = sync_client.Counterparties.create_counterparty( + profile_type=EnumProfileType.PERSONAL, + name="Test User 2", + revtag="john2pvki", + ) + time.sleep(random.randint(1, 3)) + counterparty_ids.append(counterparty["id"]) + + # Create UK Individual Counterparty + counterparty = sync_client.Counterparties.create_counterparty( + individual_first_name="John", + individual_last_name="Doe", + bank_country="GB", + currency="GBP", + sort_code="54-01-05", + account_no="12345678", + ) + time.sleep(random.randint(1, 3)) + counterparty_ids.append(counterparty["id"]) + + # Create UK Company Counterparty + counterparty = sync_client.Counterparties.create_counterparty( + company_name="John Smith Co.", + bank_country="GB", + currency="GBP", + sort_code="54-01-05", + account_no="12345678", + ) + time.sleep(random.randint(1, 3)) + counterparty_ids.append(counterparty["id"]) + + # Create International Business Counterparty (eurozone with EUR) + counterparty = sync_client.Counterparties.create_counterparty( + company_name="John Smith Co.", + bank_country="FR", + currency="EUR", + iban="FR1420041010050500013M02606", + ) + time.sleep(random.randint(1, 3)) + counterparty_ids.append(counterparty["id"]) + + # Create International Business Counterparty (outside eurozone) + counterparty = sync_client.Counterparties.create_counterparty( + company_name="Johann Meier Co.", + bank_country="CH", + currency="EUR", + iban="CH5604835012345678009", + address_street_line1="Bahnhofstrasse 4a/8", + address_city="Zurich", + address_country="CH", + address_postcode="8001", + ) + time.sleep(random.randint(1, 3)) + counterparty_ids.append(counterparty["id"]) + + # Fetch all counterparties + counterparties_all = sync_client.Counterparties.get_all_counterparties() + time.sleep(random.randint(1, 3)) + for counterparty_id in counterparty_ids: + assert counterparty_id in [counterparty["id"] for counterparty in counterparties_all] + + # Delete all created counterparties + for counterparty_id in counterparty_ids: + sync_client.Counterparties.delete_counterparty(counterparty_id=counterparty_id) + time.sleep(random.randint(1, 3)) + + # Fetch all counterparties + counterparties_all = sync_client.Counterparties.get_all_counterparties() + time.sleep(random.randint(1, 3)) + for counterparty_id in counterparty_ids: + assert counterparty_id not in [counterparty["id"] for counterparty in counterparties_all] + + +@pytest.mark.asyncio +async def test_async_get_all_counterparties(async_client: AsyncClient): + """Test the async `get_all_counterparties` counterparties method""" + # Get Counterparties (no params) + counterparties_all = await async_client.Counterparties.get_all_counterparties() + await asyncio.sleep(random.randint(1, 3)) + assert isinstance(counterparties_all, list) + for counterparty in counterparties_all: + assert isinstance(counterparty, dict) + + # Get Counterparties (with params) + counterparties_all = await async_client.Counterparties.get_all_counterparties( + name="Testing", + account_no="12345678", + created_before="2020-01-01", + limit=10, + ) + await asyncio.sleep(random.randint(1, 3)) + assert isinstance(counterparties_all, list) + assert len(counterparties_all) == 0 + + +@pytest.mark.asyncio +async def test_async_get_counterparty(async_client: AsyncClient): + """Test the async `get_counterparty` counterparties method""" + # Get all counterparties + counterparties_all = await async_client.Counterparties.get_all_counterparties() + await asyncio.sleep(random.randint(1, 3)) + assert isinstance(counterparties_all, list) + assert len(counterparties_all) > 0 + + # Get Counterparty + for counterparty in counterparties_all: + counterparty_id = counterparty["id"] + counterparty = await async_client.Counterparties.get_counterparty( + counterparty_id=counterparty_id + ) + await asyncio.sleep(random.randint(1, 3)) + assert isinstance(counterparty, dict) + + +@pytest.mark.asyncio +async def test_async_validate_account_name(async_client: AsyncClient): + """Test the async `validate_account_name` counterparties method""" + # Validate UK individual counterparty (personal account) + response = await async_client.Counterparties.validate_account_name( + sort_code="54-01-05", + account_no="12345678", + individual_first_name="John", + individual_last_name="Doe", + ) + await asyncio.sleep(random.randint(1, 3)) + assert response["result_code"] == "cannot_be_checked" + + # Validate UK company counterparty (business account) + response = await async_client.Counterparties.validate_account_name( + sort_code="54-01-05", + account_no="12345678", + company_name="John Smith Co.", + ) + await asyncio.sleep(random.randint(1, 3)) + assert response["result_code"] == "cannot_be_checked" + + +@pytest.mark.asyncio +async def test_async_create_delete_counterparty(async_client: AsyncClient): + """Test the async `create_counterparty` and `delete_counterparty` counterparties methods""" + + counterparty_ids = [] + + # Create Personal Revolut Counterparty + counterparty = await async_client.Counterparties.create_counterparty( + profile_type=EnumProfileType.PERSONAL, + name="Test User 1", + revtag="john1pvki", + ) + await asyncio.sleep(random.randint(1, 3)) + counterparty_ids.append(counterparty["id"]) + + # Create UK Individual Counterparty + counterparty = await async_client.Counterparties.create_counterparty( + individual_first_name="John", + individual_last_name="Doe", + bank_country="GB", + currency="GBP", + sort_code="54-01-05", + account_no="12345678", + ) + await asyncio.sleep(random.randint(1, 3)) + counterparty_ids.append(counterparty["id"]) + + # Create UK Company Counterparty + counterparty = await async_client.Counterparties.create_counterparty( + company_name="John Smith Co.", + bank_country="GB", + currency="GBP", + sort_code="54-01-05", + account_no="12345678", + ) + await asyncio.sleep(random.randint(1, 3)) + counterparty_ids.append(counterparty["id"]) + + # Create International Business Counterparty (eurozone with EUR) + counterparty = await async_client.Counterparties.create_counterparty( + company_name="John Smith Co.", + bank_country="FR", + currency="EUR", + iban="FR1420041010050500013M02606", + ) + await asyncio.sleep(random.randint(1, 3)) + counterparty_ids.append(counterparty["id"]) + + # Create International Business Counterparty (outside eurozone) + counterparty = await async_client.Counterparties.create_counterparty( + company_name="Johann Meier Co.", + bank_country="CH", + currency="EUR", + iban="CH5604835012345678009", + address_street_line1="Bahnhofstrasse 4a/8", + address_city="Zurich", + address_country="CH", + address_postcode="8001", + ) + await asyncio.sleep(random.randint(1, 3)) + counterparty_ids.append(counterparty["id"]) + + # Fetch all counterparties + counterparties_all = await async_client.Counterparties.get_all_counterparties() + await asyncio.sleep(random.randint(1, 3)) + for counterparty_id in counterparty_ids: + assert counterparty_id in [counterparty["id"] for counterparty in counterparties_all] + + # Delete all created counterparties + for counterparty_id in counterparty_ids: + await async_client.Counterparties.delete_counterparty(counterparty_id=counterparty_id) + await asyncio.sleep(random.randint(1, 3)) + + # Fetch all counterparties + counterparties_all = await async_client.Counterparties.get_all_counterparties() + await asyncio.sleep(random.randint(1, 3)) + for counterparty_id in counterparty_ids: + assert counterparty_id not in [counterparty["id"] for counterparty in counterparties_all] diff --git a/tests/test_foreign_exchange.py b/tests/test_foreign_exchange.py new file mode 100644 index 0000000..39156f0 --- /dev/null +++ b/tests/test_foreign_exchange.py @@ -0,0 +1,243 @@ +import time +import asyncio +from uuid import uuid4 +from decimal import Decimal +import pytest +import random + +from pyrevolut.client import Client, AsyncClient +from pyrevolut.api import EnumAccountState, EnumTransactionState + + +def test_sync_get_exchange_rate(sync_client: Client): + """Test the sync `get_exchange_rate` foreign exchange method""" + # Get Exchange Rate EUR to USD + sync_client.ForeignExchange.get_exchange_rate( + from_currency="EUR", + to_currency="USD", + ) + time.sleep(random.randint(1, 3)) + + # Get Exchange Rate USD to EUR + sync_client.ForeignExchange.get_exchange_rate( + from_currency="USD", + to_currency="EUR", + ) + time.sleep(random.randint(1, 3)) + + # Get Exchange Rate EUR to GBP + sync_client.ForeignExchange.get_exchange_rate( + from_currency="EUR", + to_currency="GBP", + ) + time.sleep(random.randint(1, 3)) + + +def test_sync_exchange_money(sync_client: Client): + """Test the sync `exchange_money` foreign exchange method""" + + with pytest.raises(ValueError, match="Something went wrong"): + # Get all accounts + accounts = sync_client.Accounts.get_all_accounts() + time.sleep(random.randint(1, 3)) + + # Get GBP and EUR accounts + gbp_account = next( + account + for account in accounts + if account["currency"] == "GBP" + and account["state"] == EnumAccountState.ACTIVE + and account["balance"] > Decimal("0") + ) + gbp_balance = gbp_account["balance"] + eur_account = next( + account + for account in accounts + if account["currency"] == "EUR" + and account["state"] == EnumAccountState.ACTIVE + and account["balance"] > Decimal("0") + ) + eur_balance = eur_account["balance"] + + # Exchange 1 EUR from EUR to GBP + response = sync_client.ForeignExchange.exchange_money( + request_id=str(uuid4()), + from_account_id=eur_account["id"], + from_currency="EUR", + to_account_id=gbp_account["id"], + to_currency="GBP", + from_amount=Decimal("1"), + to_amount=None, + reference="PyRevolut Test", + ) + time.sleep(random.randint(1, 3)) + assert response["state"] == EnumTransactionState.COMPLETED + + # Check balances + accounts = sync_client.Accounts.get_all_accounts() + time.sleep(random.randint(1, 3)) + gbp_balance2 = next( + account["balance"] + for account in accounts + if account["currency"] == "GBP" + and account["state"] == EnumAccountState.ACTIVE + and account["balance"] > Decimal("0") + ) + eur_balance2 = next( + account["balance"] + for account in accounts + if account["currency"] == "EUR" + and account["state"] == EnumAccountState.ACTIVE + and account["balance"] > Decimal("0") + ) + assert gbp_balance2 > gbp_balance + assert eur_balance2 == eur_balance - Decimal("1") + + # Exchange 1 EUR from GBP to EUR + response = sync_client.ForeignExchange.exchange_money( + request_id=str(uuid4()), + from_account_id=gbp_account["id"], + from_currency="GBP", + to_account_id=eur_account["id"], + to_currency="EUR", + from_amount=None, + to_amount=Decimal("1"), + reference="PyRevolut Test", + ) + time.sleep(random.randint(1, 3)) + assert response["state"] == EnumTransactionState.COMPLETED + + # Check balances + accounts = sync_client.Accounts.get_all_accounts() + time.sleep(random.randint(1, 3)) + gbp_balance3 = next( + account["balance"] + for account in accounts + if account["currency"] == "GBP" and account["state"] == EnumAccountState.ACTIVE + ) + eur_balance3 = next( + account["balance"] + for account in accounts + if account["currency"] == "EUR" and account["state"] == EnumAccountState.ACTIVE + ) + assert gbp_balance3 < gbp_balance2 + assert eur_balance3 == eur_balance + assert eur_balance3 > eur_balance2 + + +@pytest.mark.asyncio +async def test_async_get_exchange_rate(async_client: AsyncClient): + """Test the async `get_exchange_rate` foreign exchange method""" + # Get Exchange Rate EUR to USD + await async_client.ForeignExchange.get_exchange_rate( + from_currency="EUR", + to_currency="USD", + ) + await asyncio.sleep(random.randint(1, 3)) + + # Get Exchange Rate USD to EUR + await async_client.ForeignExchange.get_exchange_rate( + from_currency="USD", + to_currency="EUR", + ) + await asyncio.sleep(random.randint(1, 3)) + + # Get Exchange Rate EUR to GBP + await async_client.ForeignExchange.get_exchange_rate( + from_currency="EUR", + to_currency="GBP", + ) + await asyncio.sleep(random.randint(1, 3)) + + +@pytest.mark.asyncio +async def test_async_exchange_money(async_client: AsyncClient): + """Test the async `exchange_money` foreign exchange method""" + + with pytest.raises(ValueError, match="Something went wrong"): + # Get all accounts + accounts = await async_client.Accounts.get_all_accounts() + await asyncio.sleep(random.randint(1, 3)) + + # Get GBP and EUR accounts + gbp_account = next( + account + for account in accounts + if account["currency"] == "GBP" + and account["state"] == EnumAccountState.ACTIVE + and account["balance"] > Decimal("0") + ) + gbp_balance = gbp_account["balance"] + eur_account = next( + account + for account in accounts + if account["currency"] == "EUR" + and account["state"] == EnumAccountState.ACTIVE + and account["balance"] > Decimal("0") + ) + eur_balance = eur_account["balance"] + + # Exchange 1 EUR from EUR to GBP + response = await async_client.ForeignExchange.exchange_money( + request_id=str(uuid4()), + from_account_id=eur_account["id"], + from_currency="EUR", + to_account_id=gbp_account["id"], + to_currency="GBP", + from_amount=Decimal("1"), + to_amount=None, + reference="PyRevolut Test", + ) + await asyncio.sleep(random.randint(1, 3)) + assert response["state"] == EnumTransactionState.COMPLETED + + # Check balances + accounts = await async_client.Accounts.get_all_accounts() + await asyncio.sleep(random.randint(1, 3)) + gbp_balance2 = next( + account["balance"] + for account in accounts + if account["currency"] == "GBP" + and account["state"] == EnumAccountState.ACTIVE + and account["balance"] > Decimal("0") + ) + eur_balance2 = next( + account["balance"] + for account in accounts + if account["currency"] == "EUR" + and account["state"] == EnumAccountState.ACTIVE + and account["balance"] > Decimal("0") + ) + assert gbp_balance2 > gbp_balance + assert eur_balance2 == eur_balance - Decimal("1") + + # Exchange 1 EUR from GBP to EUR + response = await async_client.ForeignExchange.exchange_money( + request_id=str(uuid4()), + from_account_id=gbp_account["id"], + from_currency="GBP", + to_account_id=eur_account["id"], + to_currency="EUR", + from_amount=None, + to_amount=Decimal("1"), + reference="PyRevolut Test", + ) + await asyncio.sleep(random.randint(1, 3)) + assert response["state"] == EnumTransactionState.COMPLETED + + # Check balances + accounts = await async_client.Accounts.get_all_accounts() + await asyncio.sleep(random.randint(1, 3)) + gbp_balance3 = next( + account["balance"] + for account in accounts + if account["currency"] == "GBP" and account["state"] == EnumAccountState.ACTIVE + ) + eur_balance3 = next( + account["balance"] + for account in accounts + if account["currency"] == "EUR" and account["state"] == EnumAccountState.ACTIVE + ) + assert gbp_balance3 < gbp_balance2 + assert eur_balance3 == eur_balance + assert eur_balance3 > eur_balance2 diff --git a/tests/test_payment_drafts.py b/tests/test_payment_drafts.py new file mode 100644 index 0000000..3ff6d84 --- /dev/null +++ b/tests/test_payment_drafts.py @@ -0,0 +1,155 @@ +import time +import asyncio +from decimal import Decimal +import pytest +import random + +from pyrevolut.client import Client +from pyrevolut.api import EnumAccountState + + +def test_sync_get_all_payment_drafts(sync_client: Client): + """Test the sync `get_all_payment_drafts` payment drafts method""" + # Get all payment drafts + drafts = sync_client.PaymentDrafts.get_all_payment_drafts() + time.sleep(random.randint(1, 3)) + assert isinstance(drafts, dict) + for draft in drafts["payment_orders"]: + assert isinstance(draft, dict) + + +def test_sync_get_payment_draft(sync_client: Client): + """Test the sync `get_payment_draft` payment drafts method""" + # Get all payment drafts + drafts = sync_client.PaymentDrafts.get_all_payment_drafts() + time.sleep(random.randint(1, 3)) + assert isinstance(drafts, dict) + for draft in drafts["payment_orders"]: + assert isinstance(draft, dict) + draft_id = draft["id"] + + # Get the payment draft by ID + response = sync_client.PaymentDrafts.get_payment_draft(payment_draft_id=draft_id) + time.sleep(random.randint(1, 3)) + assert isinstance(response, dict) + assert response["id"] == draft_id + + +def test_sync_create_delete_payment_draft(sync_client: Client): + """Test the sync `create_payment_draft` and `delete_payment_draft` payment drafts methods""" + # Get all accounts + accounts = sync_client.Accounts.get_all_accounts() + time.sleep(random.randint(1, 3)) + + # Get GBP account + gbp_account = next( + account + for account in accounts + if account["currency"] == "GBP" + and account["state"] == EnumAccountState.ACTIVE + and account["balance"] > Decimal("0") + ) + + # Get recipients + recipients = sync_client.Counterparties.get_all_counterparties() + time.sleep(random.randint(1, 3)) + + # Get the first recipient in the UK (GB) + recipient = next(recipient for recipient in recipients if recipient["country"] == "GB") + + with pytest.raises( + ValueError, + match="Oops! An error occurred while processing your request. It has been logged for further investigation.", + ): + # Create a payment draft + response = sync_client.PaymentDrafts.create_payment_draft( + account_id=gbp_account["id"], + counterparty_ids=[recipient["id"]], + counterparty_account_ids=[None], + counterparty_card_ids=[None], + amounts=[Decimal("1.00")], + currencies=["GBP"], + references=["test"], + title="Test payment draft", + schedule_for="2025-01-01", + ) + time.sleep(random.randint(1, 3)) + + # Delete the payment draft + sync_client.PaymentDrafts.delete_payment_draft(payment_draft_id=response["id"]) + time.sleep(random.randint(1, 3)) + + +@pytest.mark.asyncio +async def test_async_get_all_payment_drafts(async_client: Client): + """Test the async `get_all_payment_drafts` payment drafts method""" + # Get all payment drafts + drafts = await async_client.PaymentDrafts.get_all_payment_drafts() + await asyncio.sleep(random.randint(1, 3)) + assert isinstance(drafts, dict) + for draft in drafts["payment_orders"]: + assert isinstance(draft, dict) + + +@pytest.mark.asyncio +async def test_async_get_payment_draft(async_client: Client): + """Test the async `get_payment_draft` payment drafts method""" + # Get all payment drafts + drafts = await async_client.PaymentDrafts.get_all_payment_drafts() + await asyncio.sleep(random.randint(1, 3)) + assert isinstance(drafts, dict) + for draft in drafts["payment_orders"]: + assert isinstance(draft, dict) + draft_id = draft["id"] + + # Get the payment draft by ID + response = await async_client.PaymentDrafts.get_payment_draft(payment_draft_id=draft_id) + await asyncio.sleep(random.randint(1, 3)) + assert isinstance(response, dict) + assert response["id"] == draft_id + + +@pytest.mark.asyncio +async def test_async_create_delete_payment_draft(async_client: Client): + """Test the async `create_payment_draft` and `delete_payment_draft` payment drafts methods""" + # Get all accounts + accounts = await async_client.Accounts.get_all_accounts() + await asyncio.sleep(random.randint(1, 3)) + + # Get GBP account + gbp_account = next( + account + for account in accounts + if account["currency"] == "GBP" + and account["state"] == EnumAccountState.ACTIVE + and account["balance"] > Decimal("0") + ) + + # Get recipients + recipients = await async_client.Counterparties.get_all_counterparties() + await asyncio.sleep(random.randint(1, 3)) + + # Get the first recipient in the UK (GB) + recipient = next(recipient for recipient in recipients if recipient["country"] == "GB") + + with pytest.raises( + ValueError, + match="Oops! An error occurred while processing your request. It has been logged for further investigation.", + ): + # Create a payment draft + response = await async_client.PaymentDrafts.create_payment_draft( + account_id=gbp_account["id"], + counterparty_ids=[recipient["id"]], + counterparty_account_ids=[None], + counterparty_card_ids=[None], + amounts=[Decimal("1.00")], + currencies=["GBP"], + references=["test"], + title="Test payment draft", + schedule_for="2025-01-01", + ) + await asyncio.sleep(random.randint(1, 3)) + + # Delete the payment draft + await async_client.PaymentDrafts.delete_payment_draft(payment_draft_id=response["id"]) + await asyncio.sleep(random.randint(1, 3)) diff --git a/tests/test_payout_links.py b/tests/test_payout_links.py new file mode 100644 index 0000000..41d42e8 --- /dev/null +++ b/tests/test_payout_links.py @@ -0,0 +1,197 @@ +import time +import asyncio +from decimal import Decimal +from uuid import uuid4 +import pytest +import random + +from pyrevolut.client import Client +from pyrevolut.api import ( + EnumPayoutLinkState, + EnumAccountState, + EnumPayoutLinkPaymentMethod, + EnumTransferReasonCode, +) + + +def test_sync_get_all_payout_links(sync_client: Client): + """Test the sync `get_all_payout_links` payout links method""" + # Get all payout links (no params) + links = sync_client.PayoutLinks.get_all_payout_links() + time.sleep(random.randint(1, 3)) + assert isinstance(links, list) + for link in links: + assert isinstance(link, dict) + + # Get all payout links (with params) + links = sync_client.PayoutLinks.get_all_payout_links( + state=EnumPayoutLinkState.ACTIVE, + created_before="2020-01-01", + limit=1, + ) + time.sleep(random.randint(1, 3)) + assert isinstance(links, list) + assert len(links) == 0 + + +def test_sync_get_payout_link(sync_client: Client): + """Test the sync `get_payout_link` payout links method""" + # Get all payout links + links = sync_client.PayoutLinks.get_all_payout_links() + time.sleep(random.randint(1, 3)) + assert isinstance(links, list) + for link in links: + assert isinstance(link, dict) + link_id = link["id"] + + # Get the payout link by ID + response = sync_client.PayoutLinks.get_payout_link(payout_link_id=link_id) + time.sleep(random.randint(1, 3)) + assert isinstance(response, dict) + assert response["id"] == link_id + + +def test_sync_create_cancel_payout_link(sync_client: Client): + """Test the sync `create_payout_link` and `cancel_payout_link` payout links methods""" + # Get all accounts + accounts = sync_client.Accounts.get_all_accounts() + time.sleep(random.randint(1, 3)) + + # Get GBP account + gbp_account = next( + account + for account in accounts + if account["currency"] == "GBP" + and account["state"] == EnumAccountState.ACTIVE + and account["balance"] > Decimal("0") + ) + + # Create a payout link + response = sync_client.PayoutLinks.create_payout_link( + counterparty_name="John Doe", + request_id=str(uuid4()), + account_id=gbp_account["id"], + amount=Decimal("1.00"), + currency="GBP", + reference="test payout link", + payout_methods=[ + EnumPayoutLinkPaymentMethod.REVOLUT, + EnumPayoutLinkPaymentMethod.BANK_ACCOUNT, + ], + expiry_period="P3D", # 3 days + transfer_reason_code=EnumTransferReasonCode.FAMILY, + ) + time.sleep(random.randint(1, 3)) + assert isinstance(response, dict) + assert response["state"] == EnumPayoutLinkState.ACTIVE + + # Get the payout link by ID + response = sync_client.PayoutLinks.get_payout_link(payout_link_id=response["id"]) + time.sleep(random.randint(1, 3)) + assert isinstance(response, dict) + assert response["id"] == response["id"] + assert response["state"] == EnumPayoutLinkState.ACTIVE + + # Cancel the payout link + sync_client.PayoutLinks.cancel_payout_link(payout_link_id=response["id"]) + time.sleep(random.randint(1, 3)) + + # Get the payout link by ID + response = sync_client.PayoutLinks.get_payout_link(payout_link_id=response["id"]) + time.sleep(random.randint(1, 3)) + assert isinstance(response, dict) + assert response["id"] == response["id"] + assert response["state"] == EnumPayoutLinkState.CANCELLED + + +@pytest.mark.asyncio +async def test_async_get_all_payout_links(async_client: Client): + """Test the async `get_all_payout_links` payout links method""" + # Get all payout links (no params) + links = await async_client.PayoutLinks.get_all_payout_links() + await asyncio.sleep(random.randint(1, 3)) + assert isinstance(links, list) + for link in links: + assert isinstance(link, dict) + + # Get all payout links (with params) + links = await async_client.PayoutLinks.get_all_payout_links( + state=EnumPayoutLinkState.ACTIVE, + created_before="2020-01-01", + limit=1, + ) + await asyncio.sleep(random.randint(1, 3)) + assert isinstance(links, list) + assert len(links) == 0 + + +@pytest.mark.asyncio +async def test_async_get_payout_link(async_client: Client): + """Test the async `get_payout_link` payout links method""" + # Get all payout links + links = await async_client.PayoutLinks.get_all_payout_links() + await asyncio.sleep(random.randint(1, 3)) + assert isinstance(links, list) + for link in links: + assert isinstance(link, dict) + link_id = link["id"] + + # Get the payout link by ID + response = await async_client.PayoutLinks.get_payout_link(payout_link_id=link_id) + await asyncio.sleep(random.randint(1, 3)) + assert isinstance(response, dict) + assert response["id"] == link_id + + +@pytest.mark.asyncio +async def test_async_create_cancel_payout_link(async_client: Client): + """Test the async `create_payout_link` and `cancel_payout_link` payout links methods""" + # Get all accounts + accounts = await async_client.Accounts.get_all_accounts() + await asyncio.sleep(random.randint(1, 3)) + + # Get GBP account + gbp_account = next( + account + for account in accounts + if account["currency"] == "GBP" + and account["state"] == EnumAccountState.ACTIVE + and account["balance"] > Decimal("0") + ) + + # Create a payout link + response = await async_client.PayoutLinks.create_payout_link( + counterparty_name="John Doe", + request_id=str(uuid4()), + account_id=gbp_account["id"], + amount=Decimal("1.00"), + currency="GBP", + reference="test payout link", + payout_methods=[ + EnumPayoutLinkPaymentMethod.REVOLUT, + EnumPayoutLinkPaymentMethod.BANK_ACCOUNT, + ], + expiry_period="P3D", # 3 days + transfer_reason_code=EnumTransferReasonCode.FAMILY, + ) + await asyncio.sleep(random.randint(1, 3)) + assert isinstance(response, dict) + assert response["state"] == EnumPayoutLinkState.ACTIVE + + # Get the payout link by ID + response = await async_client.PayoutLinks.get_payout_link(payout_link_id=response["id"]) + await asyncio.sleep(random.randint(1, 3)) + assert isinstance(response, dict) + assert response["id"] == response["id"] + assert response["state"] == EnumPayoutLinkState.ACTIVE + + # Cancel the payout link + await async_client.PayoutLinks.cancel_payout_link(payout_link_id=response["id"]) + await asyncio.sleep(random.randint(1, 3)) + + # Get the payout link by ID + response = await async_client.PayoutLinks.get_payout_link(payout_link_id=response["id"]) + await asyncio.sleep(random.randint(1, 3)) + assert isinstance(response, dict) + assert response["id"] == response["id"] + assert response["state"] == EnumPayoutLinkState.CANCELLED diff --git a/tests/test_simulations.py b/tests/test_simulations.py new file mode 100644 index 0000000..e506395 --- /dev/null +++ b/tests/test_simulations.py @@ -0,0 +1,130 @@ +import time +import asyncio +from decimal import Decimal +import pytest +import random + +from pyrevolut.client import Client +from pyrevolut.api import ( + EnumAccountState, + EnumTransactionState, +) + + +def test_sync_simulate_account_topup(sync_client: Client): + """Test the sync `simulate_account_topup` simulations method""" + + # Get all accounts + accounts = sync_client.Accounts.get_all_accounts() + time.sleep(random.randint(1, 3)) + + # Get GBP account + gbp_account = next( + account + for account in accounts + if account["currency"] == "GBP" and account["state"] == EnumAccountState.ACTIVE + ) + + # Get EUR account + eur_account = next( + account + for account in accounts + if account["currency"] == "EUR" and account["state"] == EnumAccountState.ACTIVE + ) + + # Simulate a top-up of the GBP account + response = sync_client.Simulations.simulate_account_topup( + account_id=gbp_account["id"], + amount=Decimal("1.00"), + currency="GBP", + reference="Sugar Daddy <3", + state=EnumTransactionState.COMPLETED, + ) + time.sleep(random.randint(1, 3)) + assert response["state"] == EnumTransactionState.COMPLETED + + # Get the GBP account by ID + account = sync_client.Accounts.get_account(account_id=gbp_account["id"]) + time.sleep(random.randint(1, 3)) + assert account["balance"] == gbp_account["balance"] + Decimal("1.00") + + # Simulate a top-up of the EUR account + response = sync_client.Simulations.simulate_account_topup( + account_id=eur_account["id"], + amount=Decimal("1.00"), + currency="EUR", + reference="Sugar Daddy <3", + state=EnumTransactionState.COMPLETED, + ) + time.sleep(random.randint(1, 3)) + + # Get the EUR account by ID + account = sync_client.Accounts.get_account(account_id=eur_account["id"]) + time.sleep(random.randint(1, 3)) + assert account["balance"] == eur_account["balance"] + Decimal("1.00") + + +def test_sync_simulate_transfer_state_update(sync_client: Client): + """Test the sync `simulate_transfer_state_update` simulations method""" + + # TODO: Implement the test + + +@pytest.mark.asyncio +async def test_async_simulate_account_topup(async_client: Client): + """Test the async `simulate_account_topup` simulations method""" + + # Get all accounts + accounts = await async_client.Accounts.get_all_accounts() + await asyncio.sleep(random.randint(1, 3)) + + # Get GBP account + gbp_account = next( + account + for account in accounts + if account["currency"] == "GBP" and account["state"] == EnumAccountState.ACTIVE + ) + + # Get EUR account + eur_account = next( + account + for account in accounts + if account["currency"] == "EUR" and account["state"] == EnumAccountState.ACTIVE + ) + + # Simulate a top-up of the GBP account + response = await async_client.Simulations.simulate_account_topup( + account_id=gbp_account["id"], + amount=Decimal("1.00"), + currency="GBP", + reference="Sugar Daddy <3", + state=EnumTransactionState.COMPLETED, + ) + await asyncio.sleep(random.randint(1, 3)) + assert response["state"] == EnumTransactionState.COMPLETED + + # Get the GBP account by ID + account = await async_client.Accounts.get_account(account_id=gbp_account["id"]) + await asyncio.sleep(random.randint(1, 3)) + assert account["balance"] == gbp_account["balance"] + Decimal("1.00") + + # Simulate a top-up of the EUR account + response = await async_client.Simulations.simulate_account_topup( + account_id=eur_account["id"], + amount=Decimal("1.00"), + currency="EUR", + reference="Sugar Daddy <3", + state=EnumTransactionState.COMPLETED, + ) + + # Get the EUR account by ID + account = await async_client.Accounts.get_account(account_id=eur_account["id"]) + await asyncio.sleep(random.randint(1, 3)) + assert account["balance"] == eur_account["balance"] + Decimal("1.00") + + +@pytest.mark.asyncio +async def test_async_simulate_transfer_state_update(async_client: Client): + """Test the async `simulate_transfer_state_update` simulations method""" + + # TODO: Implement the test diff --git a/tests/test_team_members.py b/tests/test_team_members.py new file mode 100644 index 0000000..a96808c --- /dev/null +++ b/tests/test_team_members.py @@ -0,0 +1,135 @@ +import time +import asyncio +import pytest +import random + +from pyrevolut.client import Client + + +def test_sync_get_team_members(sync_client: Client): + """Test the sync `get_team_members` team members method""" + + with pytest.raises(ValueError, match="This feature is not available in Sandbox."): + # Get all team members (no params) + team_members = sync_client.TeamMembers.get_team_members() + time.sleep(random.randint(1, 3)) + + assert isinstance(team_members, list) + assert all(isinstance(team_member, dict) for team_member in team_members) + + # Get all team members (with params) + team_members = sync_client.TeamMembers.get_team_members( + created_before="2020-01-01", + limit=10, + ) + time.sleep(random.randint(1, 3)) + assert isinstance(team_members, list) + assert len(team_members) == 0 + + +def test_sync_get_team_roles(sync_client: Client): + """Test the sync `get_team_roles` team members method""" + + with pytest.raises(ValueError, match="This feature is not available in Sandbox."): + # Get all team roles (no params) + team_roles = sync_client.TeamMembers.get_team_roles() + time.sleep(random.randint(1, 3)) + + assert isinstance(team_roles, list) + assert all(isinstance(team_role, dict) for team_role in team_roles) + + # Get all team roles (with params) + team_roles = sync_client.TeamMembers.get_team_roles( + created_before="2020-01-01", + limit=10, + ) + time.sleep(random.randint(1, 3)) + assert isinstance(team_roles, list) + assert len(team_roles) == 0 + + +def test_sync_invite_team_member(sync_client: Client): + """Test the sync `invite_team_member` team members method""" + + with pytest.raises(ValueError, match="This feature is not available in Sandbox."): + # Get all team roles + team_roles = sync_client.TeamMembers.get_team_roles() + time.sleep(random.randint(1, 3)) + + # Get the first team role + team_role = team_roles[0] + + # Invite a new team member + response = sync_client.TeamMembers.invite_team_member( + email="johndoe@example.com", + role_id=team_role["id"], + ) + time.sleep(random.randint(1, 3)) + assert isinstance(response, dict) + assert response["email"] == "johndoe@example.com" + + +@pytest.mark.asyncio +async def test_async_get_team_members(async_client: Client): + """Test the async `get_team_members` team members method""" + + with pytest.raises(ValueError, match="This feature is not available in Sandbox."): + # Get all team members (no params) + team_members = await async_client.TeamMembers.get_team_members() + await asyncio.sleep(random.randint(1, 3)) + + assert isinstance(team_members, list) + assert all(isinstance(team_member, dict) for team_member in team_members) + + # Get all team members (with params) + team_members = await async_client.TeamMembers.get_team_members( + created_before="2020-01-01", + limit=10, + ) + await asyncio.sleep(random.randint(1, 3)) + assert isinstance(team_members, list) + assert len(team_members) == 0 + + +@pytest.mark.asyncio +async def test_async_get_team_roles(async_client: Client): + """Test the async `get_team_roles` team members method""" + + with pytest.raises(ValueError, match="This feature is not available in Sandbox."): + # Get all team roles (no params) + team_roles = await async_client.TeamMembers.get_team_roles() + await asyncio.sleep(random.randint(1, 3)) + + assert isinstance(team_roles, list) + assert all(isinstance(team_role, dict) for team_role in team_roles) + + # Get all team roles (with params) + team_roles = await async_client.TeamMembers.get_team_roles( + created_before="2020-01-01", + limit=10, + ) + await asyncio.sleep(random.randint(1, 3)) + assert isinstance(team_roles, list) + assert len(team_roles) == 0 + + +@pytest.mark.asyncio +async def test_async_invite_team_member(async_client: Client): + """Test the async `invite_team_member` team members method""" + + with pytest.raises(ValueError, match="This feature is not available in Sandbox."): + # Get all team roles + team_roles = await async_client.TeamMembers.get_team_roles() + await asyncio.sleep(random.randint(1, 3)) + + # Get the first team role + team_role = team_roles[0] + + # Invite a new team member + response = await async_client.TeamMembers.invite_team_member( + email="johndoe@example.com", + role_id=team_role["id"], + ) + await asyncio.sleep(random.randint(1, 3)) + assert isinstance(response, dict) + assert response["email"] == "johndoe@example.com" diff --git a/tests/test_transactions.py b/tests/test_transactions.py new file mode 100644 index 0000000..b605d0b --- /dev/null +++ b/tests/test_transactions.py @@ -0,0 +1,109 @@ +import time +import asyncio +import random + +import pendulum +import pytest + +from pyrevolut.client import Client +from pyrevolut.api import EnumTransactionType + + +def test_sync_get_all_transactions(sync_client: Client): + """Test the sync `get_all_transactions` transactions method""" + # Get all transactions (no params) + transactions = sync_client.Transactions.get_all_transactions() + time.sleep(random.randint(1, 3)) + assert isinstance(transactions, list) + for transaction in transactions: + assert isinstance(transaction, dict) + + # Get all transactions (with params) + transactions = sync_client.Transactions.get_all_transactions( + from_datetime=pendulum.now().subtract(days=1), + to_datetime=pendulum.now(), + limit=10, + transaction_type=EnumTransactionType.TOPUP, + ) + time.sleep(random.randint(1, 3)) + assert isinstance(transactions, list) + for transaction in transactions: + assert isinstance(transaction, dict) + assert transaction["type"] == EnumTransactionType.TOPUP + + +def test_sync_get_transaction(sync_client: Client): + """Test the sync `get_transaction` transactions method""" + # Get all transactions (no params) + transactions = sync_client.Transactions.get_all_transactions(limit=3) + time.sleep(random.randint(1, 3)) + assert isinstance(transactions, list) + for transaction in transactions: + assert isinstance(transaction, dict) + + # Get the transactions by id + for transaction in transactions: + transaction_id = transaction["id"] + transaction = sync_client.Transactions.get_transaction(transaction_id=transaction_id) + time.sleep(random.randint(1, 3)) + assert isinstance(transaction, dict) + assert transaction["id"] == transaction_id + + # Get the transactions by request id + for transaction in transactions: + request_id = transaction["request_id"] + transaction = sync_client.Transactions.get_transaction(request_id=request_id) + time.sleep(random.randint(1, 3)) + assert isinstance(transaction, dict) + assert transaction["request_id"] == request_id + + +@pytest.mark.asyncio +async def test_async_get_all_transactions(async_client: Client): + """Test the async `get_all_transactions` transactions method""" + # Get all transactions (no params) + transactions = await async_client.Transactions.get_all_transactions() + await asyncio.sleep(random.randint(1, 3)) + assert isinstance(transactions, list) + for transaction in transactions: + assert isinstance(transaction, dict) + + # Get all transactions (with params) + transactions = await async_client.Transactions.get_all_transactions( + from_datetime=pendulum.now().subtract(days=1), + to_datetime=pendulum.now(), + limit=10, + transaction_type=EnumTransactionType.TOPUP, + ) + await asyncio.sleep(random.randint(1, 3)) + assert isinstance(transactions, list) + for transaction in transactions: + assert isinstance(transaction, dict) + assert transaction["type"] == EnumTransactionType.TOPUP + + +@pytest.mark.asyncio +async def test_async_get_transaction(async_client: Client): + """Test the async `get_transaction` transactions method""" + # Get all transactions (no params) + transactions = await async_client.Transactions.get_all_transactions(limit=3) + await asyncio.sleep(random.randint(1, 3)) + assert isinstance(transactions, list) + for transaction in transactions: + assert isinstance(transaction, dict) + + # Get the transactions by id + for transaction in transactions: + transaction_id = transaction["id"] + transaction = await async_client.Transactions.get_transaction(transaction_id=transaction_id) + await asyncio.sleep(random.randint(1, 3)) + assert isinstance(transaction, dict) + assert transaction["id"] == transaction_id + + # Get the transactions by request id + for transaction in transactions: + request_id = transaction["request_id"] + transaction = await async_client.Transactions.get_transaction(request_id=request_id) + await asyncio.sleep(random.randint(1, 3)) + assert isinstance(transaction, dict) + assert transaction["request_id"] == request_id diff --git a/tests/test_transfers.py b/tests/test_transfers.py new file mode 100644 index 0000000..e9ccd97 --- /dev/null +++ b/tests/test_transfers.py @@ -0,0 +1,321 @@ +import time +import asyncio +from uuid import uuid4 +from decimal import Decimal +import random + +import pytest + +from pyrevolut.client import Client +from pyrevolut.api import ( + EnumAccountState, + EnumTransactionState, + EnumTransferReasonCode, +) + + +def test_sync_get_transfer_reasons(sync_client: Client): + """Test the sync `get_transfer_reasons` transfers method""" + # Get all transfer reasons + transfer_reasons = sync_client.Transfers.get_transfer_reasons() + time.sleep(random.randint(1, 3)) + assert isinstance(transfer_reasons, list) + for transfer_reason in transfer_reasons: + assert isinstance(transfer_reason, dict) + + +def test_sync_move_money_between_accounts(sync_client: Client): + """Test the sync `move_money_between_accounts` transfers method""" + + # Get all accounts + accounts = sync_client.Accounts.get_all_accounts() + time.sleep(random.randint(1, 3)) + + # Get both GBP + gbp_account1 = next( + account + for account in accounts + if account["currency"] == "GBP" + and account["state"] == EnumAccountState.ACTIVE + and account["balance"] > Decimal("0") + ) + gbp_balance1 = gbp_account1["balance"] + gbp_account2 = next( + account + for account in accounts + if account["currency"] == "GBP" + and account["state"] == EnumAccountState.ACTIVE + and account["id"] != gbp_account1["id"] + ) + gbp_balance2 = gbp_account2["balance"] + + # Move 1 GBP from Account 1 to Account 2 + response = sync_client.Transfers.move_money_between_accounts( + request_id=str(uuid4()), + source_account_id=gbp_account1["id"], + target_account_id=gbp_account2["id"], + amount=Decimal("1"), + currency="GBP", + reference="PyRevolut Test", + ) + time.sleep(random.randint(1, 3)) + assert response["state"] == EnumTransactionState.COMPLETED + + # Check balances + accounts = sync_client.Accounts.get_all_accounts() + time.sleep(random.randint(1, 3)) + gbp_balance1_new = next( + account["balance"] for account in accounts if account["id"] == gbp_account1["id"] + ) + gbp_balance2_new = next( + account["balance"] for account in accounts if account["id"] == gbp_account2["id"] + ) + assert gbp_balance2_new == gbp_balance2 + Decimal("1") + assert gbp_balance1_new == gbp_balance1 - Decimal("1") + + # Move 1 GBP from Account 2 to Account 1 + response = sync_client.Transfers.move_money_between_accounts( + request_id=str(uuid4()), + source_account_id=gbp_account2["id"], + target_account_id=gbp_account1["id"], + amount=Decimal("1"), + currency="GBP", + reference="PyRevolut Test", + ) + time.sleep(random.randint(1, 3)) + assert response["state"] == EnumTransactionState.COMPLETED + + # Check balances + accounts = sync_client.Accounts.get_all_accounts() + time.sleep(random.randint(1, 3)) + gbp_balance1_new = next( + account["balance"] for account in accounts if account["id"] == gbp_account1["id"] + ) + gbp_balance2_new = next( + account["balance"] for account in accounts if account["id"] == gbp_account2["id"] + ) + assert gbp_balance2_new == gbp_balance2 + assert gbp_balance1_new == gbp_balance1 + + +def test_sync_create_transfer_to_another_account(sync_client: Client): + """Test the sync `create_transfer_to_another_account` transfers method""" + + # Get all accounts + accounts = sync_client.Accounts.get_all_accounts() + time.sleep(random.randint(1, 3)) + + # Get EUR account + eur_account = next( + account + for account in accounts + if account["currency"] == "EUR" + and account["state"] == EnumAccountState.ACTIVE + and account["balance"] + ) + eur_balance = eur_account["balance"] + + # If there is no EUR balance, simulate a top up + if eur_balance < Decimal("1"): + response = sync_client.Simulations.simulate_account_topup( + account_id=eur_account["id"], + amount=Decimal("1"), + currency="EUR", + reference="PyRevolut Test", + state=EnumTransactionState.COMPLETED, + ) + time.sleep(random.randint(1, 3)) + assert response["state"] == EnumTransactionState.COMPLETED + + # Get all counterparties + counterparties = sync_client.Counterparties.get_all_counterparties() + + # Get a EUR counterparty with an IBAN + eur_counterparties = [] + for counterparty in counterparties: + counterparty_accounts = counterparty.get("accounts") or [] + for account in counterparty_accounts: + if account.get("currency") == "EUR" and account.get("iban") is not None: + eur_counterparties.append(counterparty) + + # Get the first EUR counterparty + eur_counterparty = eur_counterparties[0] + eur_counterparty_account = [ + acc + for acc in eur_counterparty.get("accounts") or [] + if acc["currency"] == "EUR" and acc["iban"] is not None + ][0] + + # Create a transfer to the EUR counterparty + response = sync_client.Transfers.create_transfer_to_another_account( + request_id=str(uuid4()), + account_id=eur_account["id"], + counterparty_id=eur_counterparty["id"], + amount=Decimal("1"), + currency="EUR", + counterparty_account_id=eur_counterparty_account["id"], + reference="PyRevolut Test", + transfer_reason_code=EnumTransferReasonCode.FAMILY_SUPPORT, + ) + time.sleep(random.randint(1, 3)) + assert response["state"] == EnumTransactionState.PENDING + + # Check balance + account = sync_client.Accounts.get_account(account_id=eur_account["id"]) + time.sleep(random.randint(1, 3)) + assert account["balance"] == eur_balance - Decimal("1") + + +@pytest.mark.asyncio +async def test_async_get_transfer_reasons(async_client: Client): + """Test the async `get_transfer_reasons` transfers method""" + # Get all transfer reasons + transfer_reasons = await async_client.Transfers.get_transfer_reasons() + await asyncio.sleep(random.randint(1, 3)) + assert isinstance(transfer_reasons, list) + for transfer_reason in transfer_reasons: + assert isinstance(transfer_reason, dict) + + +@pytest.mark.asyncio +async def test_async_move_money_between_accounts(async_client: Client): + """Test the async `move_money_between_accounts` transfers method""" + + # Get all accounts + accounts = await async_client.Accounts.get_all_accounts() + await asyncio.sleep(random.randint(1, 3)) + + # Get both GBP + gbp_account1 = next( + account + for account in accounts + if account["currency"] == "GBP" + and account["state"] == EnumAccountState.ACTIVE + and account["balance"] > Decimal("0") + ) + gbp_balance1 = gbp_account1["balance"] + gbp_account2 = next( + account + for account in accounts + if account["currency"] == "GBP" + and account["state"] == EnumAccountState.ACTIVE + and account["id"] != gbp_account1["id"] + ) + gbp_balance2 = gbp_account2["balance"] + + # Move 1 GBP from Account 1 to Account 2 + response = await async_client.Transfers.move_money_between_accounts( + request_id=str(uuid4()), + source_account_id=gbp_account1["id"], + target_account_id=gbp_account2["id"], + amount=Decimal("1"), + currency="GBP", + reference="PyRevolut Test", + ) + await asyncio.sleep(random.randint(1, 3)) + assert response["state"] == EnumTransactionState.COMPLETED + + # Check balances + accounts = await async_client.Accounts.get_all_accounts() + await asyncio.sleep(random.randint(1, 3)) + gbp_balance1_new = next( + account["balance"] for account in accounts if account["id"] == gbp_account1["id"] + ) + gbp_balance2_new = next( + account["balance"] for account in accounts if account["id"] == gbp_account2["id"] + ) + assert gbp_balance2_new == gbp_balance2 + Decimal("1") + assert gbp_balance1_new == gbp_balance1 - Decimal("1") + + # Move 1 GBP from Account 2 to Account 1 + response = await async_client.Transfers.move_money_between_accounts( + request_id=str(uuid4()), + source_account_id=gbp_account2["id"], + target_account_id=gbp_account1["id"], + amount=Decimal("1"), + currency="GBP", + reference="PyRevolut Test", + ) + await asyncio.sleep(random.randint(1, 3)) + assert response["state"] == EnumTransactionState.COMPLETED + + # Check balances + accounts = await async_client.Accounts.get_all_accounts() + await asyncio.sleep(random.randint(1, 3)) + gbp_balance1_new = next( + account["balance"] for account in accounts if account["id"] == gbp_account1["id"] + ) + gbp_balance2_new = next( + account["balance"] for account in accounts if account["id"] == gbp_account2["id"] + ) + assert gbp_balance2_new == gbp_balance2 + assert gbp_balance1_new == gbp_balance1 + + +@pytest.mark.asyncio +async def test_async_create_transfer_to_another_account(async_client: Client): + """Test the async `create_transfer_to_another_account` transfers method""" + + # Get all accounts + accounts = await async_client.Accounts.get_all_accounts() + await asyncio.sleep(random.randint(1, 3)) + + # Get EUR account + eur_account = next( + account + for account in accounts + if account["currency"] == "EUR" + and account["state"] == EnumAccountState.ACTIVE + and account["balance"] + ) + eur_balance = eur_account["balance"] + + # If there is no EUR balance, simulate a top up + if eur_balance < Decimal("1"): + response = await async_client.Simulations.simulate_account_topup( + account_id=eur_account["id"], + amount=Decimal("1"), + currency="EUR", + reference="PyRevolut Test", + state=EnumTransactionState.COMPLETED, + ) + await asyncio.sleep(random.randint(1, 3)) + assert response["state"] == EnumTransactionState.COMPLETED + + # Get all counterparties + counterparties = await async_client.Counterparties.get_all_counterparties() + + # Get a EUR counterparty with an IBAN + eur_counterparties = [] + for counterparty in counterparties: + counterparty_accounts = counterparty.get("accounts") or [] + for account in counterparty_accounts: + if account.get("currency") == "EUR" and account.get("iban") is not None: + eur_counterparties.append(counterparty) + + # Get the first EUR counterparty + eur_counterparty = eur_counterparties[0] + eur_counterparty_account = [ + acc + for acc in eur_counterparty.get("accounts") or [] + if acc["currency"] == "EUR" and acc["iban"] is not None + ][0] + + # Create a transfer to the EUR counterparty + response = await async_client.Transfers.create_transfer_to_another_account( + request_id=str(uuid4()), + account_id=eur_account["id"], + counterparty_id=eur_counterparty["id"], + amount=Decimal("1"), + currency="EUR", + counterparty_account_id=eur_counterparty_account["id"], + reference="PyRevolut Test", + transfer_reason_code=EnumTransferReasonCode.FAMILY_SUPPORT, + ) + await asyncio.sleep(random.randint(1, 3)) + assert response["state"] == EnumTransactionState.PENDING + + # Check balance + account = await async_client.Accounts.get_account(account_id=eur_account["id"]) + await asyncio.sleep(random.randint(1, 3)) + assert account["balance"] == eur_balance - Decimal("1") diff --git a/tests/test_webhooks.py b/tests/test_webhooks.py new file mode 100644 index 0000000..7ef8b84 --- /dev/null +++ b/tests/test_webhooks.py @@ -0,0 +1,206 @@ +import time +import asyncio +import random + +import pytest + +from pyrevolut.client import Client +from pyrevolut.api import EnumWebhookEvent + + +def test_sync_get_all_webhooks(sync_client: Client): + """Test the sync `get_all_webhooks` webhooks method""" + # Get all webhooks + webhooks = sync_client.Webhooks.get_all_webhooks() + time.sleep(random.randint(1, 3)) + assert isinstance(webhooks, list) + for webhook in webhooks: + assert isinstance(webhook, dict) + + +def test_sync_get_webhook(sync_client: Client): + """Test the sync `get_webhook` webhooks method""" + # Get all webhooks + webhooks = sync_client.Webhooks.get_all_webhooks() + + # For each webhook, get the webhook + for webhook in webhooks: + webhook = sync_client.Webhooks.get_webhook(webhook_id=webhook["id"]) + time.sleep(random.randint(1, 3)) + assert isinstance(webhook, dict) + assert webhook["id"] == webhook["id"] + + +def test_sync_get_failed_webhook_events(sync_client: Client): + """Test the sync `get_failed_webhook_events` webhooks method""" + # Get all webhooks + webhooks = sync_client.Webhooks.get_all_webhooks() + + # For each webhook, get all failed webhooks + for webhook in webhooks: + failed_webhooks = sync_client.Webhooks.get_failed_webhook_events( + webhook_id=webhook["id"], limit=10 + ) + time.sleep(random.randint(1, 3)) + assert isinstance(failed_webhooks, list) + for failed_webhook in failed_webhooks: + assert isinstance(failed_webhook, dict) + + +def test_sync_create_update_rotate_delete_webhook(sync_client: Client): + """Test the sync `create`, `update`, `rotate` and `delete` webhooks methods""" + # Create a new webhook + webhook = sync_client.Webhooks.create_webhook( + url="https://example.com", + events=[ + EnumWebhookEvent.PAYOUT_LINK_CREATED, + ], + ) + time.sleep(random.randint(1, 3)) + assert isinstance(webhook, dict) + + # Get the webhook + webhook = sync_client.Webhooks.get_webhook(webhook_id=webhook["id"]) + time.sleep(random.randint(1, 3)) + assert isinstance(webhook, dict) + assert webhook["id"] == webhook["id"] + assert webhook["url"] is not None + assert EnumWebhookEvent.PAYOUT_LINK_CREATED in webhook["events"] + + # Update the webhook + updated_webhook = sync_client.Webhooks.update_webhook( + webhook_id=webhook["id"], + url=None, + events=[ + EnumWebhookEvent.PAYOUT_LINK_CREATED, + EnumWebhookEvent.PAYOUT_LINK_STATE_CHANGED, + ], + ) + time.sleep(random.randint(1, 3)) + assert isinstance(updated_webhook, dict) + + # Get the webhook + webhook = sync_client.Webhooks.get_webhook(webhook_id=webhook["id"]) + time.sleep(random.randint(1, 3)) + assert isinstance(webhook, dict) + assert webhook["id"] == webhook["id"] + assert webhook["url"] is not None + assert EnumWebhookEvent.PAYOUT_LINK_CREATED in webhook["events"] + assert EnumWebhookEvent.PAYOUT_LINK_STATE_CHANGED in webhook["events"] + + # Rotate the webhook + rotated_webhook = sync_client.Webhooks.rotate_webhook_secret( + webhook_id=webhook["id"], expiration_period="P1D" + ) + time.sleep(random.randint(1, 3)) + assert isinstance(rotated_webhook, dict) + + # Delete the webhook + sync_client.Webhooks.delete_webhook(webhook_id=webhook["id"]) + time.sleep(random.randint(1, 3)) + + # Get all webhooks + webhooks = sync_client.Webhooks.get_all_webhooks() + time.sleep(random.randint(1, 3)) + assert webhook["id"] not in [webhook["id"] for webhook in webhooks] + + +@pytest.mark.asyncio +async def test_async_get_all_webhooks(async_client: Client): + """Test the async `get_all_webhooks` webhooks method""" + # Get all webhooks + webhooks = await async_client.Webhooks.get_all_webhooks() + await asyncio.sleep(random.randint(1, 3)) + assert isinstance(webhooks, list) + for webhook in webhooks: + assert isinstance(webhook, dict) + + +@pytest.mark.asyncio +async def test_async_get_webhook(async_client: Client): + """Test the async `get_webhook` webhooks method""" + # Get all webhooks + webhooks = await async_client.Webhooks.get_all_webhooks() + + # For each webhook, get the webhook + for webhook in webhooks: + webhook = await async_client.Webhooks.get_webhook(webhook_id=webhook["id"]) + await asyncio.sleep(random.randint(1, 3)) + assert isinstance(webhook, dict) + assert webhook["id"] == webhook["id"] + + +@pytest.mark.asyncio +async def test_async_get_failed_webhook_events(async_client: Client): + """Test the async `get_failed_webhook_events` webhooks method""" + # Get all webhooks + webhooks = await async_client.Webhooks.get_all_webhooks() + + # For each webhook, get all failed webhooks + for webhook in webhooks: + failed_webhooks = await async_client.Webhooks.get_failed_webhook_events( + webhook_id=webhook["id"], limit=10 + ) + await asyncio.sleep(random.randint(1, 3)) + assert isinstance(failed_webhooks, list) + for failed_webhook in failed_webhooks: + assert isinstance(failed_webhook, dict) + + +@pytest.mark.asyncio +async def test_async_create_update_rotate_delete_webhook(async_client: Client): + """Test the async `create`, `update`, `rotate` and `delete` webhooks methods""" + # Create a new webhook + webhook = await async_client.Webhooks.create_webhook( + url="https://example.com", + events=[ + EnumWebhookEvent.PAYOUT_LINK_CREATED, + ], + ) + await asyncio.sleep(random.randint(1, 3)) + assert isinstance(webhook, dict) + + # Get the webhook + webhook = await async_client.Webhooks.get_webhook(webhook_id=webhook["id"]) + await asyncio.sleep(random.randint(1, 3)) + assert isinstance(webhook, dict) + assert webhook["id"] == webhook["id"] + assert webhook["url"] is not None + assert EnumWebhookEvent.PAYOUT_LINK_CREATED in webhook["events"] + + # Update the webhook + updated_webhook = await async_client.Webhooks.update_webhook( + webhook_id=webhook["id"], + url=None, + events=[ + EnumWebhookEvent.PAYOUT_LINK_CREATED, + EnumWebhookEvent.PAYOUT_LINK_STATE_CHANGED, + ], + ) + await asyncio.sleep(random.randint(1, 3)) + assert isinstance(updated_webhook, dict) + + # Get the webhook + webhook = await async_client.Webhooks.get_webhook(webhook_id=webhook["id"]) + await asyncio.sleep(random.randint(1, 3)) + assert isinstance(webhook, dict) + assert webhook["id"] == webhook["id"] + assert webhook["url"] is not None + assert EnumWebhookEvent.PAYOUT_LINK_CREATED in webhook["events"] + assert EnumWebhookEvent.PAYOUT_LINK_STATE_CHANGED in webhook["events"] + + # Rotate the webhook + rotated_webhook = await async_client.Webhooks.rotate_webhook_secret( + webhook_id=webhook["id"], expiration_period="P1D" + ) + await asyncio.sleep(random.randint(1, 3)) + assert isinstance(rotated_webhook, dict) + + # Delete the webhook + await async_client.Webhooks.delete_webhook(webhook_id=webhook["id"]) + await asyncio.sleep(random.randint(1, 3)) + + # Get all webhooks + webhooks = await async_client.Webhooks.get_all_webhooks() + await asyncio.sleep(random.randint(1, 3)) + assert webhook["id"] not in [webhook["id"] for webhook in webhooks]