Skip to content
Merged
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
15 changes: 13 additions & 2 deletions checkout_sdk/common/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -484,9 +484,20 @@ class PaymentSourceType(str, Enum):
BLIK = 'blik'


# Used by ThreeDsRequest (in payments). The /sessions endpoint accepts
# additional exemption-like values — see SessionChallengeIndicator.
class ChallengeIndicator(str, Enum):
"""Indicates the preference for whether or not a 3DS challenge should be performed.

The customer's bank has the final say on whether or not the customer receives the challenge.

This is the four-value indicator accepted by the 3ds.challenge_indicator field on POST
/payments, POST /hosted-payments, POST /payment-links and POST /payment-sessions.

For POST /sessions, which additionally supports requests for exemption, use
checkout_sdk.sessions.sessions.SessionChallengeIndicator.

[Optional]
Default: NO_PREFERENCE
"""
NO_PREFERENCE = 'no_preference'
NO_CHALLENGE_REQUESTED = 'no_challenge_requested'
CHALLENGE_REQUESTED = 'challenge_requested'
Expand Down
65 changes: 54 additions & 11 deletions checkout_sdk/sessions/sessions.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ class SdkInterfaceType(str, Enum):


class ThreeDsMethodCompletion(str, Enum):
Y = 'y'
N = 'n'
U = 'u'
Y = 'Y'
N = 'N'
U = 'U'


class CompletionInfoType(str, Enum):
Expand Down Expand Up @@ -49,10 +49,22 @@ class Category(str, Enum):
NON_PAYMENT = 'non_payment'


# Wider variant of common.enums.ChallengeIndicator. Only used by SessionRequest
# (the /sessions 3DS endpoint), which folds exemption requests into this field
# instead of having a separate `exemption` field like ThreeDsRequest does.
class SessionChallengeIndicator(str, Enum):
"""Indicates whether a challenge is requested for this session.

Used by SessionRequest.challenge_indicator for POST /sessions. This is the only field in the
API that accepts the exemption values below; the 3ds.challenge_indicator field on payments,
hosted payments, payment links and payment sessions accepts only the first four values and is
modelled by checkout_sdk.common.enums.ChallengeIndicator.

The following are requests for exemption: LOW_VALUE, TRUSTED_LISTING, TRUSTED_LISTING_PROMPT
and TRANSACTION_RISK_ASSESSMENT. If an exemption cannot be applied, then the value
NO_CHALLENGE_REQUESTED will be used instead.

[Optional]
Default: NO_PREFERENCE
max 50 characters
"""
NO_PREFERENCE = 'no_preference'
NO_CHALLENGE_REQUESTED = 'no_challenge_requested'
CHALLENGE_REQUESTED = 'challenge_requested'
Expand Down Expand Up @@ -87,6 +99,8 @@ class SessionScheme(str, Enum):
AMEX = 'amex'
DINERS = 'diners'
CARTES_BANCAIRES = 'cartes_bancaires'
DISCOVER = 'discover'
UPI = 'upi'


class AuthenticationMethod(str, Enum):
Expand All @@ -106,7 +120,20 @@ class DeliveryTimeframe(str, Enum):


class ShippingIndicator(str, Enum):
VISA = 'visa'
"""Indicates the shipping method chosen for the transaction.

Used by MerchantRiskInfo.shipping_indicator. Please choose an option that accurately describes
the cardholder's specific transaction.

[Optional]
"""
BILLING_ADDRESS = 'billing_address'
ANOTHER_ADDRESS_ON_FILE = 'another_address_on_file'
NOT_ON_FILE = 'not_on_file'
STORE_PICK_UP = 'store_pick_up'
DIGITAL_GOODS = 'digital_goods'
TRAVEL_AND_EVENT_NO_SHIPPING = 'travel_and_event_no_shipping'
OTHER = 'other'


class SdkEphemeralPublicKey:
Expand All @@ -126,8 +153,6 @@ class SessionMarketplaceData:

class SessionsBillingDescriptor:
name: str
city: str
reference: str


# Channel
Expand Down Expand Up @@ -164,6 +189,13 @@ class BrowserSession(ChannelData):
timezone: str
user_agent: str
ip_address: str
# Whether the Payment API is enabled for all parent frames. This is required for Google SPA
# support in hosted sessions.
# [Optional]
iframe_payment_allowed: bool
# The raw Sec-CH-UA header value. This can improve Google SPA support.
# [Optional]
user_agent_client_hint: str

def __init__(self):
super().__init__(ChannelType.BROWSER)
Expand Down Expand Up @@ -230,12 +262,17 @@ def __init__(self, type_p: SessionSourceType):


class SessionCardSource(SessionSource):
"""A card source for the authentication.

The sessions CardSource schema does not define `store_for_future_use`; that field belongs to the
payments sources, which create an instrument. A session only authenticates a card, so it is not
accepted here.
"""
number: str
expiry_month: int
expiry_year: int
name: str
stored: bool = False
store_for_future_use: bool

def __init__(self):
super().__init__(SessionSourceType.CARD)
Expand Down Expand Up @@ -373,6 +410,13 @@ class GoogleSpa:


class SessionRequest:
"""The request body for POST /sessions.

Declares exactly the 24 properties of the SessionRequest schema. `prior_transaction_reference`
was carried here from a June 2022 sessions update but is absent from the current API Reference,
from the API schema search and from the developer documentation, so it is no longer declared.
Assigning it still serializes, should the API accept it.
"""
source: SessionSource = SessionCardSource()
amount: int
currency: Currency
Expand All @@ -385,7 +429,6 @@ class SessionRequest:
billing_descriptor: SessionsBillingDescriptor
reference: str
merchant_risk_info: MerchantRiskInfo
prior_transaction_reference: str
transaction_type: TransactionType = TransactionType.GOODS_SERVICE
shipping_address: SessionAddress
shipping_address_matches_billing: bool
Expand Down
87 changes: 87 additions & 0 deletions tests/sessions/challenge_indicator_serialization_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import json

import pytest

from checkout_sdk.common.enums import ChallengeIndicator
from checkout_sdk.json_serializer import JsonSerializer
from checkout_sdk.payments.payments import ThreeDsRequest
from checkout_sdk.sessions.sessions import SessionChallengeIndicator, SessionRequest

# The nine values accepted by SessionRequest.challenge_indicator, per the API Reference
# ChallengeIndicator schema, in spec order.
SESSION_VALUES = [
'no_preference',
'no_challenge_requested',
'challenge_requested',
'challenge_requested_mandate',
'low_value',
'trusted_listing',
'trusted_listing_prompt',
'transaction_risk_assessment',
'data_share',
]

# The four values accepted by the 3ds.challenge_indicator field on payments, hosted payments,
# payment links and payment sessions.
PAYMENT_VALUES = [
'no_preference',
'no_challenge_requested',
'challenge_requested',
'challenge_requested_mandate',
]


def _serialize(obj):
return json.loads(json.dumps(obj, cls=JsonSerializer))


class TestChallengeIndicatorSerialization:
"""Covers the two challenge-indicator enums and their call sites: the nine-value sessions enum
used by POST /sessions, and the four-value shared enum used by the payments 3ds field.
"""

def test_session_enum_exposes_all_nine_spec_values_in_order(self):
assert [member.value for member in SessionChallengeIndicator] == SESSION_VALUES

def test_shared_enum_exposes_only_the_four_payment_values(self):
assert [member.value for member in ChallengeIndicator] == PAYMENT_VALUES

@pytest.mark.parametrize('value', SESSION_VALUES)
def test_every_session_value_serializes_on_session_request(self, value):
request = SessionRequest()
request.challenge_indicator = SessionChallengeIndicator(value)

assert _serialize(request)['challenge_indicator'] == value

def test_session_request_defaults_to_no_preference(self):
request = SessionRequest()

assert request.challenge_indicator == SessionChallengeIndicator.NO_PREFERENCE
assert _serialize(request)['challenge_indicator'] == 'no_preference'

@pytest.mark.parametrize('value', PAYMENT_VALUES)
def test_every_payment_value_serializes_on_three_ds_request(self, value):
request = ThreeDsRequest()
request.challenge_indicator = ChallengeIndicator(value)

assert _serialize(request)['challenge_indicator'] == value

@pytest.mark.parametrize('value', SESSION_VALUES)
def test_every_session_value_round_trips_through_the_enum(self, value):
assert SessionChallengeIndicator(value).value == value

def test_the_five_exemption_values_are_absent_from_the_shared_enum(self):
"""The exemption values must not leak onto the payments enum: the API rejects them on
3ds.challenge_indicator. This is the guard the split exists to provide.
"""
exemptions = {
'low_value',
'trusted_listing',
'trusted_listing_prompt',
'transaction_risk_assessment',
'data_share',
}
shared = {member.value for member in ChallengeIndicator}

assert exemptions.isdisjoint(shared)
assert exemptions.issubset({member.value for member in SessionChallengeIndicator})
Loading
Loading