From 02a5af9d4974ed4e75922246ccfa816cad5a74c7 Mon Sep 17 00:00:00 2001 From: Taslim Oseni Date: Fri, 13 Feb 2026 08:50:11 +0100 Subject: [PATCH 01/15] Add missing functions and test cases --- embed/client.py | 42 +++++ embed/resources/deposit.py | 49 ++++++ embed/resources/eurobonds.py | 76 +++++++++ embed/resources/fixed_notes.py | 202 +++++++++++++++++++++++ embed/resources/fixed_placements.py | 85 ++++++++++ embed/resources/flexible_savings.py | 155 +++++++++++++++++ embed/resources/integrations.py | 46 ++++++ embed/resources/withdrawal.py | 108 ++++++++++++ tests/resources/test_deposits.py | 22 +++ tests/resources/test_eurobonds.py | 25 +++ tests/resources/test_fixed_notes.py | 78 +++++++++ tests/resources/test_fixed_placements.py | 25 +++ tests/resources/test_flexible_savings.py | 70 ++++++++ tests/resources/test_withdrawals.py | 32 ++++ 14 files changed, 1015 insertions(+) create mode 100644 embed/resources/deposit.py create mode 100644 embed/resources/eurobonds.py create mode 100644 embed/resources/fixed_notes.py create mode 100644 embed/resources/fixed_placements.py create mode 100644 embed/resources/flexible_savings.py create mode 100644 embed/resources/integrations.py create mode 100644 embed/resources/withdrawal.py create mode 100644 tests/resources/test_deposits.py create mode 100644 tests/resources/test_eurobonds.py create mode 100644 tests/resources/test_fixed_notes.py create mode 100644 tests/resources/test_fixed_placements.py create mode 100644 tests/resources/test_flexible_savings.py create mode 100644 tests/resources/test_withdrawals.py diff --git a/embed/client.py b/embed/client.py index d28e191..69178b8 100644 --- a/embed/client.py +++ b/embed/client.py @@ -3,15 +3,22 @@ from embed.errors import CredentialsError from embed.resources.account import Account from embed.resources.asset import Asset +from embed.resources.deposit import Deposit +from embed.resources.eurobonds import Eurobond +from embed.resources.fixed_placements import FixedPlacement from embed.resources.index import Index +from embed.resources.integrations import Integration from embed.resources.investment import Investment from embed.resources.price import Price from embed.resources.saving import Saving +from embed.resources.flexible_savings import FlexibleSaving +from embed.resources.fixed_notes import FixedNote from embed.resources.settlement import Settlement from embed.resources.stock import Stock from embed.resources.stock_portfolio import StockPortfolio from embed.resources.trade import Trade from embed.resources.transaction import Transaction +from embed.resources.withdrawal import Withdrawal from embed.resources.misc import Misc from embed.resources.wallet import Wallet from embed.common import APISession @@ -61,14 +68,21 @@ def __init__( self._accounts = Account(self._session) self._assets = Asset(self._session) + self._deposits = Deposit(self._session) + self._eurobonds = Eurobond(self._session) + self._fixed_placements = FixedPlacement(self._session) self._investments = Investment(self._session) self._indexes = Index(self._session) + self._integrations = Integration(self._session) self._savings = Saving(self._session) + self._flexible_savings = FlexibleSaving(self._session) + self._fixed_notes = FixedNote(self._session) self._settlements = Settlement(self._session) self._stocks = Stock(self._session) self._stock_portfolios = StockPortfolio(self._session) self._trades = Trade(self._session) self._transactions = Transaction(self._session) + self._withdrawals = Withdrawal(self._session) self._prices = Price(self._session) self._wallets = Wallet(self._session) self._misc = Misc(self._session) @@ -85,6 +99,18 @@ def accounts(self): def assets(self): return self._assets + @property + def deposits(self): + return self._deposits + + @property + def eurobonds(self): + return self._eurobonds + + @property + def fixed_placements(self): + return self._fixed_placements + @property def investments(self): return self._investments @@ -93,10 +119,22 @@ def investments(self): def indexes(self): return self._indexes + @property + def integrations(self): + return self._integrations + @property def savings(self): return self._savings + @property + def flexible_savings(self): + return self._flexible_savings + + @property + def fixed_notes(self): + return self._fixed_notes + @property def settlements(self): return self._settlements @@ -117,6 +155,10 @@ def trades(self): def transactions(self): return self._transactions + @property + def withdrawals(self): + return self._withdrawals + @property def prices(self): return self._prices diff --git a/embed/resources/deposit.py b/embed/resources/deposit.py new file mode 100644 index 0000000..c5182c2 --- /dev/null +++ b/embed/resources/deposit.py @@ -0,0 +1,49 @@ +import json + +from embed.common import APIResponse + + +class Deposit(APIResponse): + """ + Handles all queries for Deposits including listing and retrieving. + """ + + def __init__(self, api_session): + super(Deposit, self).__init__() + self.base_url = f"{api_session.base_url}/api/{api_session.api_version}/" + self.token = api_session.token + self._headers.update({"Authorization": f"Bearer {self.token}"}) + + def list_deposits(self, **kwargs): + """ + Retrieve a list of all deposits. + + Args: + **kwargs: Arbitrary keyword arguments for filtering and pagination. + page_size (int): Optional. + page (int): Optional. + all (bool): Optional. If True, return all without pagination. + + Returns: + dict: The API response containing a list of deposits. + """ + query_path = self._format_query(kwargs) + method = "GET" + url = self.base_url + "deposits" + if query_path: + url = f"{url}?{query_path}" + return self.get_essential_details(method, url) + + def get_deposit(self, deposit_id): + """ + Retrieve details of a specific deposit. + + Args: + deposit_id (str): The unique identifier for the deposit. + + Returns: + dict: The API response containing deposit details. + """ + method = "GET" + url = self.base_url + f"deposits/{deposit_id}" + return self.get_essential_details(method, url) diff --git a/embed/resources/eurobonds.py b/embed/resources/eurobonds.py new file mode 100644 index 0000000..143aafc --- /dev/null +++ b/embed/resources/eurobonds.py @@ -0,0 +1,76 @@ +import json +from embed.common import APIResponse + +class Eurobond(APIResponse): + """ + Handles all queries for Eurobond investments. + """ + + def __init__(self, api_session): + super(Eurobond, self).__init__() + self.base_url = f"{api_session.base_url}/api/{api_session.api_version}/" + self.token = api_session.token + self._headers.update({"Authorization": f"Bearer {self.token}"}) + + def list_eurobonds(self, **kwargs): + """ + Retrieve a list of all eurobond investments. + """ + query_path = self._format_query(kwargs) + method = "GET" + url = self.base_url + "eurobonds" + if query_path: + url = f"{url}?{query_path}" + return self.get_essential_details(method, url) + + def get_eurobond(self, eurobond_id): + """ + Retrieve details of a specific eurobond. + """ + method = "GET" + url = self.base_url + f"eurobonds/{eurobond_id}" + return self.get_essential_details(method, url) + + def create_preview(self, **kwargs): + """ + Get a preview of a eurobond investment. + """ + method = "POST" + url = self.base_url + "eurobonds/preview" + payload = json.dumps(kwargs) + return self.get_essential_details(method, url, payload) + + def create_eurobond(self, **kwargs): + """ + Create a new eurobond investment. + """ + required = ["account_id", "asset_code", "amount"] + self._validate_kwargs(required, kwargs) + + if "idempotency_key" in kwargs.keys(): + self._headers.update( + {"Embed-Idempotency-Key": str(kwargs.pop("idempotency_key"))} + ) + + method = "POST" + url = self.base_url + "eurobonds" + payload = json.dumps(kwargs) + return self.get_essential_details(method, url, payload) + + def withdraw_preview(self, eurobond_id, **kwargs): + """ + Get a preview of a eurobond withdrawal. + """ + method = "POST" + url = self.base_url + f"eurobonds/{eurobond_id}/withdraw/preview" + payload = json.dumps(kwargs) + return self.get_essential_details(method, url, payload) + + def withdraw(self, eurobond_id, **kwargs): + """ + Withdraw from a eurobond investment. + """ + method = "POST" + url = self.base_url + f"eurobonds/{eurobond_id}/withdraw" + payload = json.dumps(kwargs) + return self.get_essential_details(method, url, payload) diff --git a/embed/resources/fixed_notes.py b/embed/resources/fixed_notes.py new file mode 100644 index 0000000..889d787 --- /dev/null +++ b/embed/resources/fixed_notes.py @@ -0,0 +1,202 @@ +import json + +from embed.common import APIResponse + + +class FixedNote(APIResponse): + """ + Handles all queries for Fixed Notes management including creation, withdrawal, rollover, and performance tracking. + """ + + def __init__(self, api_session): + super(FixedNote, self).__init__() + self.base_url = f"{api_session.base_url}/api/{api_session.api_version}/" + self.token = api_session.token + self._headers.update({"Authorization": f"Bearer {self.token}"}) + + def list_fixed_notes(self, **kwargs): + """ + Retrieve a list of all fixed notes. + + Args: + **kwargs: Arbitrary keyword arguments for pagination. + page_size (int): Optional. + page (int): Optional. + + Returns: + dict: The API response containing a list of fixed notes. + """ + query_path = self._format_query(kwargs) + method = "GET" + url = self.base_url + "fixed-notes" + if query_path: + url = f"{url}?{query_path}" + return self.get_essential_details(method, url) + + def get_fixed_note(self, fixed_note_id): + """ + Retrieve details of a specific fixed note. + + Args: + fixed_note_id (str): The unique identifier for the fixed note. + + Returns: + dict: The API response containing fixed note details. + """ + method = "GET" + url = self.base_url + f"fixed-notes/{fixed_note_id}" + return self.get_essential_details(method, url) + + def create_fixed_note(self, **kwargs): + """ + Create a new fixed note investment. + + Args: + **kwargs: Arbitrary keyword arguments. + account_id (str): Required. The unique identifier for the account. + asset_code (str): Required. The asset code for the fixed note. + tenor_in_months (int): Required. Duration in months. + amount_range (str): Required. Amount range (e.g., '10M-100M'). + auto_reinvest (bool): Optional. Whether to automatically reinvest. + idempotency_key (str): Optional. Unique key to prevent duplicate requests. + + Returns: + dict: The API response containing new fixed note details. + """ + required = ["account_id", "asset_code", "tenor_in_months", "amount_range"] + self._validate_kwargs(required, kwargs) + + if "idempotency_key" in kwargs.keys(): + self._headers.update( + {"Embed-Idempotency-Key": str(kwargs.pop("idempotency_key"))} + ) + + method = "POST" + url = self.base_url + "fixed-notes" + payload = json.dumps(kwargs) + return self.get_essential_details(method, url, payload) + + def get_fixed_note_rates(self, **kwargs): + """ + Retrieve fixed note rates. + + Args: + **kwargs: Arbitrary keyword arguments. + tenor_in_months (int): Required. Duration in months. + amount_range (str): Required. Amount range (e.g., '10M-100M'). + currency (str): Required. Currency code (e.g., 'NGN', 'USD'). + + Returns: + dict: The API response containing rate information. + """ + required = ["tenor_in_months", "amount_range", "currency"] + self._validate_kwargs(required, kwargs) + + query_path = "&".join(f"{k}={v}" for k, v in kwargs.items()) + method = "GET" + url = self.base_url + f"fixed-notes/rates?{query_path}" + return self.get_essential_details(method, url) + + def get_fixed_note_performance( + self, fixed_note_id: str, start_date: str = None, end_date: str = None, **kwargs + ): + """ + Retrieve performance timeseries for a fixed note. + + Args: + fixed_note_id (str): The unique identifier for the fixed note. + start_date (str): Optional. YYYY-MM-DD. + end_date (str): Optional. YYYY-MM-DD. + **kwargs: Additional filtering parameters. + + Returns: + dict: The API response containing performance data. + """ + if start_date: + kwargs["start_date"] = self._validate_date_string(start_date) + if end_date: + kwargs["end_date"] = self._validate_date_string(end_date) + + method = "GET" + url = self.base_url + f"fixed-notes/{fixed_note_id}/performance" + query_path = "&".join("{}={}".format(k, v) for k, v in kwargs.items()) + if query_path: + url = f"{url}?{query_path}" + return self.get_essential_details(method, url) + + def get_fixed_note_returns( + self, fixed_note_id: str, start_date: str = None, end_date: str = None, **kwargs + ): + """ + Retrieve returns history for a fixed note. + + Args: + fixed_note_id (str): The unique identifier for the fixed note. + start_date (str): Optional. YYYY-MM-DD. + end_date (str): Optional. YYYY-MM-DD. + **kwargs: Additional filtering parameters. + + Returns: + dict: The API response containing returns data. + """ + if start_date: + kwargs["start_date"] = self._validate_date_string(start_date) + if end_date: + kwargs["end_date"] = self._validate_date_string(end_date) + + method = "GET" + url = self.base_url + f"fixed-notes/{fixed_note_id}/returns" + query_path = "&".join("{}={}".format(k, v) for k, v in kwargs.items()) + if query_path: + url = f"{url}?{query_path}" + return self.get_essential_details(method, url) + + def withdraw(self, fixed_note_id, **kwargs): + """ + Withdraw from a fixed note. + + Args: + fixed_note_id (str): The unique identifier for the fixed note. + amount (float): Optional. Specific amount to withdraw. + liquidate_all (bool): Optional. Whether to liquidate all units. + same_day_if_mature (bool): Optional. Whether to process same day if mature. + + Returns: + dict: The API response containing withdrawal details. + """ + method = "POST" + url = self.base_url + f"fixed-notes/{fixed_note_id}/withdraw" + payload = json.dumps(kwargs) + return self.get_essential_details(method, url, payload) + + def rollover(self, fixed_note_id, tenor_in_months): + """ + Rollover a fixed note for an additional period. + + Args: + fixed_note_id (str): The unique identifier for the fixed note. + tenor_in_months (int): Additional duration in months. + + Returns: + dict: The API response containing rollover details. + """ + method = "POST" + url = self.base_url + f"fixed-notes/{fixed_note_id}/rollover" + payload = json.dumps({"tenor_in_months": tenor_in_months}) + return self.get_essential_details(method, url, payload) + + def update_auto_reinvest(self, fixed_note_id, auto_reinvest): + """ + Update the auto-reinvest status for a fixed note. + + Args: + fixed_note_id (str): The unique identifier for the fixed note. + auto_reinvest (bool): Whether to enable auto-reinvest. + + Returns: + dict: The API response containing updated fixed note details. + """ + method = "PUT" + url = self.base_url + f"fixed-notes/{fixed_note_id}/auto-reinvest" + payload = json.dumps({"auto_reinvest": auto_reinvest}) + return self.get_essential_details(method, url, payload) diff --git a/embed/resources/fixed_placements.py b/embed/resources/fixed_placements.py new file mode 100644 index 0000000..9d01040 --- /dev/null +++ b/embed/resources/fixed_placements.py @@ -0,0 +1,85 @@ +import json +from embed.common import APIResponse + +class FixedPlacement(APIResponse): + """ + Handles all queries for Fixed Placement investments. + """ + + def __init__(self, api_session): + super(FixedPlacement, self).__init__() + self.base_url = f"{api_session.base_url}/api/{api_session.api_version}/" + self.token = api_session.token + self._headers.update({"Authorization": f"Bearer {self.token}"}) + + def list_fixed_placements(self, **kwargs): + """ + Retrieve a list of all fixed placement investments. + """ + query_path = self._format_query(kwargs) + method = "GET" + url = self.base_url + "fixed-placements" + if query_path: + url = f"{url}?{query_path}" + return self.get_essential_details(method, url) + + def get_fixed_placement(self, fixed_placement_id): + """ + Retrieve details of a specific fixed placement. + """ + method = "GET" + url = self.base_url + f"fixed-placements/{fixed_placement_id}" + return self.get_essential_details(method, url) + + def create_preview(self, **kwargs): + """ + Get a preview of a fixed placement investment. + """ + method = "POST" + url = self.base_url + "fixed-placements/preview" + payload = json.dumps(kwargs) + return self.get_essential_details(method, url, payload) + + def create_fixed_placement(self, **kwargs): + """ + Create a new fixed placement investment. + """ + required = ["account_id", "asset_code", "amount"] + self._validate_kwargs(required, kwargs) + + if "idempotency_key" in kwargs.keys(): + self._headers.update( + {"Embed-Idempotency-Key": str(kwargs.pop("idempotency_key"))} + ) + + method = "POST" + url = self.base_url + "fixed-placements" + payload = json.dumps(kwargs) + return self.get_essential_details(method, url, payload) + + def top_up_preview(self, fixed_placement_id, **kwargs): + """ + Get a preview of a fixed placement top-up. + """ + method = "POST" + url = self.base_url + f"fixed-placements/{fixed_placement_id}/top-up/preview" + payload = json.dumps(kwargs) + return self.get_essential_details(method, url, payload) + + def withdraw_preview(self, fixed_placement_id, **kwargs): + """ + Get a preview of a fixed placement withdrawal. + """ + method = "POST" + url = self.base_url + f"fixed-placements/{fixed_placement_id}/withdraw/preview" + payload = json.dumps(kwargs) + return self.get_essential_details(method, url, payload) + + def withdraw(self, fixed_placement_id, **kwargs): + """ + Withdraw from a fixed placement investment. + """ + method = "POST" + url = self.base_url + f"fixed-placements/{fixed_placement_id}/withdraw" + payload = json.dumps(kwargs) + return self.get_essential_details(method, url, payload) diff --git a/embed/resources/flexible_savings.py b/embed/resources/flexible_savings.py new file mode 100644 index 0000000..782a2c2 --- /dev/null +++ b/embed/resources/flexible_savings.py @@ -0,0 +1,155 @@ +import json + +from embed.common import APIResponse + + +class FlexibleSaving(APIResponse): + """ + Handles all queries for Flexible Savings management including creation, withdrawal, and performance tracking. + """ + + def __init__(self, api_session): + super(FlexibleSaving, self).__init__() + self.base_url = f"{api_session.base_url}/api/{api_session.api_version}/" + self.token = api_session.token + self._headers.update({"Authorization": f"Bearer {self.token}"}) + + def list_flexible_savings(self, **kwargs): + """ + Retrieve a list of all flexible savings plans. + + Args: + **kwargs: Arbitrary keyword arguments for pagination. + page_size (int): Optional. + page (int): Optional. + + Returns: + dict: The API response containing a list of flexible savings. + """ + query_path = self._format_query(kwargs) + method = "GET" + url = self.base_url + "flexible-savings" + if query_path: + url = f"{url}?{query_path}" + return self.get_essential_details(method, url) + + def get_flexible_savings(self, flexible_savings_id): + """ + Retrieve details of a specific flexible savings plan. + + Args: + flexible_savings_id (str): The unique identifier for the flexible savings plan. + + Returns: + dict: The API response containing flexible savings details. + """ + method = "GET" + url = self.base_url + f"flexible-savings/{flexible_savings_id}" + return self.get_essential_details(method, url) + + def create_flexible_savings(self, **kwargs): + """ + Create a new flexible savings plan. + + Args: + **kwargs: Arbitrary keyword arguments. + account_id (str): Required. The unique identifier for the account. + currency_code (str): Required. Currency code (e.g., 'NGN', 'USD'). + idempotency_key (str): Optional. Unique key to prevent duplicate requests. + + Returns: + dict: The API response containing new flexible savings details. + """ + required = ["account_id", "currency_code"] + self._validate_kwargs(required, kwargs) + + if "idempotency_key" in kwargs.keys(): + self._headers.update( + {"Embed-Idempotency-Key": str(kwargs.pop("idempotency_key"))} + ) + + method = "POST" + url = self.base_url + "flexible-savings" + payload = json.dumps(kwargs) + return self.get_essential_details(method, url, payload) + + def get_flexible_savings_rates(self): + """ + Retrieve the current flexible savings interest rate. + + Returns: + dict: The API response containing rate information. + """ + method = "GET" + url = self.base_url + "flexible-savings/rates" + return self.get_essential_details(method, url) + + def get_flexible_savings_performance( + self, flexible_savings_id: str, start_date: str = None, end_date: str = None, **kwargs + ): + """ + Retrieve performance timeseries for a flexible savings plan. + + Args: + flexible_savings_id (str): The unique identifier for the flexible savings plan. + start_date (str): Optional. YYYY-MM-DD. + end_date (str): Optional. YYYY-MM-DD. + **kwargs: Additional filtering parameters. + + Returns: + dict: The API response containing performance data. + """ + if start_date: + kwargs["start_date"] = self._validate_date_string(start_date) + if end_date: + kwargs["end_date"] = self._validate_date_string(end_date) + + method = "GET" + url = self.base_url + f"flexible-savings/{flexible_savings_id}/performance" + query_path = "&".join("{}={}".format(k, v) for k, v in kwargs.items()) + if query_path: + url = f"{url}?{query_path}" + return self.get_essential_details(method, url) + + def get_flexible_savings_returns( + self, flexible_savings_id: str, start_date: str = None, end_date: str = None, **kwargs + ): + """ + Retrieve returns history for a flexible savings plan. + + Args: + flexible_savings_id (str): The unique identifier for the flexible savings plan. + start_date (str): Optional. YYYY-MM-DD. + end_date (str): Optional. YYYY-MM-DD. + **kwargs: Additional filtering parameters. + + Returns: + dict: The API response containing returns data. + """ + if start_date: + kwargs["start_date"] = self._validate_date_string(start_date) + if end_date: + kwargs["end_date"] = self._validate_date_string(end_date) + + method = "GET" + url = self.base_url + f"flexible-savings/{flexible_savings_id}/returns" + query_path = "&".join("{}={}".format(k, v) for k, v in kwargs.items()) + if query_path: + url = f"{url}?{query_path}" + return self.get_essential_details(method, url) + + def withdraw(self, flexible_savings_id, amount): + """ + Withdraw funds from a flexible savings plan. + + Args: + flexible_savings_id (str): The unique identifier for the flexible savings plan. + amount (float): The amount to withdraw. + + Returns: + dict: The API response containing withdrawal details. + """ + method = "POST" + url = self.base_url + f"flexible-savings/{flexible_savings_id}/withdraw" + payload = json.dumps({"amount": amount}) + return self.get_essential_details(method, url, payload) diff --git a/embed/resources/integrations.py b/embed/resources/integrations.py new file mode 100644 index 0000000..2830ae9 --- /dev/null +++ b/embed/resources/integrations.py @@ -0,0 +1,46 @@ +import json +from embed.common import APIResponse + +class Integration(APIResponse): + """ + Handles external integrations like Atomic and CSCS. + """ + + def __init__(self, api_session): + super(Integration, self).__init__() + self.base_url = f"{api_session.base_url}/api/{api_session.api_version}/integration/" + self.token = api_session.token + self._headers.update({"Authorization": f"Bearer {self.token}"}) + + def atomic_onboarding(self, **kwargs): + method = "POST" + url = self.base_url + "atomic/onboarding" + payload = json.dumps(kwargs) + return self.get_essential_details(method, url, payload) + + def get_atomic_constants(self): + method = "GET" + url = self.base_url + "atomic/constants" + return self.get_essential_details(method, url) + + def get_atomic_progress(self, account_id): + method = "POST" + url = self.base_url + "atomic/onboarding/progress" + payload = json.dumps({"account_id": account_id}) + return self.get_essential_details(method, url, payload) + + def get_atomic_agreement(self): + method = "GET" + url = self.base_url + "atomic/onboarding/agreement" + return self.get_essential_details(method, url) + + def cscs_onboarding(self, **kwargs): + method = "POST" + url = self.base_url + "cscs/onboarding" + payload = json.dumps(kwargs) + return self.get_essential_details(method, url, payload) + + def get_cscs_profile(self, account_id): + method = "GET" + url = self.base_url + f"cscs/onboarding?account_id={account_id}" + return self.get_essential_details(method, url) diff --git a/embed/resources/withdrawal.py b/embed/resources/withdrawal.py new file mode 100644 index 0000000..babad18 --- /dev/null +++ b/embed/resources/withdrawal.py @@ -0,0 +1,108 @@ +import json + +from embed.common import APIResponse + + +class Withdrawal(APIResponse): + """ + Handles all queries for Withdrawals including listing, retrieving, and managing withdrawal intents. + """ + + def __init__(self, api_session): + super(Withdrawal, self).__init__() + self.base_url = f"{api_session.base_url}/api/{api_session.api_version}/" + self.token = api_session.token + self._headers.update({"Authorization": f"Bearer {self.token}"}) + + def list_withdrawals(self, **kwargs): + """ + Retrieve a list of all withdrawals. + + Args: + **kwargs: Arbitrary keyword arguments for filtering and pagination. + page_size (int): Optional. + page (int): Optional. + all (bool): Optional. If True, return all without pagination. + + Returns: + dict: The API response containing a list of withdrawals. + """ + query_path = self._format_query(kwargs) + method = "GET" + url = self.base_url + "withdrawals" + if query_path: + url = f"{url}?{query_path}" + return self.get_essential_details(method, url) + + def get_withdrawal(self, withdrawal_id): + """ + Retrieve details of a specific withdrawal. + + Args: + withdrawal_id (str): The unique identifier for the withdrawal. + + Returns: + dict: The API response containing withdrawal details. + """ + method = "GET" + url = self.base_url + f"withdrawals/{withdrawal_id}" + return self.get_essential_details(method, url) + + def list_withdrawal_intents(self, **kwargs): + """ + Retrieve a list of all withdrawal intents. + + Args: + **kwargs: Arbitrary keyword arguments for filtering. + user_email (str): Optional. Filter by user email. + currency (str): Optional. Filter by currency. + + Returns: + dict: The API response containing a list of withdrawal intents. + """ + query_path = "&".join(f"{k}={v}" for k, v in kwargs.items()) + method = "GET" + url = self.base_url + "withdrawals/intents" + if query_path: + url = f"{url}?{query_path}" + return self.get_essential_details(method, url) + + def retry_withdrawal_intent(self, **kwargs): + """ + Retry a failed withdrawal intent. + + Args: + **kwargs: Arbitrary keyword arguments. + account_id (str): Required. The account ID. + withdrawal_intent_id (str): Required. The withdrawal intent ID. + + Returns: + dict: The API response containing retry details. + """ + required = ["account_id", "withdrawal_intent_id"] + self._validate_kwargs(required, kwargs) + + method = "POST" + url = self.base_url + "withdrawals/intents/retry" + payload = json.dumps(kwargs) + return self.get_essential_details(method, url, payload) + + def cancel_withdrawal_intent(self, **kwargs): + """ + Cancel a pending withdrawal intent. + + Args: + **kwargs: Arbitrary keyword arguments. + account_id (str): Required. The account ID. + withdrawal_intent_id (str): Required. The withdrawal intent ID. + + Returns: + dict: The API response containing cancellation details. + """ + required = ["account_id", "withdrawal_intent_id"] + self._validate_kwargs(required, kwargs) + + method = "POST" + url = self.base_url + "withdrawals/intents/cancel" + payload = json.dumps(kwargs) + return self.get_essential_details(method, url, payload) diff --git a/tests/resources/test_deposits.py b/tests/resources/test_deposits.py new file mode 100644 index 0000000..260c13a --- /dev/null +++ b/tests/resources/test_deposits.py @@ -0,0 +1,22 @@ +from embed.resources.deposit import Deposit +from unittest.mock import MagicMock, patch + +@patch("embed.common.APIResponse.get_essential_details") +def test_can_list_deposits(mock_get_essential_details, api_session): + d = Deposit(api_session) + mock_get_essential_details.return_value = MagicMock() + d.list_deposits(all=True) + d.get_essential_details.assert_called_with( + "GET", + f"{api_session.base_url}/api/{api_session.api_version}/deposits?all=True", + ) + +@patch("embed.common.APIResponse.get_essential_details") +def test_can_get_deposit(mock_get_essential_details, api_session): + d = Deposit(api_session) + mock_get_essential_details.return_value = MagicMock() + d.get_deposit("fake-d-id") + d.get_essential_details.assert_called_with( + "GET", + f"{api_session.base_url}/api/{api_session.api_version}/deposits/fake-d-id", + ) diff --git a/tests/resources/test_eurobonds.py b/tests/resources/test_eurobonds.py new file mode 100644 index 0000000..69a79f9 --- /dev/null +++ b/tests/resources/test_eurobonds.py @@ -0,0 +1,25 @@ +import json +from embed.resources.eurobonds import Eurobond +from unittest.mock import MagicMock, patch + +@patch("embed.common.APIResponse.get_essential_details") +def test_can_list_eurobonds(mock_get_essential_details, api_session): + eb = Eurobond(api_session) + mock_get_essential_details.return_value = MagicMock() + eb.list_eurobonds(account_id="fake-acc") + eb.get_essential_details.assert_called_with( + "GET", + f"{api_session.base_url}/api/{api_session.api_version}/eurobonds?account_id=fake-acc", + ) + +@patch("embed.common.APIResponse.get_essential_details") +def test_can_create_eurobond_preview(mock_get_essential_details, api_session): + eb = Eurobond(api_session) + mock_get_essential_details.return_value = MagicMock() + test_data = {"asset_code": "EB-1", "amount": 1000} + eb.create_preview(**test_data) + eb.get_essential_details.assert_called_with( + "POST", + f"{api_session.base_url}/api/{api_session.api_version}/eurobonds/preview", + json.dumps(test_data), + ) diff --git a/tests/resources/test_fixed_notes.py b/tests/resources/test_fixed_notes.py new file mode 100644 index 0000000..d829a35 --- /dev/null +++ b/tests/resources/test_fixed_notes.py @@ -0,0 +1,78 @@ +import json +from embed.resources.fixed_notes import FixedNote +from unittest.mock import MagicMock, patch + +@patch("embed.common.APIResponse.get_essential_details") +def test_can_list_fixed_notes(mock_get_essential_details, api_session): + fn = FixedNote(api_session) + mock_get_essential_details.return_value = MagicMock() + fn.list_fixed_notes(page_size=20) + fn.get_essential_details.assert_called_with( + "GET", + f"{api_session.base_url}/api/{api_session.api_version}/fixed-notes?page_size=20", + ) + +@patch("embed.common.APIResponse.get_essential_details") +def test_can_get_fixed_note(mock_get_essential_details, api_session): + fn = FixedNote(api_session) + mock_get_essential_details.return_value = MagicMock() + fn.get_fixed_note("fake-fn-id") + fn.get_essential_details.assert_called_with( + "GET", + f"{api_session.base_url}/api/{api_session.api_version}/fixed-notes/fake-fn-id", + ) + +@patch("embed.common.APIResponse.get_essential_details") +def test_can_create_fixed_note(mock_get_essential_details, api_session): + fn = FixedNote(api_session) + mock_get_essential_details.return_value = MagicMock() + test_data = { + "account_id": "fake-account-id", + "asset_code": "FN-ASSET", + "tenor_in_months": 12, + "amount_range": "10k-100k", + "idempotency_key": "test-key" + } + fn.create_fixed_note(**test_data) + fn.get_essential_details.assert_called_with( + "POST", + f"{api_session.base_url}/api/{api_session.api_version}/fixed-notes", + json.dumps({ + "account_id": "fake-account-id", + "asset_code": "FN-ASSET", + "tenor_in_months": 12, + "amount_range": "10k-100k" + }), + ) + +@patch("embed.common.APIResponse.get_essential_details") +def test_can_get_fixed_note_rates(mock_get_essential_details, api_session): + fn = FixedNote(api_session) + mock_get_essential_details.return_value = MagicMock() + fn.get_fixed_note_rates(tenor_in_months=6, amount_range="10k-100k", currency="NGN") + fn.get_essential_details.assert_called_with( + "GET", + f"{api_session.base_url}/api/{api_session.api_version}/fixed-notes/rates?tenor_in_months=6&amount_range=10k-100k¤cy=NGN", + ) + +@patch("embed.common.APIResponse.get_essential_details") +def test_can_withdraw_from_fixed_note(mock_get_essential_details, api_session): + fn = FixedNote(api_session) + mock_get_essential_details.return_value = MagicMock() + fn.withdraw("fake-fn-id", amount=5000) + fn.get_essential_details.assert_called_with( + "POST", + f"{api_session.base_url}/api/{api_session.api_version}/fixed-notes/fake-fn-id/withdraw", + json.dumps({"amount": 5000}), + ) + +@patch("embed.common.APIResponse.get_essential_details") +def test_can_rollover_fixed_note(mock_get_essential_details, api_session): + fn = FixedNote(api_session) + mock_get_essential_details.return_value = MagicMock() + fn.rollover("fake-fn-id", tenor_in_months=3) + fn.get_essential_details.assert_called_with( + "POST", + f"{api_session.base_url}/api/{api_session.api_version}/fixed-notes/fake-fn-id/rollover", + json.dumps({"tenor_in_months": 3}), + ) diff --git a/tests/resources/test_fixed_placements.py b/tests/resources/test_fixed_placements.py new file mode 100644 index 0000000..b00065c --- /dev/null +++ b/tests/resources/test_fixed_placements.py @@ -0,0 +1,25 @@ +import json +from embed.resources.fixed_placements import FixedPlacement +from unittest.mock import MagicMock, patch + +@patch("embed.common.APIResponse.get_essential_details") +def test_can_list_fixed_placements(mock_get_essential_details, api_session): + fp = FixedPlacement(api_session) + mock_get_essential_details.return_value = MagicMock() + fp.list_fixed_placements(account_id="fake-acc") + fp.get_essential_details.assert_called_with( + "GET", + f"{api_session.base_url}/api/{api_session.api_version}/fixed-placements?account_id=fake-acc", + ) + +@patch("embed.common.APIResponse.get_essential_details") +def test_can_create_fixed_placement_preview(mock_get_essential_details, api_session): + fp = FixedPlacement(api_session) + mock_get_essential_details.return_value = MagicMock() + test_data = {"asset_code": "FP-1", "amount": 5000} + fp.create_preview(**test_data) + fp.get_essential_details.assert_called_with( + "POST", + f"{api_session.base_url}/api/{api_session.api_version}/fixed-placements/preview", + json.dumps(test_data), + ) diff --git a/tests/resources/test_flexible_savings.py b/tests/resources/test_flexible_savings.py new file mode 100644 index 0000000..84ee526 --- /dev/null +++ b/tests/resources/test_flexible_savings.py @@ -0,0 +1,70 @@ +import json +from embed.resources.flexible_savings import FlexibleSaving +from unittest.mock import MagicMock, patch + +@patch("embed.common.APIResponse.get_essential_details") +def test_can_list_flexible_savings(mock_get_essential_details, api_session): + fs = FlexibleSaving(api_session) + mock_get_essential_details.return_value = MagicMock() + fs.list_flexible_savings(page_size=20) + fs.get_essential_details.assert_called_with( + "GET", + f"{api_session.base_url}/api/{api_session.api_version}/flexible-savings?page_size=20", + ) + +@patch("embed.common.APIResponse.get_essential_details") +def test_can_get_flexible_savings(mock_get_essential_details, api_session): + fs = FlexibleSaving(api_session) + mock_get_essential_details.return_value = MagicMock() + fs.get_flexible_savings("fake-fs-id") + fs.get_essential_details.assert_called_with( + "GET", + f"{api_session.base_url}/api/{api_session.api_version}/flexible-savings/fake-fs-id", + ) + +@patch("embed.common.APIResponse.get_essential_details") +def test_can_create_flexible_savings(mock_get_essential_details, api_session): + fs = FlexibleSaving(api_session) + mock_get_essential_details.return_value = MagicMock() + test_data = { + "account_id": "fake-account-id", + "currency_code": "NGN", + "idempotency_key": "test_id_key" + } + fs.create_flexible_savings(**test_data) + fs.get_essential_details.assert_called_with( + "POST", + f"{api_session.base_url}/api/{api_session.api_version}/flexible-savings", + json.dumps({"account_id": "fake-account-id", "currency_code": "NGN"}), + ) + +@patch("embed.common.APIResponse.get_essential_details") +def test_can_get_flexible_savings_rates(mock_get_essential_details, api_session): + fs = FlexibleSaving(api_session) + mock_get_essential_details.return_value = MagicMock() + fs.get_flexible_savings_rates() + fs.get_essential_details.assert_called_with( + "GET", + f"{api_session.base_url}/api/{api_session.api_version}/flexible-savings/rates", + ) + +@patch("embed.common.APIResponse.get_essential_details") +def test_can_get_flexible_savings_performance(mock_get_essential_details, api_session): + fs = FlexibleSaving(api_session) + mock_get_essential_details.return_value = MagicMock() + fs.get_flexible_savings_performance("fake-fs-id", start_date="2023-01-01") + fs.get_essential_details.assert_called_with( + "GET", + f"{api_session.base_url}/api/{api_session.api_version}/flexible-savings/fake-fs-id/performance?start_date=2023-01-01", + ) + +@patch("embed.common.APIResponse.get_essential_details") +def test_can_withdraw_from_flexible_savings(mock_get_essential_details, api_session): + fs = FlexibleSaving(api_session) + mock_get_essential_details.return_value = MagicMock() + fs.withdraw("fake-fs-id", amount=1000) + fs.get_essential_details.assert_called_with( + "POST", + f"{api_session.base_url}/api/{api_session.api_version}/flexible-savings/fake-fs-id/withdraw", + json.dumps({"amount": 1000}), + ) diff --git a/tests/resources/test_withdrawals.py b/tests/resources/test_withdrawals.py new file mode 100644 index 0000000..d11c3ca --- /dev/null +++ b/tests/resources/test_withdrawals.py @@ -0,0 +1,32 @@ +from embed.resources.withdrawal import Withdrawal +from unittest.mock import MagicMock, patch + +@patch("embed.common.APIResponse.get_essential_details") +def test_can_list_withdrawals(mock_get_essential_details, api_session): + w = Withdrawal(api_session) + mock_get_essential_details.return_value = MagicMock() + w.list_withdrawals(page=1) + w.get_essential_details.assert_called_with( + "GET", + f"{api_session.base_url}/api/{api_session.api_version}/withdrawals?page=1", + ) + +@patch("embed.common.APIResponse.get_essential_details") +def test_can_get_withdrawal(mock_get_essential_details, api_session): + w = Withdrawal(api_session) + mock_get_essential_details.return_value = MagicMock() + w.get_withdrawal("fake-w-id") + w.get_essential_details.assert_called_with( + "GET", + f"{api_session.base_url}/api/{api_session.api_version}/withdrawals/fake-w-id", + ) + +@patch("embed.common.APIResponse.get_essential_details") +def test_can_list_withdrawal_intents(mock_get_essential_details, api_session): + w = Withdrawal(api_session) + mock_get_essential_details.return_value = MagicMock() + w.list_withdrawal_intents(currency="NGN") + w.get_essential_details.assert_called_with( + "GET", + f"{api_session.base_url}/api/{api_session.api_version}/withdrawals/intents?currency=NGN", + ) From c61c0bc5a0377d8e07ebc8b60e5bdf15dce0ec7b Mon Sep 17 00:00:00 2001 From: Taslim Oseni Date: Fri, 13 Feb 2026 08:50:47 +0100 Subject: [PATCH 02/15] Bump app version --- embed/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/embed/version.py b/embed/version.py index cae7902..1e8a021 100644 --- a/embed/version.py +++ b/embed/version.py @@ -1,4 +1,4 @@ -__version__ = "2.1.1" +__version__ = "2.1.2" __author__ = "Cowrywise Developers" __license__ = "MIT" __copyright__ = "Copyright 2021-2026. Cowrywise" From 3df451692d8baac31120ab455c8ad04ec2fcbfb3 Mon Sep 17 00:00:00 2001 From: Taslim Oseni Date: Fri, 13 Feb 2026 09:00:46 +0100 Subject: [PATCH 03/15] Update ReadMe --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index ee751d9..98ca765 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,13 @@ investment = client.investment.create_investment( | `create_wallet(account_id=None, currency_code=None)` | `POST /wallets` | | `transfer(wallet_id=None, product_code=None, amount=None)` | `POST /wallets/:wallet_id/transfer` | | `get_wallet(wallet_id)` | `GET /wallets/:wallet_id` | +| `list_flexible_savings(**kwargs)` | `GET /flexible-savings` | +| `create_fixed_note(**kwargs)` | `POST /fixed-notes` | +| `list_eurobonds(**kwargs)` | `GET /eurobonds` | +| `list_fixed_placements(**kwargs)` | `GET /fixed-placements` | +| `list_withdrawals(**kwargs)` | `GET /withdrawals` | +| `cscs_onboarding(**kwargs)` | `POST /integration/cscs/onboarding` | +| `atomic_onboarding(**kwargs)` | `POST /integration/atomic/onboarding` | Check the [API reference](https://developers.cowrywise.com/reference) document for all resources and their respective endpoints. From 10f649a608380dad62d31d4afdd6a6237637356e Mon Sep 17 00:00:00 2001 From: Taslim Oseni Date: Fri, 13 Feb 2026 09:10:54 +0100 Subject: [PATCH 04/15] style: apply black formatting to resources and tests --- embed/resources/eurobonds.py | 1 + embed/resources/fixed_placements.py | 1 + embed/resources/flexible_savings.py | 12 ++++++++++-- embed/resources/integrations.py | 5 ++++- tests/resources/test_deposits.py | 1 + tests/resources/test_eurobonds.py | 1 + tests/resources/test_fixed_notes.py | 7 ++++++- tests/resources/test_fixed_placements.py | 1 + tests/resources/test_flexible_savings.py | 5 +++++ tests/resources/test_withdrawals.py | 2 ++ 10 files changed, 32 insertions(+), 4 deletions(-) diff --git a/embed/resources/eurobonds.py b/embed/resources/eurobonds.py index 143aafc..184cf75 100644 --- a/embed/resources/eurobonds.py +++ b/embed/resources/eurobonds.py @@ -1,6 +1,7 @@ import json from embed.common import APIResponse + class Eurobond(APIResponse): """ Handles all queries for Eurobond investments. diff --git a/embed/resources/fixed_placements.py b/embed/resources/fixed_placements.py index 9d01040..5ae3472 100644 --- a/embed/resources/fixed_placements.py +++ b/embed/resources/fixed_placements.py @@ -1,6 +1,7 @@ import json from embed.common import APIResponse + class FixedPlacement(APIResponse): """ Handles all queries for Fixed Placement investments. diff --git a/embed/resources/flexible_savings.py b/embed/resources/flexible_savings.py index 782a2c2..ce4c4f7 100644 --- a/embed/resources/flexible_savings.py +++ b/embed/resources/flexible_savings.py @@ -85,7 +85,11 @@ def get_flexible_savings_rates(self): return self.get_essential_details(method, url) def get_flexible_savings_performance( - self, flexible_savings_id: str, start_date: str = None, end_date: str = None, **kwargs + self, + flexible_savings_id: str, + start_date: str = None, + end_date: str = None, + **kwargs, ): """ Retrieve performance timeseries for a flexible savings plan. @@ -112,7 +116,11 @@ def get_flexible_savings_performance( return self.get_essential_details(method, url) def get_flexible_savings_returns( - self, flexible_savings_id: str, start_date: str = None, end_date: str = None, **kwargs + self, + flexible_savings_id: str, + start_date: str = None, + end_date: str = None, + **kwargs, ): """ Retrieve returns history for a flexible savings plan. diff --git a/embed/resources/integrations.py b/embed/resources/integrations.py index 2830ae9..fb7657a 100644 --- a/embed/resources/integrations.py +++ b/embed/resources/integrations.py @@ -1,6 +1,7 @@ import json from embed.common import APIResponse + class Integration(APIResponse): """ Handles external integrations like Atomic and CSCS. @@ -8,7 +9,9 @@ class Integration(APIResponse): def __init__(self, api_session): super(Integration, self).__init__() - self.base_url = f"{api_session.base_url}/api/{api_session.api_version}/integration/" + self.base_url = ( + f"{api_session.base_url}/api/{api_session.api_version}/integration/" + ) self.token = api_session.token self._headers.update({"Authorization": f"Bearer {self.token}"}) diff --git a/tests/resources/test_deposits.py b/tests/resources/test_deposits.py index 260c13a..7c28f07 100644 --- a/tests/resources/test_deposits.py +++ b/tests/resources/test_deposits.py @@ -11,6 +11,7 @@ def test_can_list_deposits(mock_get_essential_details, api_session): f"{api_session.base_url}/api/{api_session.api_version}/deposits?all=True", ) + @patch("embed.common.APIResponse.get_essential_details") def test_can_get_deposit(mock_get_essential_details, api_session): d = Deposit(api_session) diff --git a/tests/resources/test_eurobonds.py b/tests/resources/test_eurobonds.py index 69a79f9..94eb65d 100644 --- a/tests/resources/test_eurobonds.py +++ b/tests/resources/test_eurobonds.py @@ -12,6 +12,7 @@ def test_can_list_eurobonds(mock_get_essential_details, api_session): f"{api_session.base_url}/api/{api_session.api_version}/eurobonds?account_id=fake-acc", ) + @patch("embed.common.APIResponse.get_essential_details") def test_can_create_eurobond_preview(mock_get_essential_details, api_session): eb = Eurobond(api_session) diff --git a/tests/resources/test_fixed_notes.py b/tests/resources/test_fixed_notes.py index d829a35..6058c3e 100644 --- a/tests/resources/test_fixed_notes.py +++ b/tests/resources/test_fixed_notes.py @@ -12,6 +12,7 @@ def test_can_list_fixed_notes(mock_get_essential_details, api_session): f"{api_session.base_url}/api/{api_session.api_version}/fixed-notes?page_size=20", ) + @patch("embed.common.APIResponse.get_essential_details") def test_can_get_fixed_note(mock_get_essential_details, api_session): fn = FixedNote(api_session) @@ -22,6 +23,7 @@ def test_can_get_fixed_note(mock_get_essential_details, api_session): f"{api_session.base_url}/api/{api_session.api_version}/fixed-notes/fake-fn-id", ) + @patch("embed.common.APIResponse.get_essential_details") def test_can_create_fixed_note(mock_get_essential_details, api_session): fn = FixedNote(api_session) @@ -31,7 +33,7 @@ def test_can_create_fixed_note(mock_get_essential_details, api_session): "asset_code": "FN-ASSET", "tenor_in_months": 12, "amount_range": "10k-100k", - "idempotency_key": "test-key" + "idempotency_key": "test-key", } fn.create_fixed_note(**test_data) fn.get_essential_details.assert_called_with( @@ -45,6 +47,7 @@ def test_can_create_fixed_note(mock_get_essential_details, api_session): }), ) + @patch("embed.common.APIResponse.get_essential_details") def test_can_get_fixed_note_rates(mock_get_essential_details, api_session): fn = FixedNote(api_session) @@ -55,6 +58,7 @@ def test_can_get_fixed_note_rates(mock_get_essential_details, api_session): f"{api_session.base_url}/api/{api_session.api_version}/fixed-notes/rates?tenor_in_months=6&amount_range=10k-100k¤cy=NGN", ) + @patch("embed.common.APIResponse.get_essential_details") def test_can_withdraw_from_fixed_note(mock_get_essential_details, api_session): fn = FixedNote(api_session) @@ -66,6 +70,7 @@ def test_can_withdraw_from_fixed_note(mock_get_essential_details, api_session): json.dumps({"amount": 5000}), ) + @patch("embed.common.APIResponse.get_essential_details") def test_can_rollover_fixed_note(mock_get_essential_details, api_session): fn = FixedNote(api_session) diff --git a/tests/resources/test_fixed_placements.py b/tests/resources/test_fixed_placements.py index b00065c..11ae73b 100644 --- a/tests/resources/test_fixed_placements.py +++ b/tests/resources/test_fixed_placements.py @@ -12,6 +12,7 @@ def test_can_list_fixed_placements(mock_get_essential_details, api_session): f"{api_session.base_url}/api/{api_session.api_version}/fixed-placements?account_id=fake-acc", ) + @patch("embed.common.APIResponse.get_essential_details") def test_can_create_fixed_placement_preview(mock_get_essential_details, api_session): fp = FixedPlacement(api_session) diff --git a/tests/resources/test_flexible_savings.py b/tests/resources/test_flexible_savings.py index 84ee526..2f8a322 100644 --- a/tests/resources/test_flexible_savings.py +++ b/tests/resources/test_flexible_savings.py @@ -12,6 +12,7 @@ def test_can_list_flexible_savings(mock_get_essential_details, api_session): f"{api_session.base_url}/api/{api_session.api_version}/flexible-savings?page_size=20", ) + @patch("embed.common.APIResponse.get_essential_details") def test_can_get_flexible_savings(mock_get_essential_details, api_session): fs = FlexibleSaving(api_session) @@ -22,6 +23,7 @@ def test_can_get_flexible_savings(mock_get_essential_details, api_session): f"{api_session.base_url}/api/{api_session.api_version}/flexible-savings/fake-fs-id", ) + @patch("embed.common.APIResponse.get_essential_details") def test_can_create_flexible_savings(mock_get_essential_details, api_session): fs = FlexibleSaving(api_session) @@ -38,6 +40,7 @@ def test_can_create_flexible_savings(mock_get_essential_details, api_session): json.dumps({"account_id": "fake-account-id", "currency_code": "NGN"}), ) + @patch("embed.common.APIResponse.get_essential_details") def test_can_get_flexible_savings_rates(mock_get_essential_details, api_session): fs = FlexibleSaving(api_session) @@ -48,6 +51,7 @@ def test_can_get_flexible_savings_rates(mock_get_essential_details, api_session) f"{api_session.base_url}/api/{api_session.api_version}/flexible-savings/rates", ) + @patch("embed.common.APIResponse.get_essential_details") def test_can_get_flexible_savings_performance(mock_get_essential_details, api_session): fs = FlexibleSaving(api_session) @@ -58,6 +62,7 @@ def test_can_get_flexible_savings_performance(mock_get_essential_details, api_se f"{api_session.base_url}/api/{api_session.api_version}/flexible-savings/fake-fs-id/performance?start_date=2023-01-01", ) + @patch("embed.common.APIResponse.get_essential_details") def test_can_withdraw_from_flexible_savings(mock_get_essential_details, api_session): fs = FlexibleSaving(api_session) diff --git a/tests/resources/test_withdrawals.py b/tests/resources/test_withdrawals.py index d11c3ca..4d52b2b 100644 --- a/tests/resources/test_withdrawals.py +++ b/tests/resources/test_withdrawals.py @@ -11,6 +11,7 @@ def test_can_list_withdrawals(mock_get_essential_details, api_session): f"{api_session.base_url}/api/{api_session.api_version}/withdrawals?page=1", ) + @patch("embed.common.APIResponse.get_essential_details") def test_can_get_withdrawal(mock_get_essential_details, api_session): w = Withdrawal(api_session) @@ -21,6 +22,7 @@ def test_can_get_withdrawal(mock_get_essential_details, api_session): f"{api_session.base_url}/api/{api_session.api_version}/withdrawals/fake-w-id", ) + @patch("embed.common.APIResponse.get_essential_details") def test_can_list_withdrawal_intents(mock_get_essential_details, api_session): w = Withdrawal(api_session) From 6a9c09178425f859445a4fcc985f1e630a1db7cd Mon Sep 17 00:00:00 2001 From: Taslim Oseni Date: Fri, 13 Feb 2026 09:14:14 +0100 Subject: [PATCH 05/15] Fix pre-commit checks --- tests/resources/test_deposits.py | 1 + tests/resources/test_eurobonds.py | 1 + tests/resources/test_fixed_notes.py | 15 +++++++++------ tests/resources/test_fixed_placements.py | 1 + tests/resources/test_flexible_savings.py | 3 ++- tests/resources/test_withdrawals.py | 1 + 6 files changed, 15 insertions(+), 7 deletions(-) diff --git a/tests/resources/test_deposits.py b/tests/resources/test_deposits.py index 7c28f07..de8b905 100644 --- a/tests/resources/test_deposits.py +++ b/tests/resources/test_deposits.py @@ -1,6 +1,7 @@ from embed.resources.deposit import Deposit from unittest.mock import MagicMock, patch + @patch("embed.common.APIResponse.get_essential_details") def test_can_list_deposits(mock_get_essential_details, api_session): d = Deposit(api_session) diff --git a/tests/resources/test_eurobonds.py b/tests/resources/test_eurobonds.py index 94eb65d..0fb9993 100644 --- a/tests/resources/test_eurobonds.py +++ b/tests/resources/test_eurobonds.py @@ -2,6 +2,7 @@ from embed.resources.eurobonds import Eurobond from unittest.mock import MagicMock, patch + @patch("embed.common.APIResponse.get_essential_details") def test_can_list_eurobonds(mock_get_essential_details, api_session): eb = Eurobond(api_session) diff --git a/tests/resources/test_fixed_notes.py b/tests/resources/test_fixed_notes.py index 6058c3e..730a0e5 100644 --- a/tests/resources/test_fixed_notes.py +++ b/tests/resources/test_fixed_notes.py @@ -2,6 +2,7 @@ from embed.resources.fixed_notes import FixedNote from unittest.mock import MagicMock, patch + @patch("embed.common.APIResponse.get_essential_details") def test_can_list_fixed_notes(mock_get_essential_details, api_session): fn = FixedNote(api_session) @@ -39,12 +40,14 @@ def test_can_create_fixed_note(mock_get_essential_details, api_session): fn.get_essential_details.assert_called_with( "POST", f"{api_session.base_url}/api/{api_session.api_version}/fixed-notes", - json.dumps({ - "account_id": "fake-account-id", - "asset_code": "FN-ASSET", - "tenor_in_months": 12, - "amount_range": "10k-100k" - }), + json.dumps( + { + "account_id": "fake-account-id", + "asset_code": "FN-ASSET", + "tenor_in_months": 12, + "amount_range": "10k-100k", + } + ), ) diff --git a/tests/resources/test_fixed_placements.py b/tests/resources/test_fixed_placements.py index 11ae73b..2382ba5 100644 --- a/tests/resources/test_fixed_placements.py +++ b/tests/resources/test_fixed_placements.py @@ -2,6 +2,7 @@ from embed.resources.fixed_placements import FixedPlacement from unittest.mock import MagicMock, patch + @patch("embed.common.APIResponse.get_essential_details") def test_can_list_fixed_placements(mock_get_essential_details, api_session): fp = FixedPlacement(api_session) diff --git a/tests/resources/test_flexible_savings.py b/tests/resources/test_flexible_savings.py index 2f8a322..2561341 100644 --- a/tests/resources/test_flexible_savings.py +++ b/tests/resources/test_flexible_savings.py @@ -2,6 +2,7 @@ from embed.resources.flexible_savings import FlexibleSaving from unittest.mock import MagicMock, patch + @patch("embed.common.APIResponse.get_essential_details") def test_can_list_flexible_savings(mock_get_essential_details, api_session): fs = FlexibleSaving(api_session) @@ -31,7 +32,7 @@ def test_can_create_flexible_savings(mock_get_essential_details, api_session): test_data = { "account_id": "fake-account-id", "currency_code": "NGN", - "idempotency_key": "test_id_key" + "idempotency_key": "test_id_key", } fs.create_flexible_savings(**test_data) fs.get_essential_details.assert_called_with( diff --git a/tests/resources/test_withdrawals.py b/tests/resources/test_withdrawals.py index 4d52b2b..c4d61d1 100644 --- a/tests/resources/test_withdrawals.py +++ b/tests/resources/test_withdrawals.py @@ -1,6 +1,7 @@ from embed.resources.withdrawal import Withdrawal from unittest.mock import MagicMock, patch + @patch("embed.common.APIResponse.get_essential_details") def test_can_list_withdrawals(mock_get_essential_details, api_session): w = Withdrawal(api_session) From 881030664bbe97fc309f48fc359e96c08dde0f21 Mon Sep 17 00:00:00 2001 From: Taslim Oseni Date: Sun, 15 Feb 2026 11:43:40 +0100 Subject: [PATCH 06/15] Update embed/version.py Co-authored-by: Chima Ataman --- embed/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/embed/version.py b/embed/version.py index 1e8a021..c9627cd 100644 --- a/embed/version.py +++ b/embed/version.py @@ -1,4 +1,4 @@ -__version__ = "2.1.2" +__version__ = "2.2.0" __author__ = "Cowrywise Developers" __license__ = "MIT" __copyright__ = "Copyright 2021-2026. Cowrywise" From 807a5bfabeb6e5f5e5178762c2586c89c0a6fa37 Mon Sep 17 00:00:00 2001 From: Taslim Oseni Date: Sun, 15 Feb 2026 16:09:17 +0100 Subject: [PATCH 07/15] Remove Eurobonds, Fixed Placements and Atomic Integration functionalities --- README.md | 5 +- embed/client.py | 11 ---- embed/resources/eurobonds.py | 77 -------------------------- embed/resources/fixed_placements.py | 86 ----------------------------- embed/resources/integrations.py | 22 -------- 5 files changed, 1 insertion(+), 200 deletions(-) delete mode 100644 embed/resources/eurobonds.py delete mode 100644 embed/resources/fixed_placements.py diff --git a/README.md b/README.md index 98ca765..ec23502 100644 --- a/README.md +++ b/README.md @@ -94,11 +94,8 @@ investment = client.investment.create_investment( | `get_wallet(wallet_id)` | `GET /wallets/:wallet_id` | | `list_flexible_savings(**kwargs)` | `GET /flexible-savings` | | `create_fixed_note(**kwargs)` | `POST /fixed-notes` | -| `list_eurobonds(**kwargs)` | `GET /eurobonds` | -| `list_fixed_placements(**kwargs)` | `GET /fixed-placements` | + | `list_withdrawals(**kwargs)` | `GET /withdrawals` | -| `cscs_onboarding(**kwargs)` | `POST /integration/cscs/onboarding` | -| `atomic_onboarding(**kwargs)` | `POST /integration/atomic/onboarding` | Check the [API reference](https://developers.cowrywise.com/reference) document for all resources and their respective endpoints. diff --git a/embed/client.py b/embed/client.py index 69178b8..f0f1490 100644 --- a/embed/client.py +++ b/embed/client.py @@ -4,8 +4,6 @@ from embed.resources.account import Account from embed.resources.asset import Asset from embed.resources.deposit import Deposit -from embed.resources.eurobonds import Eurobond -from embed.resources.fixed_placements import FixedPlacement from embed.resources.index import Index from embed.resources.integrations import Integration from embed.resources.investment import Investment @@ -69,8 +67,6 @@ def __init__( self._accounts = Account(self._session) self._assets = Asset(self._session) self._deposits = Deposit(self._session) - self._eurobonds = Eurobond(self._session) - self._fixed_placements = FixedPlacement(self._session) self._investments = Investment(self._session) self._indexes = Index(self._session) self._integrations = Integration(self._session) @@ -103,13 +99,6 @@ def assets(self): def deposits(self): return self._deposits - @property - def eurobonds(self): - return self._eurobonds - - @property - def fixed_placements(self): - return self._fixed_placements @property def investments(self): diff --git a/embed/resources/eurobonds.py b/embed/resources/eurobonds.py deleted file mode 100644 index 184cf75..0000000 --- a/embed/resources/eurobonds.py +++ /dev/null @@ -1,77 +0,0 @@ -import json -from embed.common import APIResponse - - -class Eurobond(APIResponse): - """ - Handles all queries for Eurobond investments. - """ - - def __init__(self, api_session): - super(Eurobond, self).__init__() - self.base_url = f"{api_session.base_url}/api/{api_session.api_version}/" - self.token = api_session.token - self._headers.update({"Authorization": f"Bearer {self.token}"}) - - def list_eurobonds(self, **kwargs): - """ - Retrieve a list of all eurobond investments. - """ - query_path = self._format_query(kwargs) - method = "GET" - url = self.base_url + "eurobonds" - if query_path: - url = f"{url}?{query_path}" - return self.get_essential_details(method, url) - - def get_eurobond(self, eurobond_id): - """ - Retrieve details of a specific eurobond. - """ - method = "GET" - url = self.base_url + f"eurobonds/{eurobond_id}" - return self.get_essential_details(method, url) - - def create_preview(self, **kwargs): - """ - Get a preview of a eurobond investment. - """ - method = "POST" - url = self.base_url + "eurobonds/preview" - payload = json.dumps(kwargs) - return self.get_essential_details(method, url, payload) - - def create_eurobond(self, **kwargs): - """ - Create a new eurobond investment. - """ - required = ["account_id", "asset_code", "amount"] - self._validate_kwargs(required, kwargs) - - if "idempotency_key" in kwargs.keys(): - self._headers.update( - {"Embed-Idempotency-Key": str(kwargs.pop("idempotency_key"))} - ) - - method = "POST" - url = self.base_url + "eurobonds" - payload = json.dumps(kwargs) - return self.get_essential_details(method, url, payload) - - def withdraw_preview(self, eurobond_id, **kwargs): - """ - Get a preview of a eurobond withdrawal. - """ - method = "POST" - url = self.base_url + f"eurobonds/{eurobond_id}/withdraw/preview" - payload = json.dumps(kwargs) - return self.get_essential_details(method, url, payload) - - def withdraw(self, eurobond_id, **kwargs): - """ - Withdraw from a eurobond investment. - """ - method = "POST" - url = self.base_url + f"eurobonds/{eurobond_id}/withdraw" - payload = json.dumps(kwargs) - return self.get_essential_details(method, url, payload) diff --git a/embed/resources/fixed_placements.py b/embed/resources/fixed_placements.py deleted file mode 100644 index 5ae3472..0000000 --- a/embed/resources/fixed_placements.py +++ /dev/null @@ -1,86 +0,0 @@ -import json -from embed.common import APIResponse - - -class FixedPlacement(APIResponse): - """ - Handles all queries for Fixed Placement investments. - """ - - def __init__(self, api_session): - super(FixedPlacement, self).__init__() - self.base_url = f"{api_session.base_url}/api/{api_session.api_version}/" - self.token = api_session.token - self._headers.update({"Authorization": f"Bearer {self.token}"}) - - def list_fixed_placements(self, **kwargs): - """ - Retrieve a list of all fixed placement investments. - """ - query_path = self._format_query(kwargs) - method = "GET" - url = self.base_url + "fixed-placements" - if query_path: - url = f"{url}?{query_path}" - return self.get_essential_details(method, url) - - def get_fixed_placement(self, fixed_placement_id): - """ - Retrieve details of a specific fixed placement. - """ - method = "GET" - url = self.base_url + f"fixed-placements/{fixed_placement_id}" - return self.get_essential_details(method, url) - - def create_preview(self, **kwargs): - """ - Get a preview of a fixed placement investment. - """ - method = "POST" - url = self.base_url + "fixed-placements/preview" - payload = json.dumps(kwargs) - return self.get_essential_details(method, url, payload) - - def create_fixed_placement(self, **kwargs): - """ - Create a new fixed placement investment. - """ - required = ["account_id", "asset_code", "amount"] - self._validate_kwargs(required, kwargs) - - if "idempotency_key" in kwargs.keys(): - self._headers.update( - {"Embed-Idempotency-Key": str(kwargs.pop("idempotency_key"))} - ) - - method = "POST" - url = self.base_url + "fixed-placements" - payload = json.dumps(kwargs) - return self.get_essential_details(method, url, payload) - - def top_up_preview(self, fixed_placement_id, **kwargs): - """ - Get a preview of a fixed placement top-up. - """ - method = "POST" - url = self.base_url + f"fixed-placements/{fixed_placement_id}/top-up/preview" - payload = json.dumps(kwargs) - return self.get_essential_details(method, url, payload) - - def withdraw_preview(self, fixed_placement_id, **kwargs): - """ - Get a preview of a fixed placement withdrawal. - """ - method = "POST" - url = self.base_url + f"fixed-placements/{fixed_placement_id}/withdraw/preview" - payload = json.dumps(kwargs) - return self.get_essential_details(method, url, payload) - - def withdraw(self, fixed_placement_id, **kwargs): - """ - Withdraw from a fixed placement investment. - """ - method = "POST" - url = self.base_url + f"fixed-placements/{fixed_placement_id}/withdraw" - payload = json.dumps(kwargs) - return self.get_essential_details(method, url, payload) diff --git a/embed/resources/integrations.py b/embed/resources/integrations.py index fb7657a..f372c2f 100644 --- a/embed/resources/integrations.py +++ b/embed/resources/integrations.py @@ -15,28 +15,6 @@ def __init__(self, api_session): self.token = api_session.token self._headers.update({"Authorization": f"Bearer {self.token}"}) - def atomic_onboarding(self, **kwargs): - method = "POST" - url = self.base_url + "atomic/onboarding" - payload = json.dumps(kwargs) - return self.get_essential_details(method, url, payload) - - def get_atomic_constants(self): - method = "GET" - url = self.base_url + "atomic/constants" - return self.get_essential_details(method, url) - - def get_atomic_progress(self, account_id): - method = "POST" - url = self.base_url + "atomic/onboarding/progress" - payload = json.dumps({"account_id": account_id}) - return self.get_essential_details(method, url, payload) - - def get_atomic_agreement(self): - method = "GET" - url = self.base_url + "atomic/onboarding/agreement" - return self.get_essential_details(method, url) - def cscs_onboarding(self, **kwargs): method = "POST" url = self.base_url + "cscs/onboarding" From 6444604cfb12b7d41b20bb2cb53038c78ed2da41 Mon Sep 17 00:00:00 2001 From: Taslim Oseni Date: Sun, 15 Feb 2026 16:09:47 +0100 Subject: [PATCH 08/15] Update auto-reinvest to use PATCH and add DELETE method --- embed/resources/fixed_notes.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/embed/resources/fixed_notes.py b/embed/resources/fixed_notes.py index 889d787..c253e71 100644 --- a/embed/resources/fixed_notes.py +++ b/embed/resources/fixed_notes.py @@ -196,7 +196,21 @@ def update_auto_reinvest(self, fixed_note_id, auto_reinvest): Returns: dict: The API response containing updated fixed note details. """ - method = "PUT" + method = "PATCH" url = self.base_url + f"fixed-notes/{fixed_note_id}/auto-reinvest" payload = json.dumps({"auto_reinvest": auto_reinvest}) return self.get_essential_details(method, url, payload) + + def delete_auto_reinvest(self, fixed_note_id): + """ + Delete the auto-reinvest status for a fixed note. + + Args: + fixed_note_id (str): The unique identifier for the fixed note. + + Returns: + dict: The API response. + """ + method = "DELETE" + url = self.base_url + f"fixed-notes/{fixed_note_id}/auto-reinvest" + return self.get_essential_details(method, url) From 790dd1e0e824665e2d8310297b4d040e048f1828 Mon Sep 17 00:00:00 2001 From: Taslim Oseni Date: Sun, 15 Feb 2026 16:10:31 +0100 Subject: [PATCH 09/15] Refactor Integrations to CSCS --- embed/client.py | 8 ++++---- embed/resources/{integrations.py => cscs.py} | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) rename embed/resources/{integrations.py => cscs.py} (75%) diff --git a/embed/client.py b/embed/client.py index f0f1490..6773911 100644 --- a/embed/client.py +++ b/embed/client.py @@ -5,7 +5,7 @@ from embed.resources.asset import Asset from embed.resources.deposit import Deposit from embed.resources.index import Index -from embed.resources.integrations import Integration +from embed.resources.cscs import CSCS from embed.resources.investment import Investment from embed.resources.price import Price from embed.resources.saving import Saving @@ -69,7 +69,7 @@ def __init__( self._deposits = Deposit(self._session) self._investments = Investment(self._session) self._indexes = Index(self._session) - self._integrations = Integration(self._session) + self._cscs = CSCS(self._session) self._savings = Saving(self._session) self._flexible_savings = FlexibleSaving(self._session) self._fixed_notes = FixedNote(self._session) @@ -109,8 +109,8 @@ def indexes(self): return self._indexes @property - def integrations(self): - return self._integrations + def cscs(self): + return self._cscs @property def savings(self): diff --git a/embed/resources/integrations.py b/embed/resources/cscs.py similarity index 75% rename from embed/resources/integrations.py rename to embed/resources/cscs.py index f372c2f..161c2bc 100644 --- a/embed/resources/integrations.py +++ b/embed/resources/cscs.py @@ -2,26 +2,26 @@ from embed.common import APIResponse -class Integration(APIResponse): +class CSCS(APIResponse): """ - Handles external integrations like Atomic and CSCS. + Handles CSCS integration. """ def __init__(self, api_session): - super(Integration, self).__init__() + super(CSCS, self).__init__() self.base_url = ( f"{api_session.base_url}/api/{api_session.api_version}/integration/" ) self.token = api_session.token self._headers.update({"Authorization": f"Bearer {self.token}"}) - def cscs_onboarding(self, **kwargs): + def onboarding(self, **kwargs): method = "POST" url = self.base_url + "cscs/onboarding" payload = json.dumps(kwargs) return self.get_essential_details(method, url, payload) - def get_cscs_profile(self, account_id): + def get_profile(self, account_id): method = "GET" url = self.base_url + f"cscs/onboarding?account_id={account_id}" return self.get_essential_details(method, url) From ed599493289ad5bc8840e42bca0fd4f0b7683a69 Mon Sep 17 00:00:00 2001 From: Taslim Oseni Date: Sun, 15 Feb 2026 16:11:57 +0100 Subject: [PATCH 10/15] Add WithdrawalIntent resource and fix withdrawal intents endpoints --- embed/client.py | 7 ++ embed/resources/withdrawal.py | 59 ---------------- embed/resources/withdrawal_intents.py | 97 +++++++++++++++++++++++++++ 3 files changed, 104 insertions(+), 59 deletions(-) create mode 100644 embed/resources/withdrawal_intents.py diff --git a/embed/client.py b/embed/client.py index 6773911..fa8584b 100644 --- a/embed/client.py +++ b/embed/client.py @@ -17,6 +17,7 @@ from embed.resources.trade import Trade from embed.resources.transaction import Transaction from embed.resources.withdrawal import Withdrawal +from embed.resources.withdrawal_intents import WithdrawalIntent from embed.resources.misc import Misc from embed.resources.wallet import Wallet from embed.common import APISession @@ -79,6 +80,7 @@ def __init__( self._trades = Trade(self._session) self._transactions = Transaction(self._session) self._withdrawals = Withdrawal(self._session) + self._withdrawal_intents = WithdrawalIntent(self._session) self._prices = Price(self._session) self._wallets = Wallet(self._session) self._misc = Misc(self._session) @@ -148,6 +150,11 @@ def transactions(self): def withdrawals(self): return self._withdrawals + @property + def withdrawal_intents(self): + return self._withdrawal_intents + + @property def prices(self): return self._prices diff --git a/embed/resources/withdrawal.py b/embed/resources/withdrawal.py index babad18..72c4103 100644 --- a/embed/resources/withdrawal.py +++ b/embed/resources/withdrawal.py @@ -47,62 +47,3 @@ def get_withdrawal(self, withdrawal_id): method = "GET" url = self.base_url + f"withdrawals/{withdrawal_id}" return self.get_essential_details(method, url) - - def list_withdrawal_intents(self, **kwargs): - """ - Retrieve a list of all withdrawal intents. - - Args: - **kwargs: Arbitrary keyword arguments for filtering. - user_email (str): Optional. Filter by user email. - currency (str): Optional. Filter by currency. - - Returns: - dict: The API response containing a list of withdrawal intents. - """ - query_path = "&".join(f"{k}={v}" for k, v in kwargs.items()) - method = "GET" - url = self.base_url + "withdrawals/intents" - if query_path: - url = f"{url}?{query_path}" - return self.get_essential_details(method, url) - - def retry_withdrawal_intent(self, **kwargs): - """ - Retry a failed withdrawal intent. - - Args: - **kwargs: Arbitrary keyword arguments. - account_id (str): Required. The account ID. - withdrawal_intent_id (str): Required. The withdrawal intent ID. - - Returns: - dict: The API response containing retry details. - """ - required = ["account_id", "withdrawal_intent_id"] - self._validate_kwargs(required, kwargs) - - method = "POST" - url = self.base_url + "withdrawals/intents/retry" - payload = json.dumps(kwargs) - return self.get_essential_details(method, url, payload) - - def cancel_withdrawal_intent(self, **kwargs): - """ - Cancel a pending withdrawal intent. - - Args: - **kwargs: Arbitrary keyword arguments. - account_id (str): Required. The account ID. - withdrawal_intent_id (str): Required. The withdrawal intent ID. - - Returns: - dict: The API response containing cancellation details. - """ - required = ["account_id", "withdrawal_intent_id"] - self._validate_kwargs(required, kwargs) - - method = "POST" - url = self.base_url + "withdrawals/intents/cancel" - payload = json.dumps(kwargs) - return self.get_essential_details(method, url, payload) diff --git a/embed/resources/withdrawal_intents.py b/embed/resources/withdrawal_intents.py new file mode 100644 index 0000000..7cc20fd --- /dev/null +++ b/embed/resources/withdrawal_intents.py @@ -0,0 +1,97 @@ +import json +from embed.common import APIResponse + + +class WithdrawalIntent(APIResponse): + """ + Handles withdrawal intents. + """ + + def __init__(self, api_session): + super(WithdrawalIntent, self).__init__() + self.base_url = f"{api_session.base_url}/api/{api_session.api_version}/" + self.token = api_session.token + self._headers.update({"Authorization": f"Bearer {self.token}"}) + + def create_withdrawal_intent(self, **kwargs): + """ + Initiate a withdrawal intent. + + Args: + **kwargs: Arbitrary keyword arguments. + account_id (str): Required. The account ID. + bank_id (str): Required. The bank ID. + amount (float): Required. The amount to withdraw. + currency (str): Required. The currency code. + + Returns: + dict: The API response. + """ + required = ["account_id", "bank_id", "amount", "currency"] + self._validate_kwargs(required, kwargs) + + method = "POST" + url = self.base_url + "withdrawal-intents" + payload = json.dumps(kwargs) + return self.get_essential_details(method, url, payload) + + def list_withdrawal_intents(self, **kwargs): + """ + Retrieve a list of withdrawal intents. + + Args: + **kwargs: Arbitrary keyword arguments for filtering. + account_id (str): Optional. + currency (str): Optional. + + Returns: + dict: The API response. + """ + query_path = "&".join(f"{k}={v}" for k, v in kwargs.items()) + method = "GET" + url = self.base_url + "withdrawal-intents" + if query_path: + url = f"{url}?{query_path}" + return self.get_essential_details(method, url) + + def retry_withdrawal_intent(self, **kwargs): + """ + Retry a failed withdrawal intent. + + Args: + **kwargs: Arbitrary keyword arguments. + account_id (str): Required. + reference (str): Required. + currency (str): Required. + + Returns: + dict: The API response. + """ + required = ["account_id", "reference", "currency"] + self._validate_kwargs(required, kwargs) + + method = "POST" + url = self.base_url + "withdrawal-intents/retry" + payload = json.dumps(kwargs) + return self.get_essential_details(method, url, payload) + + def cancel_withdrawal_intent(self, **kwargs): + """ + Cancel a pending withdrawal intent. + + Args: + **kwargs: Arbitrary keyword arguments. + account_id (str): Required. + reference (str): Required. + currency (str): Required. + + Returns: + dict: The API response. + """ + required = ["account_id", "reference", "currency"] + self._validate_kwargs(required, kwargs) + + method = "POST" + url = self.base_url + "withdrawal-intents/cancel" + payload = json.dumps(kwargs) + return self.get_essential_details(method, url, payload) From e880242dd8f52faf00faf87a07aa5fd772b44c8f Mon Sep 17 00:00:00 2001 From: Taslim Oseni Date: Sun, 15 Feb 2026 16:14:27 +0100 Subject: [PATCH 11/15] Update ReadMe --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index ec23502..90d8b8b 100644 --- a/README.md +++ b/README.md @@ -94,7 +94,6 @@ investment = client.investment.create_investment( | `get_wallet(wallet_id)` | `GET /wallets/:wallet_id` | | `list_flexible_savings(**kwargs)` | `GET /flexible-savings` | | `create_fixed_note(**kwargs)` | `POST /fixed-notes` | - | `list_withdrawals(**kwargs)` | `GET /withdrawals` | Check the [API reference](https://developers.cowrywise.com/reference) document for all resources and their respective endpoints. From 10520fd6d90d3ece93646c87dcfe6cda5df31b5d Mon Sep 17 00:00:00 2001 From: Taslim Oseni Date: Sun, 15 Feb 2026 16:24:20 +0100 Subject: [PATCH 12/15] Fix formatting issues caught by black --- embed/client.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/embed/client.py b/embed/client.py index fa8584b..276a91b 100644 --- a/embed/client.py +++ b/embed/client.py @@ -101,7 +101,6 @@ def assets(self): def deposits(self): return self._deposits - @property def investments(self): return self._investments @@ -154,7 +153,6 @@ def withdrawals(self): def withdrawal_intents(self): return self._withdrawal_intents - @property def prices(self): return self._prices From 5ebb0d1dec14ab6d9b2c7a0fbd342d7aa3748886 Mon Sep 17 00:00:00 2001 From: Taslim Oseni Date: Sun, 15 Feb 2026 16:27:41 +0100 Subject: [PATCH 13/15] Update tests to reflect API changes --- tests/resources/test_cscs.py | 30 ++++++++++ tests/resources/test_eurobonds.py | 27 --------- tests/resources/test_fixed_notes.py | 23 ++++++++ tests/resources/test_fixed_placements.py | 27 --------- tests/resources/test_withdrawal_intents.py | 66 ++++++++++++++++++++++ tests/resources/test_withdrawals.py | 10 +--- 6 files changed, 120 insertions(+), 63 deletions(-) create mode 100644 tests/resources/test_cscs.py delete mode 100644 tests/resources/test_eurobonds.py delete mode 100644 tests/resources/test_fixed_placements.py create mode 100644 tests/resources/test_withdrawal_intents.py diff --git a/tests/resources/test_cscs.py b/tests/resources/test_cscs.py new file mode 100644 index 0000000..deb0eec --- /dev/null +++ b/tests/resources/test_cscs.py @@ -0,0 +1,30 @@ +import json +from embed.resources.cscs import CSCS +from unittest.mock import MagicMock, patch + + +@patch("embed.common.APIResponse.get_essential_details") +def test_can_onboard_cscs(mock_get_essential_details, api_session): + cscs = CSCS(api_session) + mock_get_essential_details.return_value = MagicMock() + test_data = { + "account_id": "fake-account-id", + "cscs_number": "fake-cscs-number", + } + cscs.onboarding(**test_data) + cscs.get_essential_details.assert_called_with( + "POST", + f"{api_session.base_url}/api/{api_session.api_version}/integration/cscs/onboarding", + json.dumps(test_data), + ) + + +@patch("embed.common.APIResponse.get_essential_details") +def test_can_get_cscs_profile(mock_get_essential_details, api_session): + cscs = CSCS(api_session) + mock_get_essential_details.return_value = MagicMock() + cscs.get_profile("fake-account-id") + cscs.get_essential_details.assert_called_with( + "GET", + f"{api_session.base_url}/api/{api_session.api_version}/integration/cscs/onboarding?account_id=fake-account-id", + ) diff --git a/tests/resources/test_eurobonds.py b/tests/resources/test_eurobonds.py deleted file mode 100644 index 0fb9993..0000000 --- a/tests/resources/test_eurobonds.py +++ /dev/null @@ -1,27 +0,0 @@ -import json -from embed.resources.eurobonds import Eurobond -from unittest.mock import MagicMock, patch - - -@patch("embed.common.APIResponse.get_essential_details") -def test_can_list_eurobonds(mock_get_essential_details, api_session): - eb = Eurobond(api_session) - mock_get_essential_details.return_value = MagicMock() - eb.list_eurobonds(account_id="fake-acc") - eb.get_essential_details.assert_called_with( - "GET", - f"{api_session.base_url}/api/{api_session.api_version}/eurobonds?account_id=fake-acc", - ) - - -@patch("embed.common.APIResponse.get_essential_details") -def test_can_create_eurobond_preview(mock_get_essential_details, api_session): - eb = Eurobond(api_session) - mock_get_essential_details.return_value = MagicMock() - test_data = {"asset_code": "EB-1", "amount": 1000} - eb.create_preview(**test_data) - eb.get_essential_details.assert_called_with( - "POST", - f"{api_session.base_url}/api/{api_session.api_version}/eurobonds/preview", - json.dumps(test_data), - ) diff --git a/tests/resources/test_fixed_notes.py b/tests/resources/test_fixed_notes.py index 730a0e5..ed81a84 100644 --- a/tests/resources/test_fixed_notes.py +++ b/tests/resources/test_fixed_notes.py @@ -84,3 +84,26 @@ def test_can_rollover_fixed_note(mock_get_essential_details, api_session): f"{api_session.base_url}/api/{api_session.api_version}/fixed-notes/fake-fn-id/rollover", json.dumps({"tenor_in_months": 3}), ) + + +@patch("embed.common.APIResponse.get_essential_details") +def test_can_update_auto_reinvest(mock_get_essential_details, api_session): + fn = FixedNote(api_session) + mock_get_essential_details.return_value = MagicMock() + fn.update_auto_reinvest("fake-fn-id", auto_reinvest=True) + fn.get_essential_details.assert_called_with( + "PATCH", + f"{api_session.base_url}/api/{api_session.api_version}/fixed-notes/fake-fn-id/auto-reinvest", + json.dumps({"auto_reinvest": True}), + ) + + +@patch("embed.common.APIResponse.get_essential_details") +def test_can_delete_auto_reinvest(mock_get_essential_details, api_session): + fn = FixedNote(api_session) + mock_get_essential_details.return_value = MagicMock() + fn.delete_auto_reinvest("fake-fn-id") + fn.get_essential_details.assert_called_with( + "DELETE", + f"{api_session.base_url}/api/{api_session.api_version}/fixed-notes/fake-fn-id/auto-reinvest", + ) diff --git a/tests/resources/test_fixed_placements.py b/tests/resources/test_fixed_placements.py deleted file mode 100644 index 2382ba5..0000000 --- a/tests/resources/test_fixed_placements.py +++ /dev/null @@ -1,27 +0,0 @@ -import json -from embed.resources.fixed_placements import FixedPlacement -from unittest.mock import MagicMock, patch - - -@patch("embed.common.APIResponse.get_essential_details") -def test_can_list_fixed_placements(mock_get_essential_details, api_session): - fp = FixedPlacement(api_session) - mock_get_essential_details.return_value = MagicMock() - fp.list_fixed_placements(account_id="fake-acc") - fp.get_essential_details.assert_called_with( - "GET", - f"{api_session.base_url}/api/{api_session.api_version}/fixed-placements?account_id=fake-acc", - ) - - -@patch("embed.common.APIResponse.get_essential_details") -def test_can_create_fixed_placement_preview(mock_get_essential_details, api_session): - fp = FixedPlacement(api_session) - mock_get_essential_details.return_value = MagicMock() - test_data = {"asset_code": "FP-1", "amount": 5000} - fp.create_preview(**test_data) - fp.get_essential_details.assert_called_with( - "POST", - f"{api_session.base_url}/api/{api_session.api_version}/fixed-placements/preview", - json.dumps(test_data), - ) diff --git a/tests/resources/test_withdrawal_intents.py b/tests/resources/test_withdrawal_intents.py new file mode 100644 index 0000000..7efae3e --- /dev/null +++ b/tests/resources/test_withdrawal_intents.py @@ -0,0 +1,66 @@ +import json +from embed.resources.withdrawal_intents import WithdrawalIntent +from unittest.mock import MagicMock, patch + + +@patch("embed.common.APIResponse.get_essential_details") +def test_can_create_withdrawal_intent(mock_get_essential_details, api_session): + wi = WithdrawalIntent(api_session) + mock_get_essential_details.return_value = MagicMock() + test_data = { + "account_id": "fake-account-id", + "bank_id": "fake-bank-id", + "amount": 5000, + "currency": "NGN", + } + wi.create_withdrawal_intent(**test_data) + wi.get_essential_details.assert_called_with( + "POST", + f"{api_session.base_url}/api/{api_session.api_version}/withdrawal-intents", + json.dumps(test_data), + ) + + +@patch("embed.common.APIResponse.get_essential_details") +def test_can_list_withdrawal_intents(mock_get_essential_details, api_session): + wi = WithdrawalIntent(api_session) + mock_get_essential_details.return_value = MagicMock() + wi.list_withdrawal_intents(currency="NGN") + wi.get_essential_details.assert_called_with( + "GET", + f"{api_session.base_url}/api/{api_session.api_version}/withdrawal-intents?currency=NGN", + ) + + +@patch("embed.common.APIResponse.get_essential_details") +def test_can_retry_withdrawal_intent(mock_get_essential_details, api_session): + wi = WithdrawalIntent(api_session) + mock_get_essential_details.return_value = MagicMock() + test_data = { + "account_id": "fake-account-id", + "reference": "fake-ref", + "currency": "NGN", + } + wi.retry_withdrawal_intent(**test_data) + wi.get_essential_details.assert_called_with( + "POST", + f"{api_session.base_url}/api/{api_session.api_version}/withdrawal-intents/retry", + json.dumps(test_data), + ) + + +@patch("embed.common.APIResponse.get_essential_details") +def test_can_cancel_withdrawal_intent(mock_get_essential_details, api_session): + wi = WithdrawalIntent(api_session) + mock_get_essential_details.return_value = MagicMock() + test_data = { + "account_id": "fake-account-id", + "reference": "fake-ref", + "currency": "NGN", + } + wi.cancel_withdrawal_intent(**test_data) + wi.get_essential_details.assert_called_with( + "POST", + f"{api_session.base_url}/api/{api_session.api_version}/withdrawal-intents/cancel", + json.dumps(test_data), + ) diff --git a/tests/resources/test_withdrawals.py b/tests/resources/test_withdrawals.py index c4d61d1..b9e2cf2 100644 --- a/tests/resources/test_withdrawals.py +++ b/tests/resources/test_withdrawals.py @@ -24,12 +24,4 @@ def test_can_get_withdrawal(mock_get_essential_details, api_session): ) -@patch("embed.common.APIResponse.get_essential_details") -def test_can_list_withdrawal_intents(mock_get_essential_details, api_session): - w = Withdrawal(api_session) - mock_get_essential_details.return_value = MagicMock() - w.list_withdrawal_intents(currency="NGN") - w.get_essential_details.assert_called_with( - "GET", - f"{api_session.base_url}/api/{api_session.api_version}/withdrawals/intents?currency=NGN", - ) + From e84afbd6725393b9cb56865b1ab9042824811f5d Mon Sep 17 00:00:00 2001 From: Taslim Oseni Date: Sun, 15 Feb 2026 16:31:51 +0100 Subject: [PATCH 14/15] Fix trailing whitespace in test_withdrawals.py --- tests/resources/test_withdrawals.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/resources/test_withdrawals.py b/tests/resources/test_withdrawals.py index b9e2cf2..92a341d 100644 --- a/tests/resources/test_withdrawals.py +++ b/tests/resources/test_withdrawals.py @@ -22,6 +22,3 @@ def test_can_get_withdrawal(mock_get_essential_details, api_session): "GET", f"{api_session.base_url}/api/{api_session.api_version}/withdrawals/fake-w-id", ) - - - From 02b1bf1bf61e831ce48251017521ca603c15d582 Mon Sep 17 00:00:00 2001 From: Taslim Oseni Date: Sun, 15 Feb 2026 16:49:47 +0100 Subject: [PATCH 15/15] Update fixed notes update method and rename CSCS to Integrations --- embed/client.py | 8 +++--- embed/resources/fixed_notes.py | 26 +++++-------------- embed/resources/{cscs.py => integrations.py} | 10 +++---- tests/resources/test_fixed_notes.py | 17 +++--------- .../{test_cscs.py => test_integrations.py} | 16 ++++++------ 5 files changed, 26 insertions(+), 51 deletions(-) rename embed/resources/{cscs.py => integrations.py} (77%) rename tests/resources/{test_cscs.py => test_integrations.py} (64%) diff --git a/embed/client.py b/embed/client.py index 276a91b..eceebc7 100644 --- a/embed/client.py +++ b/embed/client.py @@ -5,7 +5,7 @@ from embed.resources.asset import Asset from embed.resources.deposit import Deposit from embed.resources.index import Index -from embed.resources.cscs import CSCS +from embed.resources.integrations import Integration from embed.resources.investment import Investment from embed.resources.price import Price from embed.resources.saving import Saving @@ -70,7 +70,7 @@ def __init__( self._deposits = Deposit(self._session) self._investments = Investment(self._session) self._indexes = Index(self._session) - self._cscs = CSCS(self._session) + self._integrations = Integration(self._session) self._savings = Saving(self._session) self._flexible_savings = FlexibleSaving(self._session) self._fixed_notes = FixedNote(self._session) @@ -110,8 +110,8 @@ def indexes(self): return self._indexes @property - def cscs(self): - return self._cscs + def integrations(self): + return self._integrations @property def savings(self): diff --git a/embed/resources/fixed_notes.py b/embed/resources/fixed_notes.py index c253e71..e583ba6 100644 --- a/embed/resources/fixed_notes.py +++ b/embed/resources/fixed_notes.py @@ -185,32 +185,18 @@ def rollover(self, fixed_note_id, tenor_in_months): payload = json.dumps({"tenor_in_months": tenor_in_months}) return self.get_essential_details(method, url, payload) - def update_auto_reinvest(self, fixed_note_id, auto_reinvest): + def partial_update(self, fixed_note_id, **kwargs): """ - Update the auto-reinvest status for a fixed note. + Partially update a fixed note. Args: fixed_note_id (str): The unique identifier for the fixed note. - auto_reinvest (bool): Whether to enable auto-reinvest. + **kwargs: Fields to update (e.g., auto_reinvest). Returns: - dict: The API response containing updated fixed note details. + dict: The API response. """ method = "PATCH" - url = self.base_url + f"fixed-notes/{fixed_note_id}/auto-reinvest" - payload = json.dumps({"auto_reinvest": auto_reinvest}) + url = self.base_url + f"fixed-notes/{fixed_note_id}" + payload = json.dumps(kwargs) return self.get_essential_details(method, url, payload) - - def delete_auto_reinvest(self, fixed_note_id): - """ - Delete the auto-reinvest status for a fixed note. - - Args: - fixed_note_id (str): The unique identifier for the fixed note. - - Returns: - dict: The API response. - """ - method = "DELETE" - url = self.base_url + f"fixed-notes/{fixed_note_id}/auto-reinvest" - return self.get_essential_details(method, url) diff --git a/embed/resources/cscs.py b/embed/resources/integrations.py similarity index 77% rename from embed/resources/cscs.py rename to embed/resources/integrations.py index 161c2bc..88a45e6 100644 --- a/embed/resources/cscs.py +++ b/embed/resources/integrations.py @@ -2,26 +2,26 @@ from embed.common import APIResponse -class CSCS(APIResponse): +class Integration(APIResponse): """ - Handles CSCS integration. + Handles external integrations. """ def __init__(self, api_session): - super(CSCS, self).__init__() + super(Integration, self).__init__() self.base_url = ( f"{api_session.base_url}/api/{api_session.api_version}/integration/" ) self.token = api_session.token self._headers.update({"Authorization": f"Bearer {self.token}"}) - def onboarding(self, **kwargs): + def cscs_onboarding(self, **kwargs): method = "POST" url = self.base_url + "cscs/onboarding" payload = json.dumps(kwargs) return self.get_essential_details(method, url, payload) - def get_profile(self, account_id): + def get_cscs_profile(self, account_id): method = "GET" url = self.base_url + f"cscs/onboarding?account_id={account_id}" return self.get_essential_details(method, url) diff --git a/tests/resources/test_fixed_notes.py b/tests/resources/test_fixed_notes.py index ed81a84..7ffb1e7 100644 --- a/tests/resources/test_fixed_notes.py +++ b/tests/resources/test_fixed_notes.py @@ -87,23 +87,12 @@ def test_can_rollover_fixed_note(mock_get_essential_details, api_session): @patch("embed.common.APIResponse.get_essential_details") -def test_can_update_auto_reinvest(mock_get_essential_details, api_session): +def test_can_partial_update(mock_get_essential_details, api_session): fn = FixedNote(api_session) mock_get_essential_details.return_value = MagicMock() - fn.update_auto_reinvest("fake-fn-id", auto_reinvest=True) + fn.partial_update("fake-fn-id", auto_reinvest=True) fn.get_essential_details.assert_called_with( "PATCH", - f"{api_session.base_url}/api/{api_session.api_version}/fixed-notes/fake-fn-id/auto-reinvest", + f"{api_session.base_url}/api/{api_session.api_version}/fixed-notes/fake-fn-id", json.dumps({"auto_reinvest": True}), ) - - -@patch("embed.common.APIResponse.get_essential_details") -def test_can_delete_auto_reinvest(mock_get_essential_details, api_session): - fn = FixedNote(api_session) - mock_get_essential_details.return_value = MagicMock() - fn.delete_auto_reinvest("fake-fn-id") - fn.get_essential_details.assert_called_with( - "DELETE", - f"{api_session.base_url}/api/{api_session.api_version}/fixed-notes/fake-fn-id/auto-reinvest", - ) diff --git a/tests/resources/test_cscs.py b/tests/resources/test_integrations.py similarity index 64% rename from tests/resources/test_cscs.py rename to tests/resources/test_integrations.py index deb0eec..1559425 100644 --- a/tests/resources/test_cscs.py +++ b/tests/resources/test_integrations.py @@ -1,18 +1,18 @@ import json -from embed.resources.cscs import CSCS +from embed.resources.integrations import Integration from unittest.mock import MagicMock, patch @patch("embed.common.APIResponse.get_essential_details") -def test_can_onboard_cscs(mock_get_essential_details, api_session): - cscs = CSCS(api_session) +def test_can_cscs_onboarding(mock_get_essential_details, api_session): + integration = Integration(api_session) mock_get_essential_details.return_value = MagicMock() test_data = { "account_id": "fake-account-id", "cscs_number": "fake-cscs-number", } - cscs.onboarding(**test_data) - cscs.get_essential_details.assert_called_with( + integration.cscs_onboarding(**test_data) + integration.get_essential_details.assert_called_with( "POST", f"{api_session.base_url}/api/{api_session.api_version}/integration/cscs/onboarding", json.dumps(test_data), @@ -21,10 +21,10 @@ def test_can_onboard_cscs(mock_get_essential_details, api_session): @patch("embed.common.APIResponse.get_essential_details") def test_can_get_cscs_profile(mock_get_essential_details, api_session): - cscs = CSCS(api_session) + integration = Integration(api_session) mock_get_essential_details.return_value = MagicMock() - cscs.get_profile("fake-account-id") - cscs.get_essential_details.assert_called_with( + integration.get_cscs_profile("fake-account-id") + integration.get_essential_details.assert_called_with( "GET", f"{api_session.base_url}/api/{api_session.api_version}/integration/cscs/onboarding?account_id=fake-account-id", )