diff --git a/src/main/java/com/checkout/issuing/IssuingClient.java b/src/main/java/com/checkout/issuing/IssuingClient.java index 4f542b22..0776efd9 100644 --- a/src/main/java/com/checkout/issuing/IssuingClient.java +++ b/src/main/java/com/checkout/issuing/IssuingClient.java @@ -20,6 +20,7 @@ import com.checkout.issuing.cards.requests.update.UpdateCardRequest; import com.checkout.issuing.cards.responses.CardDetailsResponse; import com.checkout.issuing.cards.responses.CardResponse; +import com.checkout.issuing.cards.responses.activate.ActivateCardResponse; import com.checkout.issuing.cards.responses.GetDigitalCardResponse; import com.checkout.issuing.cards.responses.credentials.CardCredentialsResponse; import com.checkout.issuing.cards.responses.enrollment.ThreeDSEnrollmentDetailsResponse; @@ -87,7 +88,7 @@ public interface IssuingClient { CompletableFuture getCardThreeDSDetails(String cardId); - CompletableFuture activateCard(String cardId); + CompletableFuture activateCard(String cardId); CompletableFuture getCardCredentials(final String cardId, final CardCredentialsQuery queryFilter); @@ -229,7 +230,7 @@ CompletableFuture simulateReversal( ThreeDSEnrollmentDetailsResponse getCardThreeDSDetailsSync(String cardId); - VoidResponse activateCardSync(String cardId); + ActivateCardResponse activateCardSync(String cardId); CardCredentialsResponse getCardCredentialsSync(String cardId, CardCredentialsQuery queryFilter); diff --git a/src/main/java/com/checkout/issuing/IssuingClientImpl.java b/src/main/java/com/checkout/issuing/IssuingClientImpl.java index 4da2d35c..ea477e3c 100644 --- a/src/main/java/com/checkout/issuing/IssuingClientImpl.java +++ b/src/main/java/com/checkout/issuing/IssuingClientImpl.java @@ -25,6 +25,7 @@ import com.checkout.issuing.cards.requests.update.UpdateCardRequest; import com.checkout.issuing.cards.responses.CardDetailsResponse; import com.checkout.issuing.cards.responses.CardResponse; +import com.checkout.issuing.cards.responses.activate.ActivateCardResponse; import com.checkout.issuing.cards.responses.credentials.CardCredentialsResponse; import com.checkout.issuing.cards.responses.enrollment.ThreeDSEnrollmentDetailsResponse; import com.checkout.issuing.cards.responses.enrollment.ThreeDSEnrollmentResponse; @@ -254,12 +255,12 @@ public CompletableFuture getCardThreeDSDetails } @Override - public CompletableFuture activateCard(final String cardId) { + public CompletableFuture activateCard(final String cardId) { validateParams("cardId", cardId); return apiClient.postAsync( buildPath(ISSUING_PATH, CARDS_PATH, cardId, ACTIVATE_PATH), sdkAuthorization(), - VoidResponse.class, + ActivateCardResponse.class, null, null ); @@ -845,12 +846,12 @@ public ThreeDSEnrollmentDetailsResponse getCardThreeDSDetailsSync(final String c } @Override - public VoidResponse activateCardSync(final String cardId) { + public ActivateCardResponse activateCardSync(final String cardId) { validateCardId(cardId); return apiClient.post( buildPath(ISSUING_PATH, CARDS_PATH, cardId, ACTIVATE_PATH), sdkAuthorization(), - VoidResponse.class, + ActivateCardResponse.class, null, null ); diff --git a/src/main/java/com/checkout/issuing/cards/requests/create/CardRequest.java b/src/main/java/com/checkout/issuing/cards/requests/create/CardRequest.java index d1a16caa..75c41176 100644 --- a/src/main/java/com/checkout/issuing/cards/requests/create/CardRequest.java +++ b/src/main/java/com/checkout/issuing/cards/requests/create/CardRequest.java @@ -49,11 +49,23 @@ public abstract class CardRequest { *

* [Optional] *

- * Format: date (YYYY-MM-DD, time is midnight UTC) - * Example: 2027-03-12 + * Format: yyyy-MM-dd + * + * @deprecated Use {@link #scheduledRevocationDate} instead. If both fields are provided, + * {@code scheduledRevocationDate} overrides this value. */ + @Deprecated private LocalDate revocationDate; + /** + * The card will be revoked at midnight UTC on the date specified. + *

+ * [Optional] + *

+ * Format: yyyy-MM-dd + */ + private LocalDate scheduledRevocationDate; + /** * Date scheduling the card's first activation. Only applies to the initial activation of a * card. Two formats are supported: date only (YYYY-MM-DD, treated as midnight UTC), or date diff --git a/src/main/java/com/checkout/issuing/cards/requests/create/PhysicalCardRequest.java b/src/main/java/com/checkout/issuing/cards/requests/create/PhysicalCardRequest.java index 2d383756..6c5e7f29 100644 --- a/src/main/java/com/checkout/issuing/cards/requests/create/PhysicalCardRequest.java +++ b/src/main/java/com/checkout/issuing/cards/requests/create/PhysicalCardRequest.java @@ -32,11 +32,13 @@ private PhysicalCardRequest(final String cardholderId, final ShippingInstruction shippingInstructions, final IssuingCardMetadata metadata, final LocalDate revocationDate, + final LocalDate scheduledRevocationDate, final String scheduledActivationDate) { super(CardType.PHYSICAL, cardholderId, lifetime, reference, cardProductId, displayName, activateCard); this.shippingInstructions = shippingInstructions; setMetadata(metadata); setRevocationDate(revocationDate); + setScheduledRevocationDate(scheduledRevocationDate); setScheduledActivationDate(scheduledActivationDate); } diff --git a/src/main/java/com/checkout/issuing/cards/requests/create/VirtualCardRequest.java b/src/main/java/com/checkout/issuing/cards/requests/create/VirtualCardRequest.java index 7c47cea7..58cef5d0 100644 --- a/src/main/java/com/checkout/issuing/cards/requests/create/VirtualCardRequest.java +++ b/src/main/java/com/checkout/issuing/cards/requests/create/VirtualCardRequest.java @@ -53,6 +53,7 @@ private VirtualCardRequest(final String cardholderId, final Boolean isSingleUse, final IssuingCardMetadata metadata, final LocalDate revocationDate, + final LocalDate scheduledRevocationDate, final String scheduledActivationDate, final List returnCredentials, final List controlProfiles, @@ -64,6 +65,7 @@ private VirtualCardRequest(final String cardholderId, this.controls = controls; setMetadata(metadata); setRevocationDate(revocationDate); + setScheduledRevocationDate(scheduledRevocationDate); setScheduledActivationDate(scheduledActivationDate); } } \ No newline at end of file diff --git a/src/main/java/com/checkout/issuing/cards/requests/update/UpdateCardRequest.java b/src/main/java/com/checkout/issuing/cards/requests/update/UpdateCardRequest.java index 23cf05e9..f36bd84e 100644 --- a/src/main/java/com/checkout/issuing/cards/requests/update/UpdateCardRequest.java +++ b/src/main/java/com/checkout/issuing/cards/requests/update/UpdateCardRequest.java @@ -1,5 +1,6 @@ package com.checkout.issuing.cards.requests.update; +import com.checkout.issuing.cards.CardStatus; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -13,6 +14,17 @@ @AllArgsConstructor public final class UpdateCardRequest { + /** + * Set the card's status to {@code ACTIVE} to reactivate an {@code INACTIVE} or + * {@code SUSPENDED} card. The API only accepts {@code active} for this field. + *

+ * [Optional] + *

+ * Mutually exclusive with {@link #scheduledActivationDate}: submitting both results in the + * API error {@code scheduled_activation_date_conflicts_with_activation}. + */ + private CardStatus status; + /** * Your reference. * [Optional] @@ -58,8 +70,20 @@ public final class UpdateCardRequest { *

* [Optional] *

- * Format: date (YYYY-MM-DD, time is midnight UTC) - * Example: 2027-03-12 + * Format: yyyy-MM-dd + * + * @deprecated Use {@link #scheduledRevocationDate} instead. If both fields are provided, + * {@code scheduledRevocationDate} overrides this value. */ + @Deprecated private LocalDate revocationDate; + + /** + * The card will be revoked at midnight UTC on the date specified. + *

+ * [Optional] + *

+ * Format: yyyy-MM-dd + */ + private LocalDate scheduledRevocationDate; } \ No newline at end of file diff --git a/src/main/java/com/checkout/issuing/cards/responses/CardDetailsResponse.java b/src/main/java/com/checkout/issuing/cards/responses/CardDetailsResponse.java index 9402a320..58fcae07 100644 --- a/src/main/java/com/checkout/issuing/cards/responses/CardDetailsResponse.java +++ b/src/main/java/com/checkout/issuing/cards/responses/CardDetailsResponse.java @@ -164,11 +164,31 @@ public abstract class CardDetailsResponse extends Resource { *

* [Optional] *

- * Format: date (YYYY-MM-DD, time is midnight UTC) - * Example: 2027-03-12 + * Format: yyyy-MM-dd + * + * @deprecated Use {@link #scheduledRevocationDate} instead. */ + @Deprecated protected LocalDate revocationDate; + /** + * The card will be revoked at midnight UTC on the date specified. + *

+ * [Optional] + *

+ * Format: yyyy-MM-dd + */ + protected LocalDate scheduledRevocationDate; + + /** + * The date and time the card was last activated. + *

+ * [Optional, nullable, read only] + *

+ * Returns {@code null} if the card has never been activated. + */ + protected Instant lastActivatedOn; + /** * When a card is renewed, the unique identifier of the first card in the renewal history. * [Optional] diff --git a/src/main/java/com/checkout/issuing/cards/responses/CardResponse.java b/src/main/java/com/checkout/issuing/cards/responses/CardResponse.java index 578edb66..3489690c 100644 --- a/src/main/java/com/checkout/issuing/cards/responses/CardResponse.java +++ b/src/main/java/com/checkout/issuing/cards/responses/CardResponse.java @@ -31,4 +31,13 @@ public final class CardResponse extends Resource { protected String reference; protected Instant createdDate; + + /** + * The date and time the card was last activated. + *

+ * [Optional, nullable, read only] + *

+ * Returns {@code null} if the card has never been activated. + */ + protected Instant lastActivatedOn; } diff --git a/src/main/java/com/checkout/issuing/cards/responses/activate/ActivateCardResponse.java b/src/main/java/com/checkout/issuing/cards/responses/activate/ActivateCardResponse.java new file mode 100644 index 00000000..f569b4c6 --- /dev/null +++ b/src/main/java/com/checkout/issuing/cards/responses/activate/ActivateCardResponse.java @@ -0,0 +1,25 @@ +package com.checkout.issuing.cards.responses.activate; + +import com.checkout.common.Resource; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.time.Instant; + +/** + * Response returned when activating a card. + */ +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public final class ActivateCardResponse extends Resource { + + /** + * The time the card was activated. + *

+ * [Required] + *

+ */ + private Instant lastActivatedOn; +} diff --git a/src/main/java/com/checkout/issuing/cards/responses/update/UpdateCardResponse.java b/src/main/java/com/checkout/issuing/cards/responses/update/UpdateCardResponse.java index b5569b39..19cf09e7 100644 --- a/src/main/java/com/checkout/issuing/cards/responses/update/UpdateCardResponse.java +++ b/src/main/java/com/checkout/issuing/cards/responses/update/UpdateCardResponse.java @@ -1,20 +1,149 @@ package com.checkout.issuing.cards.responses.update; +import com.checkout.common.CountryCode; +import com.checkout.common.Currency; import com.checkout.common.Resource; +import com.checkout.issuing.cards.CardStatus; +import com.checkout.issuing.cards.CardType; +import com.checkout.issuing.cards.IssuingScheme; +import com.checkout.issuing.cards.requests.update.IssuingCardMetadata; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; import java.time.Instant; +import java.time.LocalDate; /** - * The response returned when a card's details are updated. + * Response returned when updating a card. + *

+ * As of the 2026-09-17 API change, {@code update-card-response} is defined as the full + * {@code get-card-response} field set (see {@link com.checkout.issuing.cards.responses.CardDetailsResponse}) + * with {@code lastModifiedDate} required. It no longer includes {@code encrypted_cvv}: that field + * was added by the 2026-09-02 change and removed again by this one. */ @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public final class UpdateCardResponse extends Resource { + private String id; + + private String cardholderId; + + private String cardProductId; + + private String clientId; + + /** + * The entity's unique identifier. + * [Optional] + * ^ent_[a-z0-9]{26}$ + * min 30 characters, max 30 characters + */ + private String entityId; + + /** + * The Dashboard user's unique identifier. + * [Optional] + * ^usr_[a-z0-9]{26}$ + * min 30 characters, max 30 characters + */ + private String userId; + + private String lastFour; + + private Integer expiryMonth; + + private Integer expiryYear; + + private CardStatus status; + + private CardType type; + + private String displayName; + + private Currency billingCurrency; + + private CountryCode issuingCountry; + + private String reference; + + /** + * User's metadata. + * [Optional] + */ + private IssuingCardMetadata metadata; + + private Instant createdDate; + + /** + * Date scheduling the card's first activation. Only applies to the initial activation of a + * card. Two formats are supported: date only (YYYY-MM-DD, treated as midnight UTC), or date + * with round hour (YYYY-MM-DDTHH:mmZ in UTC, or YYYY-MM-DDTHH:mm+HH:mm with offset). Only + * round hours are allowed when a time is provided (HH:00). The value must be at least the next + * round hour after the request time. + *

+ * [Optional] + *

+ * Example: 2026-06-01T10:00Z + */ + private String scheduledActivationDate; + + /** + * Date for the card to be automatically revoked. Must be after the current date and date only in the + * form yyyy-mm-dd. + *

+ * [Optional] + *

+ * Format: yyyy-MM-dd + * + * @deprecated Use {@link #scheduledRevocationDate} instead. + */ + @Deprecated + private LocalDate revocationDate; + + /** + * The card will be revoked at midnight UTC on the date specified. + *

+ * [Optional] + *

+ * Format: yyyy-MM-dd + */ + private LocalDate scheduledRevocationDate; + + /** + * The date and time the card was last activated. + *

+ * [Optional, nullable, read only] + *

+ * Returns {@code null} if the card has never been activated. + */ + private Instant lastActivatedOn; + + /** + * When a card is renewed, the unique identifier of the first card in the renewal history. + * [Optional] + * ^crd_[a-z0-9]{26}$ + * min 30 characters, max 30 characters + */ + private String rootCardId; + + /** + * When a card is renewed, the unique identifier of the previous card in the renewal history. + * [Optional] + * ^crd_[a-z0-9]{26}$ + * min 30 characters, max 30 characters + */ + private String parentCardId; + + /** + * The card scheme. + * [Optional] + * Enum: "mastercard" "visa" + */ + private IssuingScheme scheme; + /** * The date and time when the card was last modified, in UTC. * [Required] @@ -23,8 +152,9 @@ public final class UpdateCardResponse extends Resource { private Instant lastModifiedDate; /** - * The card's encrypted CVV, returned when the return-encrypted-cvv header is true. + * Specifies whether the virtual card is set to expire after a single use. + * Only present when the underlying card is virtual; physical cards never send it. * [Optional] */ - private String encryptedCvv; + private Boolean isSingleUse; } diff --git a/src/test/java/com/checkout/issuing/IssuingCardsTestIT.java b/src/test/java/com/checkout/issuing/IssuingCardsTestIT.java index c4968d06..a112f68a 100644 --- a/src/test/java/com/checkout/issuing/IssuingCardsTestIT.java +++ b/src/test/java/com/checkout/issuing/IssuingCardsTestIT.java @@ -19,6 +19,7 @@ import com.checkout.issuing.cards.responses.enrollment.ThreeDSUpdateResponse; import com.checkout.issuing.cards.responses.renew.RenewCardResponse; import com.checkout.issuing.cards.responses.update.UpdateCardResponse; +import com.checkout.issuing.cards.responses.activate.ActivateCardResponse; import com.checkout.payments.VoidResponse; import org.apache.http.HttpStatus; import org.junit.jupiter.api.BeforeAll; @@ -85,7 +86,7 @@ void shouldGetThreeDSDetails() { @Test void shouldActivateCard() { - final VoidResponse activationResponse = blocking(() -> + final ActivateCardResponse activationResponse = blocking(() -> issuingApi.issuingClient().activateCard(card.getId())); validateActivateCardResponse(activationResponse); @@ -208,7 +209,7 @@ void shouldGetThreeDSDetailsSync() { @Test void shouldActivateCardSync() { - final VoidResponse activationResponse = + final ActivateCardResponse activationResponse = issuingApi.issuingClient().activateCardSync(card.getId()); validateActivateCardResponse(activationResponse); @@ -359,9 +360,10 @@ private void validateThreeDSEnrollmentDetails(ThreeDSEnrollmentDetailsResponse r assertNotNull(response); } - private void validateActivateCardResponse(VoidResponse activatedResponse) { + private void validateActivateCardResponse(ActivateCardResponse activatedResponse) { assertNotNull(activatedResponse); assertEquals(HttpStatus.SC_OK, activatedResponse.getHttpStatusCode()); + assertNotNull(activatedResponse.getLastActivatedOn()); } private void validateCredentialsResponse(CardCredentialsResponse credentialsResponse) { diff --git a/src/test/java/com/checkout/issuing/IssuingClientImplTest.java b/src/test/java/com/checkout/issuing/IssuingClientImplTest.java index a1994a75..5548605a 100644 --- a/src/test/java/com/checkout/issuing/IssuingClientImplTest.java +++ b/src/test/java/com/checkout/issuing/IssuingClientImplTest.java @@ -25,6 +25,7 @@ import com.checkout.issuing.cards.requests.update.UpdateCardRequest; import com.checkout.issuing.cards.responses.CardDetailsResponse; import com.checkout.issuing.cards.responses.CardResponse; +import com.checkout.issuing.cards.responses.activate.ActivateCardResponse; import com.checkout.issuing.cards.responses.credentials.CardCredentialsResponse; import com.checkout.issuing.cards.responses.enrollment.ThreeDSEnrollmentDetailsResponse; import com.checkout.issuing.cards.responses.enrollment.ThreeDSEnrollmentResponse; @@ -306,19 +307,19 @@ void shouldGetThreeDSDetails() throws ExecutionException, InterruptedException { @Test void shouldActivateCard() throws ExecutionException, InterruptedException { - final VoidResponse response = createVoidResponse(); + final ActivateCardResponse response = createActivateCardResponse(); when(apiClient.postAsync( "issuing/cards/card_id/activate", authorization, - VoidResponse.class, + ActivateCardResponse.class, null, null )).thenReturn(CompletableFuture.completedFuture(response)); - final CompletableFuture future = client.activateCard("card_id"); + final CompletableFuture future = client.activateCard("card_id"); - validateVoidResponse(response, future.get()); + validateActivateCardResponse(response, future.get()); } @Test @@ -1135,19 +1136,19 @@ void shouldGetThreeDSDetailsSync() { @Test void shouldActivateCardSync() { - final VoidResponse expectedResponse = createVoidResponse(); + final ActivateCardResponse expectedResponse = createActivateCardResponse(); when(apiClient.post( "issuing/cards/card_id/activate", authorization, - VoidResponse.class, + ActivateCardResponse.class, null, null )).thenReturn(expectedResponse); - final VoidResponse actualResponse = client.activateCardSync("card_id"); + final ActivateCardResponse actualResponse = client.activateCardSync("card_id"); - validateVoidResponse(expectedResponse, actualResponse); + validateActivateCardResponse(expectedResponse, actualResponse); } @Test @@ -1910,6 +1911,10 @@ private VoidResponse createVoidResponse() { return mock(VoidResponse.class); } + private ActivateCardResponse createActivateCardResponse() { + return mock(ActivateCardResponse.class); + } + private CardCredentialsQuery createCardCredentialsQuery() { return mock(CardCredentialsQuery.class); } @@ -2077,6 +2082,11 @@ private void validateVoidResponse(VoidResponse expected, VoidResponse actual) { assertEquals(expected, actual); } + private void validateActivateCardResponse(ActivateCardResponse expected, ActivateCardResponse actual) { + assertNotNull(actual); + assertEquals(expected, actual); + } + private void validateCardCredentialsResponse(CardCredentialsResponse expected, CardCredentialsResponse actual) { assertNotNull(actual); assertEquals(expected, actual); diff --git a/src/test/java/com/checkout/issuing/cards/CardScheduledRevocationAndActivationSerializationTest.java b/src/test/java/com/checkout/issuing/cards/CardScheduledRevocationAndActivationSerializationTest.java new file mode 100644 index 00000000..b498943e --- /dev/null +++ b/src/test/java/com/checkout/issuing/cards/CardScheduledRevocationAndActivationSerializationTest.java @@ -0,0 +1,148 @@ +package com.checkout.issuing.cards; + +import com.checkout.GsonSerializer; +import com.checkout.issuing.cards.requests.create.VirtualCardRequest; +import com.checkout.issuing.cards.requests.update.UpdateCardRequest; +import com.checkout.issuing.cards.responses.CardResponse; +import com.checkout.issuing.cards.responses.VirtualCardDetailsResponse; +import com.checkout.issuing.cards.responses.activate.ActivateCardResponse; +import com.checkout.issuing.cards.responses.update.UpdateCardResponse; +import org.junit.jupiter.api.Test; + +import java.time.Instant; +import java.time.LocalDate; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * GSON serialization tests for the 2026-09-17 issuing card changes (INT-1700): + * {@code scheduled_revocation_date}, {@code last_activated_on}, and the {@code status} + * field on update-card-request. + */ +class CardScheduledRevocationAndActivationSerializationTest { + + private final GsonSerializer serializer = new GsonSerializer(); + + @Test + void shouldSerializeScheduledRevocationDateOnAddCardRequest() { + final VirtualCardRequest request = VirtualCardRequest.builder() + .cardholderId("crh_test") + .scheduledRevocationDate(LocalDate.of(2027, 3, 12)) + .revocationDate(LocalDate.of(2027, 1, 1)) + .build(); + + final String json = serializer.toJson(request); + + assertTrue(json.contains("\"scheduled_revocation_date\":\"2027-03-12\"")); + assertTrue(json.contains("\"revocation_date\":\"2027-01-01\"")); + } + + @Test + void shouldSerializeStatusAndScheduledRevocationDateOnUpdateCardRequest() { + final UpdateCardRequest request = UpdateCardRequest.builder() + .status(CardStatus.ACTIVE) + .scheduledRevocationDate(LocalDate.of(2027, 3, 12)) + .build(); + + final String json = serializer.toJson(request); + + assertTrue(json.contains("\"status\":\"active\"")); + assertTrue(json.contains("\"scheduled_revocation_date\":\"2027-03-12\"")); + } + + @Test + void shouldRoundTripUpdateCardRequestWithStatusAndScheduledRevocationDate() { + final UpdateCardRequest original = UpdateCardRequest.builder() + .status(CardStatus.ACTIVE) + .scheduledRevocationDate(LocalDate.of(2027, 3, 12)) + .build(); + + final UpdateCardRequest deserialized = + serializer.fromJson(serializer.toJson(original), UpdateCardRequest.class); + + assertEquals(original.getStatus(), deserialized.getStatus()); + assertEquals(original.getScheduledRevocationDate(), deserialized.getScheduledRevocationDate()); + } + + @Test + void shouldDeserializeScheduledRevocationDateAndLastActivatedOnOnGetCardResponse() { + final String json = "{\"type\":\"virtual\",\"id\":\"crd_test\",\"entity_id\":\"ent_test\"," + + "\"scheduled_revocation_date\":\"2027-03-12\"," + + "\"last_activated_on\":\"2019-09-10T10:11:12Z\"}"; + + final VirtualCardDetailsResponse response = + serializer.fromJson(json, VirtualCardDetailsResponse.class); + + assertNotNull(response); + assertEquals(LocalDate.of(2027, 3, 12), response.getScheduledRevocationDate()); + assertEquals(Instant.parse("2019-09-10T10:11:12Z"), response.getLastActivatedOn()); + } + + @Test + void shouldDeserializeNullLastActivatedOnOnGetCardResponseWhenCardNeverActivated() { + final String json = "{\"type\":\"virtual\",\"id\":\"crd_test\",\"last_activated_on\":null}"; + + final VirtualCardDetailsResponse response = + serializer.fromJson(json, VirtualCardDetailsResponse.class); + + assertNotNull(response); + assertNull(response.getLastActivatedOn()); + } + + @Test + void shouldDeserializeLastActivatedOnOnAddCardResponse() { + final String json = "{\"id\":\"crd_test\",\"last_activated_on\":\"2019-09-10T10:11:12Z\"}"; + + final CardResponse response = serializer.fromJson(json, CardResponse.class); + + assertNotNull(response); + assertEquals(Instant.parse("2019-09-10T10:11:12Z"), response.getLastActivatedOn()); + } + + @Test + void shouldDeserializeActivateCardResponseSwaggerExample() { + final String json = "{\"last_activated_on\":\"2019-09-10T10:11:12Z\"}"; + + final ActivateCardResponse response = serializer.fromJson(json, ActivateCardResponse.class); + + assertNotNull(response); + assertEquals(Instant.parse("2019-09-10T10:11:12Z"), response.getLastActivatedOn()); + } + + @Test + void shouldDeserializeUpdateCardResponseWithFullGetCardResponseFieldsAndNoEncryptedCvv() { + final String json = "{\"id\":\"crd_test\",\"cardholder_id\":\"crh_test\",\"client_id\":\"cli_test\"," + + "\"entity_id\":\"ent_test\",\"last_four\":\"4321\",\"expiry_month\":12,\"expiry_year\":2030," + + "\"status\":\"active\",\"type\":\"virtual\",\"billing_currency\":\"USD\"," + + "\"issuing_country\":\"US\",\"scheme\":\"visa\",\"scheduled_revocation_date\":\"2027-03-12\"," + + "\"last_activated_on\":\"2019-09-10T10:11:12Z\"," + + "\"last_modified_date\":\"2026-09-17T00:00:00Z\",\"encrypted_cvv\":\"should-be-ignored\"}"; + + final UpdateCardResponse response = serializer.fromJson(json, UpdateCardResponse.class); + + assertNotNull(response); + assertEquals("crd_test", response.getId()); + assertEquals(CardStatus.ACTIVE, response.getStatus()); + assertEquals(LocalDate.of(2027, 3, 12), response.getScheduledRevocationDate()); + assertEquals(Instant.parse("2019-09-10T10:11:12Z"), response.getLastActivatedOn()); + assertNotNull(response.getLastModifiedDate()); + assertFalse(serializer.toJson(response).contains("encrypted_cvv")); + } + + // Verifies the 2026-09-23 spec update: update-card-response gained a virtual/physical + // discriminator, and the virtual variant adds is_single_use. + @Test + void shouldDeserializeIsSingleUseOnVirtualCardUpdateResponse() { + final String json = "{\"type\":\"virtual\",\"last_modified_date\":\"2019-09-10T10:11:12Z\"," + + "\"is_single_use\":true}"; + + final UpdateCardResponse response = serializer.fromJson(json, UpdateCardResponse.class); + + assertNotNull(response); + assertEquals(Boolean.TRUE, response.getIsSingleUse()); + } +} diff --git a/src/test/java/com/checkout/issuing/cards/CardUpdateResponseSerializationTest.java b/src/test/java/com/checkout/issuing/cards/CardUpdateResponseSerializationTest.java index b11157ec..beb6e818 100644 --- a/src/test/java/com/checkout/issuing/cards/CardUpdateResponseSerializationTest.java +++ b/src/test/java/com/checkout/issuing/cards/CardUpdateResponseSerializationTest.java @@ -1,83 +1,23 @@ package com.checkout.issuing.cards; -import com.checkout.GsonSerializer; import com.checkout.issuing.cards.requests.update.CardUpdateHeaders; -import com.checkout.issuing.cards.responses.update.UpdateCardResponse; import org.junit.jupiter.api.Test; import java.util.Map; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; /** - * GSON serialization tests for UpdateCardResponse and CardUpdateHeaders. + * Tests for CardUpdateHeaders. * * Swagger reference: PATCH /issuing/cards/{cardId} - * Schema: update-card-response. Only last_modified_date is required; encrypted_cvv is returned - * solely when the return-encrypted-cvv header is set to true. + * Schema: update-card-response. The 2026-09-02 delta added encrypted_cvv here; the 2026-09-17 + * delta (INT-1700) removed it again, so the current spec never includes it. Coverage for the + * current update-card-response field set lives in CardScheduledRevocationAndActivationSerializationTest. */ class CardUpdateResponseSerializationTest { - private final GsonSerializer serializer = new GsonSerializer(); - - @Test - void shouldDeserializeWithEncryptedCvv() { - final String json = "{" - + "\"last_modified_date\":\"2026-06-01T10:00:00Z\"," - + "\"encrypted_cvv\":\"ZW5jcnlwdGVkLWN2dg==\"," - + "\"_links\":{\"self\":{\"href\":\"https://api.checkout.com/issuing/cards/crd_test\"}}" - + "}"; - - final UpdateCardResponse response = serializer.fromJson(json, UpdateCardResponse.class); - - assertNotNull(response); - assertNotNull(response.getLastModifiedDate()); - assertEquals("ZW5jcnlwdGVkLWN2dg==", response.getEncryptedCvv()); - } - - /** - * encrypted_cvv is optional: a plain update that did not opt in must leave it null. - */ - @Test - void shouldDeserializeWithoutEncryptedCvv() { - final UpdateCardResponse response = serializer.fromJson( - "{\"last_modified_date\":\"2026-06-01T10:00:00Z\"}", UpdateCardResponse.class); - - assertNotNull(response); - assertNotNull(response.getLastModifiedDate()); - assertNull(response.getEncryptedCvv()); - } - - /** - * Gson HTML-escapes by default, so a Base64 value containing '=' is emitted as \\u003d. The key - * name is what this test pins; the exact value is verified by the round trip below. - */ - @Test - void shouldSerializeEncryptedCvvUsingTheSwaggerKey() { - final UpdateCardResponse response = new UpdateCardResponse(); - response.setEncryptedCvv("ZW5jcnlwdGVkLWN2dg=="); - - final String json = serializer.toJson(response); - - assertTrue(json.contains("\"encrypted_cvv\"")); - assertFalse(json.contains("\"encryptedCvv\"")); - } - - @Test - void shouldRoundTripEncryptedCvvExactly() { - final UpdateCardResponse original = new UpdateCardResponse(); - original.setEncryptedCvv("ZW5jcnlwdGVkLWN2dg=="); - - final UpdateCardResponse deserialized = - serializer.fromJson(serializer.toJson(original), UpdateCardResponse.class); - - assertEquals("ZW5jcnlwdGVkLWN2dg==", deserialized.getEncryptedCvv()); - } - @Test void shouldExposeTheUpdateHeadersUsingTheExactSwaggerHeaderNames() { final CardUpdateHeaders headers = CardUpdateHeaders.builder()