Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions checkout_sdk/common/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
16 changes: 16 additions & 0 deletions checkout_sdk/payments/payments.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
5 changes: 1 addition & 4 deletions checkout_sdk/payments/sessions/sessions.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -104,10 +105,6 @@ class Instruction:
purpose: InstructionPurpose


class CustomerRetry:
max_attempts: int


class AccountHolderType(str, Enum):
INDIVIDUAL = 'individual'
CORPORATE = 'corporate'
Expand Down
114 changes: 114 additions & 0 deletions tests/payments/retry_request_serialization_test.py
Original file line number Diff line number Diff line change
@@ -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}
Loading