diff --git a/README.md b/README.md index ee751d9..90d8b8b 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,9 @@ 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_withdrawals(**kwargs)` | `GET /withdrawals` | 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 d28e191..eceebc7 100644 --- a/embed/client.py +++ b/embed/client.py @@ -3,15 +3,21 @@ 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.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.withdrawal_intents import WithdrawalIntent from embed.resources.misc import Misc from embed.resources.wallet import Wallet from embed.common import APISession @@ -61,14 +67,20 @@ def __init__( self._accounts = Account(self._session) self._assets = Asset(self._session) + self._deposits = Deposit(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._withdrawal_intents = WithdrawalIntent(self._session) self._prices = Price(self._session) self._wallets = Wallet(self._session) self._misc = Misc(self._session) @@ -85,6 +97,10 @@ def accounts(self): def assets(self): return self._assets + @property + def deposits(self): + return self._deposits + @property def investments(self): return self._investments @@ -93,10 +109,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 +145,14 @@ def trades(self): def transactions(self): return self._transactions + @property + 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/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/fixed_notes.py b/embed/resources/fixed_notes.py new file mode 100644 index 0000000..e583ba6 --- /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 partial_update(self, fixed_note_id, **kwargs): + """ + Partially update a fixed note. + + Args: + fixed_note_id (str): The unique identifier for the fixed note. + **kwargs: Fields to update (e.g., auto_reinvest). + + Returns: + dict: The API response. + """ + method = "PATCH" + url = self.base_url + f"fixed-notes/{fixed_note_id}" + 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..ce4c4f7 --- /dev/null +++ b/embed/resources/flexible_savings.py @@ -0,0 +1,163 @@ +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..88a45e6 --- /dev/null +++ b/embed/resources/integrations.py @@ -0,0 +1,27 @@ +import json +from embed.common import APIResponse + + +class Integration(APIResponse): + """ + Handles external integrations. + """ + + 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 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..72c4103 --- /dev/null +++ b/embed/resources/withdrawal.py @@ -0,0 +1,49 @@ +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) 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) diff --git a/embed/version.py b/embed/version.py index cae7902..c9627cd 100644 --- a/embed/version.py +++ b/embed/version.py @@ -1,4 +1,4 @@ -__version__ = "2.1.1" +__version__ = "2.2.0" __author__ = "Cowrywise Developers" __license__ = "MIT" __copyright__ = "Copyright 2021-2026. Cowrywise" diff --git a/tests/resources/test_deposits.py b/tests/resources/test_deposits.py new file mode 100644 index 0000000..de8b905 --- /dev/null +++ b/tests/resources/test_deposits.py @@ -0,0 +1,24 @@ +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_fixed_notes.py b/tests/resources/test_fixed_notes.py new file mode 100644 index 0000000..7ffb1e7 --- /dev/null +++ b/tests/resources/test_fixed_notes.py @@ -0,0 +1,98 @@ +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}), + ) + + +@patch("embed.common.APIResponse.get_essential_details") +def test_can_partial_update(mock_get_essential_details, api_session): + fn = FixedNote(api_session) + mock_get_essential_details.return_value = MagicMock() + 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", + json.dumps({"auto_reinvest": True}), + ) diff --git a/tests/resources/test_flexible_savings.py b/tests/resources/test_flexible_savings.py new file mode 100644 index 0000000..2561341 --- /dev/null +++ b/tests/resources/test_flexible_savings.py @@ -0,0 +1,76 @@ +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_integrations.py b/tests/resources/test_integrations.py new file mode 100644 index 0000000..1559425 --- /dev/null +++ b/tests/resources/test_integrations.py @@ -0,0 +1,30 @@ +import json +from embed.resources.integrations import Integration +from unittest.mock import MagicMock, patch + + +@patch("embed.common.APIResponse.get_essential_details") +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", + } + 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), + ) + + +@patch("embed.common.APIResponse.get_essential_details") +def test_can_get_cscs_profile(mock_get_essential_details, api_session): + integration = Integration(api_session) + mock_get_essential_details.return_value = MagicMock() + 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", + ) 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 new file mode 100644 index 0000000..92a341d --- /dev/null +++ b/tests/resources/test_withdrawals.py @@ -0,0 +1,24 @@ +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", + )