From f4a2b0613fc5255b4ffa297a536c7c729dd48d84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Armando=20Rodr=C3=ADguez?= <127134616+armando-rodriguez-cko@users.noreply.github.com> Date: Tue, 27 Oct 2026 12:00:17 +0100 Subject: [PATCH] deprecate(payments): deprecate top-level retry max_attempts and end_after_days The retry object of POST /payments has no max_attempts or end_after_days; they belong to dunning. Payment sessions now share CustomerRetry from common (the old import path still works). Retry and customer retry fields are documented. --- checkout_sdk/common/common.py | 8 ++ checkout_sdk/payments/payments.py | 16 +++ checkout_sdk/payments/sessions/sessions.py | 5 +- .../retry_request_serialization_test.py | 114 ++++++++++++++++++ 4 files changed, 139 insertions(+), 4 deletions(-) create mode 100644 tests/payments/retry_request_serialization_test.py diff --git a/checkout_sdk/common/common.py b/checkout_sdk/common/common.py index 4cef37d4..f0460928 100644 --- a/checkout_sdk/common/common.py +++ b/checkout_sdk/common/common.py @@ -29,6 +29,14 @@ class CustomerRequest: class CustomerRetry: + """Configures the parameters for customer retries. + + A customer retry is a payment attempt performed by the customer on the checkout page. This differs + from authorization request retries performed in the back end by Checkout.com. + """ + # The maximum number of customer retry attempts, excluding the initial payment attempt. + # [Optional] on hosted payments and payment links, [Required] on payment sessions. + # Range: 0 to 5. Default: 5. max_attempts: int diff --git a/checkout_sdk/payments/payments.py b/checkout_sdk/payments/payments.py index d4d0a400..4c69a48c 100644 --- a/checkout_sdk/payments/payments.py +++ b/checkout_sdk/payments/payments.py @@ -1042,20 +1042,36 @@ class PaymentSegment: class DowntimeRetryRequest: + """Configuration of Downtime retries.""" + # [Required] Indicates if Checkout.com retries the payment when it's declined due to issuer or acquirer downtime. enabled: bool class DunningRetryRequest: + """Configuration of Dunning retries.""" + # [Required] Indicates if Checkout.com retries the payment when it's declined with a supported decline code. enabled: bool + # [Optional] The maximum number of authorization retry attempts, excluding the initial authorization. + # Range: 1 to 15. Default: 6. max_attempts: int + # [Optional] The maximum number of days between the initial request and the final retry attempt. + # Range: 1 to 60. Default: 30. end_after_days: int class PaymentRetryRequest: + """Configuration of asynchronous retries.""" + # [Required] Indicates whether asynchronous retries are enabled for the payment. enabled: bool + # @deprecated max_attempts is not part of RetryRequest in the API and will be removed in the next major + # version. Use dunning.max_attempts (DunningRetryRequest) instead. max_attempts: int + # @deprecated end_after_days is not part of RetryRequest in the API and will be removed in the next major + # version. Use dunning.end_after_days (DunningRetryRequest) instead. end_after_days: int + # [Optional] Configuration of asynchronous Downtime retries. downtime: DowntimeRetryRequest + # [Optional] Configuration of asynchronous Dunning retries. dunning: DunningRetryRequest diff --git a/checkout_sdk/payments/sessions/sessions.py b/checkout_sdk/payments/sessions/sessions.py index 87b1b861..6216ad81 100644 --- a/checkout_sdk/payments/sessions/sessions.py +++ b/checkout_sdk/payments/sessions/sessions.py @@ -1,6 +1,7 @@ from datetime import datetime from enum import Enum +from checkout_sdk.common.common import CustomerRetry from checkout_sdk.common.enums import Currency from checkout_sdk.payments.payments import PaymentType, BillingDescriptor, ShippingDetails, \ PaymentRecipient, ProcessingSettings, RiskRequest, ThreeDsRequest, PaymentSender, PaymentPlan, AuthorizationType @@ -104,10 +105,6 @@ class Instruction: purpose: InstructionPurpose -class CustomerRetry: - max_attempts: int - - class AccountHolderType(str, Enum): INDIVIDUAL = 'individual' CORPORATE = 'corporate' diff --git a/tests/payments/retry_request_serialization_test.py b/tests/payments/retry_request_serialization_test.py new file mode 100644 index 00000000..4aa77a72 --- /dev/null +++ b/tests/payments/retry_request_serialization_test.py @@ -0,0 +1,114 @@ +import json + +from checkout_sdk.common.common import CustomerRetry +from checkout_sdk.json_serializer import JsonSerializer +from checkout_sdk.payments.hosted.hosted_payments import HostedPaymentsSessionRequest +from checkout_sdk.payments.links.payments_links import PaymentLinkRequest +from checkout_sdk.payments.payments import PaymentRetryRequest, DunningRetryRequest, DowntimeRetryRequest, \ + PaymentRequest +from checkout_sdk.payments.sessions import sessions as payment_sessions +from checkout_sdk.payments.sessions.sessions import PaymentSessionsRequest + + +def _serialize(obj): + return json.loads(json.dumps(obj, cls=JsonSerializer)) + + +def _build_retry(enabled=True): + dunning = DunningRetryRequest() + dunning.enabled = enabled + dunning.max_attempts = 6 + dunning.end_after_days = 30 + downtime = DowntimeRetryRequest() + downtime.enabled = enabled + retry = PaymentRetryRequest() + retry.enabled = enabled + retry.dunning = dunning + retry.downtime = downtime + return retry + + +class TestRetryRequestSerialization: + + def test_should_serialize_all_retry_properties_on_payment_request(self): + request = PaymentRequest() + request.retry = _build_retry() + body = _serialize(request) + assert body['retry'] == { + 'enabled': True, + 'dunning': {'enabled': True, 'max_attempts': 6, 'end_after_days': 30}, + 'downtime': {'enabled': True}, + } + + def test_should_serialize_enabled_false(self): + body = _serialize(_build_retry(enabled=False)) + assert body['enabled'] is False + assert body['dunning']['enabled'] is False + assert body['downtime']['enabled'] is False + + def test_should_serialize_sdk_objects_key_by_key(self): + body = _serialize(_build_retry()) + assert set(body.keys()) == {'enabled', 'dunning', 'downtime'} + assert body['enabled'] is True + assert set(body['dunning'].keys()) == {'enabled', 'max_attempts', 'end_after_days'} + assert body['dunning']['enabled'] is True + assert body['dunning']['max_attempts'] == 6 + assert body['dunning']['end_after_days'] == 30 + assert body['downtime'] == {'enabled': True} + + def test_should_serialize_deprecated_top_level_fields_with_dunning(self): + retry = _build_retry() + retry.max_attempts = 3 + retry.end_after_days = 10 + assert _serialize(retry) == { + 'enabled': True, + 'max_attempts': 3, + 'end_after_days': 10, + 'dunning': {'enabled': True, 'max_attempts': 6, 'end_after_days': 30}, + 'downtime': {'enabled': True}, + } + + def test_should_not_send_unset_deprecated_top_level_fields(self): + body = _serialize(_build_retry()) + assert 'max_attempts' not in body + assert 'end_after_days' not in body + + +class TestCustomerRetrySerialization: + + def _customer_retry(self): + # swagger example: max_attempts = 2 + customer_retry = CustomerRetry() + customer_retry.max_attempts = 2 + return customer_retry + + def test_should_serialize_on_hosted_payments(self): + request = HostedPaymentsSessionRequest() + request.customer_retry = self._customer_retry() + assert _serialize(request)['customer_retry'] == {'max_attempts': 2} + + def test_should_serialize_on_payment_links(self): + request = PaymentLinkRequest() + request.customer_retry = self._customer_retry() + assert _serialize(request)['customer_retry'] == {'max_attempts': 2} + + def test_should_serialize_on_payment_sessions(self): + request = PaymentSessionsRequest() + request.customer_retry = self._customer_retry() + assert _serialize(request)['customer_retry'] == {'max_attempts': 2} + + def test_should_serialize_zero_attempts(self): + customer_retry = CustomerRetry() + customer_retry.max_attempts = 0 + assert _serialize(customer_retry) == {'max_attempts': 0} + + def test_sessions_module_exposes_shared_type(self): + assert payment_sessions.CustomerRetry is CustomerRetry + + def test_should_roundtrip_customer_retry(self): + body = _serialize(self._customer_retry()) + restored = CustomerRetry() + for key, value in body.items(): + setattr(restored, key, value) + assert restored.max_attempts == 2 + assert _serialize(restored) == body == {'max_attempts': 2}