From 6a0371b8cdf05e22ab75e53ad0060fa07618f270 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Armando=20Rodr=C3=ADguez?= <127134616+armando-rodriguez-cko@users.noreply.github.com> Date: Thu, 24 Sep 2026 18:10:39 +0200 Subject: [PATCH 1/2] feat(issuing): add scheduled_revocation_date and update-card status Swagger 2026-09-17: add-card-request and update-card-request gain scheduled_revocation_date (replaces deprecated revocation_date); update-card-request gains status to reactivate an inactive/suspended card. update-card-response no longer includes encrypted_cvv on the live API; this SDK has no typed issuing response classes, so tests are updated to reflect the current response shape. --- checkout_sdk/issuing/cards.py | 20 +++++++++++++++-- .../issuing/card_update_headers_wire_test.py | 12 +++++----- .../issuing/cards_issuing_integration_test.py | 18 ++++----------- tests/issuing/issuing_serialization_test.py | 22 ++++++++++++++++++- 4 files changed, 50 insertions(+), 22 deletions(-) diff --git a/checkout_sdk/issuing/cards.py b/checkout_sdk/issuing/cards.py index 0fe9057..d517e36 100644 --- a/checkout_sdk/issuing/cards.py +++ b/checkout_sdk/issuing/cards.py @@ -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' @@ -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 @@ -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 @@ -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 @@ -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: diff --git a/tests/issuing/card_update_headers_wire_test.py b/tests/issuing/card_update_headers_wire_test.py index 346c6f9..6dd3022 100644 --- a/tests/issuing/card_update_headers_wire_test.py +++ b/tests/issuing/card_update_headers_wire_test.py @@ -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, diff --git a/tests/issuing/cards_issuing_integration_test.py b/tests/issuing/cards_issuing_integration_test.py index c6a006d..319954d 100644 --- a/tests/issuing/cards_issuing_integration_test.py +++ b/tests/issuing/cards_issuing_integration_test.py @@ -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 @@ -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() diff --git a/tests/issuing/issuing_serialization_test.py b/tests/issuing/issuing_serialization_test.py index 37b344e..cf60934 100644 --- a/tests/issuing/issuing_serialization_test.py +++ b/tests/issuing/issuing_serialization_test.py @@ -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, @@ -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): From 64373c19f2bbe676c0c146222dbe598881253aea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Armando=20Rodr=C3=ADguez?= <127134616+armando-rodriguez-cko@users.noreply.github.com> Date: Fri, 25 Sep 2026 11:28:25 +0200 Subject: [PATCH 2/2] fix(issuing): document is_single_use on update-card-response for virtual cards Swagger 2026-09-23 split update-card-response into a virtual/physical discriminator; the virtual variant adds is_single_use (specifies whether the card is set to expire after a single use). Physical cards never send it. This SDK has no typed issuing response classes, so this is a doc + test update. --- checkout_sdk/issuing/issuing_client.py | 2 ++ tests/issuing/card_update_headers_wire_test.py | 12 ++++++++++++ 2 files changed, 14 insertions(+) diff --git a/checkout_sdk/issuing/issuing_client.py b/checkout_sdk/issuing/issuing_client.py index d9cfdad..3a9cfd9 100644 --- a/checkout_sdk/issuing/issuing_client.py +++ b/checkout_sdk/issuing/issuing_client.py @@ -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(), diff --git a/tests/issuing/card_update_headers_wire_test.py b/tests/issuing/card_update_headers_wire_test.py index 6dd3022..41d4daa 100644 --- a/tests/issuing/card_update_headers_wire_test.py +++ b/tests/issuing/card_update_headers_wire_test.py @@ -129,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