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
20 changes: 18 additions & 2 deletions checkout_sdk/issuing/cards.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ class SuspendReason(str, Enum):
SUSPECTED_STOLEN = 'suspected_stolen'


class CardStatusUpdate(str, Enum):
# Set on UpdateCardRequest.status to reactivate an inactive or suspended card.
# Mutually exclusive with UpdateCardRequest.scheduled_activation_date.
ACTIVE = 'active'


class ReturnCredentials(str, Enum):
NUMBER = 'number'
CVC2 = 'cvc2'
Expand Down Expand Up @@ -94,7 +100,8 @@ class CardRequest:
# User's metadata.
# [Optional]
metadata: CardMetadata
# Date scheduling the card's automatic revocation.
# Deprecated: use scheduled_revocation_date instead. If both are provided,
# scheduled_revocation_date overrides this value.
# [Optional]
# Format: yyyy-MM-dd (time is midnight UTC)
# Example: 2027-03-12
Expand All @@ -107,6 +114,8 @@ class CardRequest:
# [Optional]
# Example: 2026-06-01T10:00Z
scheduled_activation_date: str
# yyyy-mm-dd. The card is revoked at midnight UTC on this date. (IssuingScheduledRevocationDate)
scheduled_revocation_date: str

def __init__(self, type_p: CardType):
self.type = type_p
Expand All @@ -131,6 +140,10 @@ def __init__(self):

class UpdateCardRequest:
"""Request body for PATCH /issuing/cards/{cardId}."""
# Set to CardStatusUpdate.ACTIVE to reactivate an inactive or suspended card.
# Mutually exclusive with scheduled_activation_date (API returns
# scheduled_activation_date_conflicts_with_activation otherwise).
status: CardStatusUpdate
# Your reference.
# [Optional]
# max 256 characters
Expand Down Expand Up @@ -159,11 +172,14 @@ class UpdateCardRequest:
# [Optional]
# Example: 2026-06-01T10:00Z
scheduled_activation_date: str
# Date scheduling the card's automatic revocation.
# Deprecated: use scheduled_revocation_date instead. If both are provided,
# scheduled_revocation_date overrides this value.
# [Optional]
# Format: yyyy-MM-dd (time is midnight UTC)
# Example: 2027-03-12
revocation_date: str
# yyyy-mm-dd. The card is revoked at midnight UTC on this date. (IssuingScheduledRevocationDate)
scheduled_revocation_date: str


class CardUpdateHeaders:
Expand Down
2 changes: 2 additions & 0 deletions checkout_sdk/issuing/issuing_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ def update_card(self, card_id: str, update_card_request: UpdateCardRequest,
code encryption_key_required.
Returns:
ResponseWrapper with the update response, including encrypted_cvv when requested.
For a virtual card, the response may also include is_single_use, specifying whether
the card is set to expire after a single use. Physical cards never send it.
"""
return self._api_client.patch(self.build_path(self.__ISSUING, self.__CARDS, card_id),
self._sdk_authorization(),
Expand Down
24 changes: 19 additions & 5 deletions tests/issuing/card_update_headers_wire_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,18 +106,20 @@ def test_a_422_surfaces_the_encryption_key_required_error(self, mock_sdk_configu
assert 'encryption_key_required' in exc.value.error_details
assert exc.value.request_id == '0HLHPN8802NUF:00000003'

def test_a_successful_update_returns_the_encrypted_cvv(self, mock_sdk_configuration):
client, _ = _build(mock_sdk_configuration, body='{'
'"last_modified_date":"2026-06-01T10:00:00Z",'
'"encrypted_cvv":"oJMoNMEEUiQKYOsQ4Zd"}')
def test_a_successful_update_with_the_headers_still_has_no_encrypted_cvv(self, mock_sdk_configuration):
"""The 2026-09-17 spec (INT-1700) removed encrypted_cvv from update-card-response
entirely, so return-encrypted-cvv/Encryption-Key no longer make the response carry it.
This test previously asserted the opposite (added by INT-1695, when the field still
existed)."""
client, _ = _build(mock_sdk_configuration, body='{"last_modified_date":"2026-06-01T10:00:00Z"}')
headers = CardUpdateHeaders()
headers.return_encrypted_cvv = 'true'
headers.encryption_key = 'MIIBIjAN'

response = client.update_card('crd_123', UpdateCardRequest(), headers)

assert response.encrypted_cvv == 'oJMoNMEEUiQKYOsQ4Zd'
assert response.last_modified_date == '2026-06-01T10:00:00Z'
assert not hasattr(response, 'encrypted_cvv')

def test_a_successful_update_without_the_headers_has_no_encrypted_cvv(self, mock_sdk_configuration):
client, _ = _build(mock_sdk_configuration,
Expand All @@ -127,3 +129,15 @@ def test_a_successful_update_without_the_headers_has_no_encrypted_cvv(self, mock

assert response.last_modified_date == '2026-06-01T10:00:00Z'
assert not hasattr(response, 'encrypted_cvv')

def test_update_card_response_round_trips_is_single_use_for_virtual_cards(self, mock_sdk_configuration):
"""The 2026-09-23 spec update split update-card-response into a virtual/physical
discriminator; the virtual variant adds is_single_use."""
client, _ = _build(mock_sdk_configuration, body='{'
'"type":"virtual",'
'"last_modified_date":"2026-06-01T10:00:00Z",'
'"is_single_use":true}')

response = client.update_card('crd_123', UpdateCardRequest())

assert response.is_single_use is True
18 changes: 4 additions & 14 deletions tests/issuing/cards_issuing_integration_test.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import os
from datetime import datetime, timedelta

import pytest

from checkout_sdk.issuing.cards import PasswordEnrollmentRequest, SecurityPair, UpdateThreeDsEnrollmentRequest, \
CardCredentialsQuery, CardUpdateHeaders, RevokeRequest, RevokeReason, SuspendRequest, SuspendReason, \
CardCredentialsQuery, RevokeRequest, RevokeReason, SuspendRequest, SuspendReason, \
UpdateCardRequest, CardMetadata, VirtualCardRenewRequest
from tests.checkout_test_utils import assert_response, phone

Expand Down Expand Up @@ -69,18 +68,9 @@ def test_should_update_card_scheduled_activation_date(self, issuing_checkout_api
assert_response(response)
assert response.http_metadata.status_code == 200

def test_should_update_card_returning_the_encrypted_cvv(self, issuing_checkout_api, active_card):
request = UpdateCardRequest()
request.reference = 'UPDATED-REF-123'

headers = CardUpdateHeaders()
headers.return_encrypted_cvv = 'true'
headers.encryption_key = os.environ.get('CHECKOUT_ISSUING_ENCRYPTION_KEY', '')

response = issuing_checkout_api.issuing.update_card(active_card.id, request, headers)

assert_response(response, 'encrypted_cvv')
assert response.http_metadata.status_code == 200
# The 2026-09-17 spec (INT-1700) removed encrypted_cvv from update-card-response entirely,
# so return-encrypted-cvv/Encryption-Key no longer make the update response carry it. This
# test previously asserted the opposite (added by INT-1695, when the field still existed).

def test_should_renew_card(self, issuing_checkout_api, card):
request = VirtualCardRenewRequest()
Expand Down
22 changes: 21 additions & 1 deletion tests/issuing/issuing_serialization_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from checkout_sdk.json_serializer import JsonSerializer
from checkout_sdk.api_client import ApiClient
from checkout_sdk.issuing.cards import CardRequest, CardType, CardUpdateHeaders, UpdateCardRequest, \
VirtualCardRequest
VirtualCardRequest, CardStatusUpdate
from checkout_sdk.issuing.disputes import (
IssuingDisputeFraudType, IssuingDisputeFraudDetails, CreateDisputeRequest,
EscalateDisputeRequest, AmendDisputeRequest, SubmitDisputeRequest,
Expand Down Expand Up @@ -46,16 +46,36 @@ def test_update_card_serializes_scheduled_activation_and_revocation_date(self):
'revocation_date': '2027-03-12',
}

def test_update_card_serializes_scheduled_revocation_date_alongside_deprecated_field(self):
request = UpdateCardRequest()
request.reference = 'ref'
request.revocation_date = '2027-03-12'
request.scheduled_revocation_date = '2027-04-01'

assert _serialize(request) == {
'reference': 'ref',
'revocation_date': '2027-03-12',
'scheduled_revocation_date': '2027-04-01',
}

def test_update_card_serializes_status_to_reactivate_card(self):
request = UpdateCardRequest()
request.status = CardStatusUpdate.ACTIVE

assert _serialize(request) == {'status': 'active'}

def test_create_card_serializes_scheduled_activation_date(self):
request = VirtualCardRequest()
request.cardholder_id = 'crh_1'
request.scheduled_activation_date = '2026-06-01T10:00Z'
request.revocation_date = '2027-03-12'
request.scheduled_revocation_date = '2027-04-01'

result = _serialize(request)
assert result['type'] == CardType.VIRTUAL.value
assert result['scheduled_activation_date'] == '2026-06-01T10:00Z'
assert result['revocation_date'] == '2027-03-12'
assert result['scheduled_revocation_date'] == '2027-04-01'
assert 'activation_date' not in result

def test_revocation_date_accepts_a_date_object_through_the_serializer(self):
Expand Down
Loading