From da8a059e38d5b2ec3d8fa198a6ea04b48c5daa19 Mon Sep 17 00:00:00 2001 From: thomasc Date: Mon, 23 Mar 2026 19:06:34 +0100 Subject: [PATCH] IEX-2498 add endpoint coverage for new capital, mandates, and recurring URL flows Add serialization/deserialization tests and mock payloads for Capital Dynamic Offers and Balance Platform Direct Debit Mandates APIs, including tax form summary URL assertions. Also update recurring live URL resolution to use the paltokenization live domain format with required live endpoint prefix, and validate both test/live recurring endpoint behavior in DetermineEndpoint tests. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> --- Adyen/client.py | 37 +++--- test/BalancePlatformTest.py | 105 ++++++++++++++++++ test/CapitalTest.py | 33 ++++++ test/DetermineEndpointTest.py | 39 +++++++ .../calculate-dynamic-offer-success.json | 16 +++ ...ate-static-offer-from-dynamic-success.json | 9 ++ .../capital/get-dynamic-offers-success.json | 18 +++ .../configuration/get-mandate-success.json | 7 ++ .../configuration/get-mandates-success.json | 9 ++ .../get-tax-form-summary-success.json | 8 ++ .../configuration/update-mandate-success.json | 5 + 11 files changed, 269 insertions(+), 17 deletions(-) create mode 100644 test/mocks/capital/calculate-dynamic-offer-success.json create mode 100644 test/mocks/capital/create-static-offer-from-dynamic-success.json create mode 100644 test/mocks/capital/get-dynamic-offers-success.json create mode 100644 test/mocks/configuration/get-mandate-success.json create mode 100644 test/mocks/configuration/get-mandates-success.json create mode 100644 test/mocks/configuration/get-tax-form-summary-success.json create mode 100644 test/mocks/configuration/update-mandate-success.json diff --git a/Adyen/client.py b/Adyen/client.py index 969614e7..0cf7c858 100644 --- a/Adyen/client.py +++ b/Adyen/client.py @@ -142,41 +142,44 @@ def __init__( self.api_session_authentication_version = api_session_authentication_version self.api_capital_version = api_capital_version + def _require_live_endpoint_prefix(self): + if self.live_endpoint_prefix is None: + error_string = ( + "Please set your live suffix. You can set it by running " + "adyen.client.live_endpoint_prefix = 'Your live suffix'" + ) + raise AdyenEndpointInvalidFormat(error_string) + return self.live_endpoint_prefix + def _determine_api_url(self, platform, endpoint): if platform == "test": # Replace live with test in base url is configured with live url by default return endpoint.replace("-live", "-test") if "pal-" in endpoint: - if self.live_endpoint_prefix is None: - error_string = ( - "Please set your live suffix. You can set it by running " - "adyen.client.live_endpoint_prefix = 'Your live suffix'" - ) - raise AdyenEndpointInvalidFormat(error_string) + live_endpoint_prefix = self._require_live_endpoint_prefix() endpoint = endpoint.replace( "https://pal-test.adyen.com/pal/servlet/", - "https://" + self.live_endpoint_prefix + "-pal-live.adyenpayments.com/pal/servlet/", + f"https://{live_endpoint_prefix}-pal-live.adyenpayments.com/pal/servlet/", + ) + elif "paltokenization-" in endpoint: + live_endpoint_prefix = self._require_live_endpoint_prefix() + endpoint = endpoint.replace( + "https://paltokenization-test.adyen.com/paltokenization/servlet/", + f"https://{live_endpoint_prefix}-paltokenization-live.adyenpayments.com/paltokenization/servlet/", ) elif "checkout-" in endpoint: - if self.live_endpoint_prefix is None: - error_string = ( - "Please set your live suffix. You can set it by running " - "adyen.client.live_endpoint_prefix = 'Your live suffix'" - ) - raise AdyenEndpointInvalidFormat(error_string) + live_endpoint_prefix = self._require_live_endpoint_prefix() if "possdk" in endpoint: endpoint = endpoint.replace( "https://checkout-test.adyen.com/", - "https://" + self.live_endpoint_prefix + "-checkout-live.adyenpayments.com/", + f"https://{live_endpoint_prefix}-checkout-live.adyenpayments.com/", ) else: endpoint = endpoint.replace( "https://checkout-test.adyen.com/", - "https://" - + self.live_endpoint_prefix - + "-checkout-live.adyenpayments.com/checkout/", + f"https://{live_endpoint_prefix}-checkout-live.adyenpayments.com/checkout/", ) elif "authe/api" in endpoint: endpoint = endpoint.replace("https://test.adyen.com", "https://authe-live.adyen.com") diff --git a/test/BalancePlatformTest.py b/test/BalancePlatformTest.py index 401d6ae8..71378712 100644 --- a/test/BalancePlatformTest.py +++ b/test/BalancePlatformTest.py @@ -238,3 +238,108 @@ def test_update_network_token(self): json=request, xapikey="YourXapikey", ) + + def test_get_list_of_mandates(self): + request = {} + self.adyen.client = self.test.create_client_from_file( + 200, request, "test/mocks/configuration/get-mandates-success.json" + ) + result = self.adyen.balancePlatform.direct_debit_mandates_api.get_list_of_mandates() + self.assertEqual(1, len(result.message["mandates"])) + self.assertEqual("MD00000000000000000000001", result.message["mandates"][0]["id"]) + self.adyen.client.http_client.request.assert_called_once_with( + "GET", + f"{self.balance_platform_url}/mandates", + headers={ + "adyen-library-name": "adyen-python-api-library", + "adyen-library-version": settings.LIB_VERSION, + "User-Agent": "adyen-python-api-library/" + settings.LIB_VERSION, + }, + json=None, + xapikey="YourXapikey", + ) + + def test_get_mandate_by_id(self): + request = {} + mandate_id = "MD00000000000000000000001" + self.adyen.client = self.test.create_client_from_file( + 200, request, "test/mocks/configuration/get-mandate-success.json" + ) + result = self.adyen.balancePlatform.direct_debit_mandates_api.get_mandate_by_id(mandate_id) + self.assertEqual(mandate_id, result.message["id"]) + self.adyen.client.http_client.request.assert_called_once_with( + "GET", + f"{self.balance_platform_url}/mandates/{mandate_id}", + headers={ + "adyen-library-name": "adyen-python-api-library", + "adyen-library-version": settings.LIB_VERSION, + "User-Agent": "adyen-python-api-library/" + settings.LIB_VERSION, + }, + json=None, + xapikey="YourXapikey", + ) + + def test_cancel_mandate(self): + mandate_id = "MD00000000000000000000001" + self.adyen.client = self.test.create_client_from_file(202, None) + result = self.adyen.balancePlatform.direct_debit_mandates_api.cancel_mandate(mandate_id) + + self.assertEqual(202, result.status_code) + self.assertEqual({}, result.message) + self.assertEqual("", result.raw_response) + self.adyen.client.http_client.request.assert_called_once_with( + "POST", + f"{self.balance_platform_url}/mandates/{mandate_id}/cancel", + headers={ + "adyen-library-name": "adyen-python-api-library", + "adyen-library-version": settings.LIB_VERSION, + "User-Agent": "adyen-python-api-library/" + settings.LIB_VERSION, + }, + json=None, + xapikey="YourXapikey", + ) + + def test_update_mandate(self): + request = {"status": "active"} + mandate_id = "MD00000000000000000000001" + self.adyen.client = self.test.create_client_from_file( + 200, request, "test/mocks/configuration/update-mandate-success.json" + ) + result = self.adyen.balancePlatform.direct_debit_mandates_api.update_mandate( + request, mandate_id + ) + self.assertEqual(mandate_id, result.message["id"]) + self.assertEqual("active", result.message["status"]) + self.adyen.client.http_client.request.assert_called_once_with( + "PATCH", + f"{self.balance_platform_url}/mandates/{mandate_id}", + headers={ + "adyen-library-name": "adyen-python-api-library", + "adyen-library-version": settings.LIB_VERSION, + "User-Agent": "adyen-python-api-library/" + settings.LIB_VERSION, + }, + json=request, + xapikey="YourXapikey", + ) + + def test_get_tax_form_summary(self): + request = {} + account_holder_id = "AH00000000000000000000001" + self.adyen.client = self.test.create_client_from_file( + 200, request, "test/mocks/configuration/get-tax-form-summary-success.json" + ) + result = self.adyen.balancePlatform.account_holders_api.get_tax_form_summary( + account_holder_id + ) + self.assertEqual("available", result.message["taxForms"][0]["status"]) + self.adyen.client.http_client.request.assert_called_once_with( + "GET", + f"{self.balance_platform_url}/accountHolders/{account_holder_id}/taxFormSummary", + headers={ + "adyen-library-name": "adyen-python-api-library", + "adyen-library-version": settings.LIB_VERSION, + "User-Agent": "adyen-python-api-library/" + settings.LIB_VERSION, + }, + json=None, + xapikey="YourXapikey", + ) diff --git a/test/CapitalTest.py b/test/CapitalTest.py index f39906ab..5bc82fd5 100644 --- a/test/CapitalTest.py +++ b/test/CapitalTest.py @@ -103,3 +103,36 @@ def test_get_grant_offer(self): ) result = self.adyen.capital.grant_offers_api.get_grant_offer(id="GO00000000000000000000001") self.assertEqual("GO00000000000000000000001", result.message["id"]) + + def test_get_all_dynamic_offers(self): + request = {} + self.adyen.client = self.test.create_client_from_file( + 200, request, "test/mocks/capital/get-dynamic-offers-success.json" + ) + result = self.adyen.capital.dynamic_offers_api.get_all_dynamic_offers() + self.assertEqual(1, len(result.message["dynamicOffers"])) + self.assertEqual("DO00000000000000000000001", result.message["dynamicOffers"][0]["id"]) + + def test_calculate_preliminary_offer_from_dynamic_offer(self): + request = {"amount": {"currency": "EUR", "value": 10000}} + self.adyen.client = self.test.create_client_from_file( + 200, request, "test/mocks/capital/calculate-dynamic-offer-success.json" + ) + result = ( + self.adyen.capital.dynamic_offers_api.calculate_preliminary_offer_from_dynamic_offer( + request, id="DO00000000000000000000001" + ) + ) + self.assertEqual("DO00000000000000000000001", result.message["id"]) + self.assertEqual(1000, result.message["repayment"]["basisPoints"]) + + def test_create_static_offer_from_dynamic_offer(self): + request = {"amount": {"currency": "EUR", "value": 10000}} + self.adyen.client = self.test.create_client_from_file( + 200, request, "test/mocks/capital/create-static-offer-from-dynamic-success.json" + ) + result = self.adyen.capital.dynamic_offers_api.create_static_offer_from_dynamic_offer( + request, id="DO00000000000000000000001" + ) + self.assertEqual("GO00000000000000000000002", result.message["id"]) + self.assertEqual("cashAdvance", result.message["contractType"]) diff --git a/test/DetermineEndpointTest.py b/test/DetermineEndpointTest.py index 80299d9b..9ecf7864 100644 --- a/test/DetermineEndpointTest.py +++ b/test/DetermineEndpointTest.py @@ -3,6 +3,8 @@ import Adyen from Adyen.services.posMobile import AdyenPosMobileApi +RECURRING_DETAILS = "/listRecurringDetails" + try: from BaseTest import BaseTest except ImportError: @@ -25,6 +27,8 @@ class TestDetermineUrl(unittest.TestCase): management_url = adyen.management.account_merchant_level_api.baseUrl sessionauth_url = adyen.sessionAuthentication.session_authentication_api.baseUrl sessionauth_version = sessionauth_url.split("/")[-1] + recurring_url = adyen.recurring.recurring_api.baseUrl + recurring_version = recurring_url.split("/")[-1] capital_url = adyen.capital.grants_api.baseUrl capital_version = capital_url.split("/")[-1] @@ -154,3 +158,38 @@ def test_live_capital_api_url(self): self.assertEqual( url, f"https://balanceplatform-api-live.adyen.com/capital/{self.capital_version}" ) + + def test_recurring_api_base_url(self): + self.assertTrue( + self.recurring_url.startswith( + "https://paltokenization-test.adyen.com/paltokenization/servlet/Recurring/" + ) + ) + + def test_recurring_api_url_test_platform(self): + self.client.live_endpoint_prefix = None + url = self.adyen.client._determine_api_url( + "test", self.recurring_url + RECURRING_DETAILS + ) + self.assertEqual(url, f"{self.recurring_url}{RECURRING_DETAILS}") + + def test_recurring_api_url_live_with_prefix(self): + self.client.live_endpoint_prefix = "1797a841fbb37ca7-AdyenDemo" + url = self.adyen.client._determine_api_url( + "live", self.recurring_url + RECURRING_DETAILS + ) + self.assertEqual( + url, + "https://1797a841fbb37ca7-AdyenDemo-paltokenization-live.adyenpayments.com" + f"/paltokenization/servlet/Recurring/{self.recurring_version}{RECURRING_DETAILS}", + ) + + def test_recurring_api_url_live_no_prefix_raises(self): + self.client.live_endpoint_prefix = None + self.assertRaisesRegex( + AdyenEndpointInvalidFormat, + "Please set your live suffix", + self.adyen.client._determine_api_url, + "live", + self.recurring_url + "RECURRING_DETAILS", + ) diff --git a/test/mocks/capital/calculate-dynamic-offer-success.json b/test/mocks/capital/calculate-dynamic-offer-success.json new file mode 100644 index 00000000..b363c085 --- /dev/null +++ b/test/mocks/capital/calculate-dynamic-offer-success.json @@ -0,0 +1,16 @@ +{ + "id": "DO00000000000000000000001", + "amount": { + "currency": "EUR", + "value": 10000 + }, + "fee": { + "amount": { + "currency": "EUR", + "value": 1000 + } + }, + "repayment": { + "basisPoints": 1000 + } +} diff --git a/test/mocks/capital/create-static-offer-from-dynamic-success.json b/test/mocks/capital/create-static-offer-from-dynamic-success.json new file mode 100644 index 00000000..a77c712d --- /dev/null +++ b/test/mocks/capital/create-static-offer-from-dynamic-success.json @@ -0,0 +1,9 @@ +{ + "id": "GO00000000000000000000002", + "accountHolderId": "AH00000000000000000000001", + "contractType": "cashAdvance", + "amount": { + "currency": "EUR", + "value": 10000 + } +} diff --git a/test/mocks/capital/get-dynamic-offers-success.json b/test/mocks/capital/get-dynamic-offers-success.json new file mode 100644 index 00000000..7ed00f92 --- /dev/null +++ b/test/mocks/capital/get-dynamic-offers-success.json @@ -0,0 +1,18 @@ +{ + "dynamicOffers": [ + { + "id": "DO00000000000000000000001", + "accountHolderId": "AH00000000000000000000001", + "financing": { + "minimum": { + "currency": "EUR", + "value": 5000 + }, + "maximum": { + "currency": "EUR", + "value": 25000 + } + } + } + ] +} diff --git a/test/mocks/configuration/get-mandate-success.json b/test/mocks/configuration/get-mandate-success.json new file mode 100644 index 00000000..03efaa7e --- /dev/null +++ b/test/mocks/configuration/get-mandate-success.json @@ -0,0 +1,7 @@ +{ + "id": "MD00000000000000000000001", + "status": "active", + "accountHolderId": "AH00000000000000000000001", + "balanceAccountId": "BA00000000000000000000001", + "paymentInstrumentId": "PI00000000000000000000001" +} diff --git a/test/mocks/configuration/get-mandates-success.json b/test/mocks/configuration/get-mandates-success.json new file mode 100644 index 00000000..6f84db07 --- /dev/null +++ b/test/mocks/configuration/get-mandates-success.json @@ -0,0 +1,9 @@ +{ + "mandates": [ + { + "id": "MD00000000000000000000001", + "status": "active" + } + ], + "itemsTotal": 1 +} diff --git a/test/mocks/configuration/get-tax-form-summary-success.json b/test/mocks/configuration/get-tax-form-summary-success.json new file mode 100644 index 00000000..df8ec99f --- /dev/null +++ b/test/mocks/configuration/get-tax-form-summary-success.json @@ -0,0 +1,8 @@ +{ + "taxForms": [ + { + "year": 2025, + "status": "available" + } + ] +} diff --git a/test/mocks/configuration/update-mandate-success.json b/test/mocks/configuration/update-mandate-success.json new file mode 100644 index 00000000..83773be2 --- /dev/null +++ b/test/mocks/configuration/update-mandate-success.json @@ -0,0 +1,5 @@ +{ + "id": "MD00000000000000000000001", + "status": "active", + "accountHolderId": "AH00000000000000000000001" +}