diff --git a/src/main/java/com/checkout/GsonSerializer.java b/src/main/java/com/checkout/GsonSerializer.java index 40a90956..3fb8c0b6 100644 --- a/src/main/java/com/checkout/GsonSerializer.java +++ b/src/main/java/com/checkout/GsonSerializer.java @@ -18,6 +18,8 @@ import com.checkout.issuing.controls.responses.create.MccCardControlResponse; import com.checkout.issuing.controls.responses.create.VelocityCardControlResponse; import com.checkout.payments.PaymentDestinationType; +import com.checkout.payments.Product; +import com.checkout.payments.ProductType; import com.checkout.payments.previous.PaymentAction; import com.checkout.payments.sender.Sender; import com.checkout.payments.sender.SenderType; @@ -36,8 +38,10 @@ import com.google.gson.JsonPrimitive; import com.google.gson.JsonSerializationContext; import com.google.gson.JsonSerializer; +import com.google.gson.annotations.SerializedName; import com.google.gson.reflect.TypeToken; import com.google.gson.typeadapters.RuntimeTypeAdapterFactory; +import lombok.Getter; import org.apache.commons.lang3.EnumUtils; import java.lang.reflect.Type; @@ -55,6 +59,7 @@ import java.util.stream.Collectors; import java.util.stream.IntStream; +@Getter public class GsonSerializer implements Serializer { private static final List DEFAULT_FORMATTERS = Arrays.asList( @@ -149,6 +154,7 @@ public class GsonSerializer implements Serializer { .registerTypeAdapter(WEBHOOKS_TYPE, webhooksResponseDeserializer()) .registerTypeAdapter(PREVIOUS_PAYMENT_ACTIONS_TYPE, paymentActionsResponsePreviousDeserializer()) .registerTypeAdapter(PAYMENT_ACTIONS_TYPE, paymentActionsResponseDeserializer()) + .registerTypeAdapter(Product.class, getProductDeserializer()) .create(); private final Gson gson; @@ -161,10 +167,6 @@ public GsonSerializer(final Gson gson) { this.gson = gson; } - public Gson getGson() { - return gson; - } - @Override public String toJson(final T object) { return gson.toJson(object); @@ -276,18 +278,16 @@ private static JsonDeserializer getInstantJsonDeserializer() { return (json, typeOfT, context) -> { String dateString; - // Check if the JSON is a number or a string if (json.isJsonPrimitive() && json.getAsJsonPrimitive().isNumber()) { - dateString = String.valueOf(json.getAsLong()); // Convert numeric value to string + dateString = String.valueOf(json.getAsLong()); } else { - dateString = json.getAsString(); // Use the string value directly + dateString = json.getAsString(); } try { - // Try parsing the string as an ISO-8601 Instant return Instant.parse(dateString); } catch (final DateTimeParseException ex) { - if (dateString.matches("\\d{8}")) { // Handle numeric format yyyyMMdd + if (dateString.matches("\\d{8}")) { try { DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd"); LocalDateTime dateTime = LocalDate.parse(dateString, formatter).atStartOfDay(); @@ -296,8 +296,7 @@ private static JsonDeserializer getInstantJsonDeserializer() { throw new JsonParseException("Failed to parse numeric date in format yyyyMMdd: " + dateString, e); } } - // Explicitly handle the yyyy-MM-dd format - if (dateString.length() == 10) { // Handle format yyyy-MM-dd + if (dateString.length() == 10) { try { DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); LocalDate date = LocalDate.parse(dateString, formatter); @@ -306,18 +305,79 @@ private static JsonDeserializer getInstantJsonDeserializer() { throw new JsonParseException("Failed to parse date in format yyyy-MM-dd: " + dateString, e); } } - // Attempt parsing with the DEFAULT_FORMATTERS for (final DateTimeFormatter formatter : DEFAULT_FORMATTERS) { try { final LocalDateTime dateTime = LocalDateTime.parse(dateString, formatter); return dateTime.toInstant(ZoneOffset.UTC); } catch (final DateTimeParseException ignored) { - // Continue with the next formatter } } - // Rethrow the original exception if no format matches throw ex; } }; } + + private static JsonDeserializer getProductDeserializer() { + return (json, typeOfT, context) -> { + Product product = new Product(); + JsonObject jsonObject = json.getAsJsonObject(); + + JsonElement typeElement = jsonObject.get("type"); + Object typeValue = null; + + if (typeElement != null && typeElement.isJsonPrimitive()) { + String typeAsString = typeElement.getAsString(); + if (EnumUtils.isValidEnumIgnoreCase(ProductType.class, typeAsString)) { + typeValue = ProductType.valueOf(typeAsString.toUpperCase()); + } else { + typeValue = typeAsString; + } + } + product.setType(typeValue); + + + jsonObject.entrySet().stream() + .filter(entry -> !entry.getKey().equals("type")) + .forEach(entry -> { + try { + String jsonKey = entry.getKey(); + JsonElement jsonValue = entry.getValue(); + + java.lang.reflect.Field field = Arrays.stream(Product.class.getDeclaredFields()) + .filter(f -> { + SerializedName annotation = f.getAnnotation(SerializedName.class); + return (annotation != null && annotation.value().equals(jsonKey)) || f.getName().equals(jsonKey); + }) + .findFirst() + .orElse(null); + + if (field != null) { + field.setAccessible(true); + Class fieldType = field.getType(); + + if (jsonValue.isJsonNull()) { + field.set(product, null); + } else if (fieldType.equals(String.class)) { + field.set(product, jsonValue.getAsString()); + } else if (fieldType.equals(Long.class) || fieldType.equals(long.class)) { + field.set(product, jsonValue.getAsLong()); + } else if (fieldType.equals(Instant.class)) { + String dateString = jsonValue.getAsString(); + if (dateString.matches("\\d{4}-\\d{2}-\\d{2}")) { + dateString += "T00:00:00Z"; + } + field.set(product, Instant.parse(dateString)); + } else { + Object nestedObject = context.deserialize(jsonValue, fieldType); + field.set(product, nestedObject); + } + } + } catch (IllegalAccessException e) { + System.err.println("Error setting field: " + entry.getKey() + ", " + e.getMessage()); + } + }); + + return product; + }; + } } diff --git a/src/main/java/com/checkout/payments/Product.java b/src/main/java/com/checkout/payments/Product.java index 967dc27a..89c07c3c 100644 --- a/src/main/java/com/checkout/payments/Product.java +++ b/src/main/java/com/checkout/payments/Product.java @@ -4,15 +4,17 @@ import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; import java.time.Instant; @Data @Builder @AllArgsConstructor +@NoArgsConstructor public final class Product { - private ProductType type; + private Object type; private String name; @@ -54,4 +56,12 @@ public final class Product { @SerializedName("service_ends_on") private Instant serviceEndsOn; + public ProductType getTypeAsEnum() { + return type instanceof ProductType ? (ProductType) type : null; + } + + public String getTypeAsString() { + return type instanceof String ? (String) type : null; + } + } diff --git a/src/test/java/com/checkout/GsonSerializerTest.java b/src/test/java/com/checkout/GsonSerializerTest.java index 86230b2f..3bfd2bee 100644 --- a/src/test/java/com/checkout/GsonSerializerTest.java +++ b/src/test/java/com/checkout/GsonSerializerTest.java @@ -5,6 +5,8 @@ import com.checkout.issuing.cardholders.CardholderCardsResponse; import com.checkout.issuing.cards.responses.PhysicalCardDetailsResponse; import com.checkout.issuing.cards.responses.VirtualCardDetailsResponse; +import com.checkout.payments.Product; +import com.checkout.payments.ProductType; import com.checkout.payments.contexts.PaymentContextDetailsResponse; import com.checkout.payments.previous.response.GetPaymentResponse; import com.checkout.payments.previous.response.PaymentResponse; @@ -171,6 +173,38 @@ void shouldSerializePaymentDetailsResponseFromJson() { assertNotNull(paymentDetailsResponse.getPaymentPlan()); } + @Test + void shouldDeserializeProductWithEnumType() { + String json = "{ \"type\": \"DIGITAL\", \"name\": \"Product Name\" }"; + + Product product = serializer.fromJson(json, Product.class); + + assertNotNull(product); + assertEquals(ProductType.DIGITAL, product.getTypeAsEnum()); + assertNull(product.getTypeAsString()); + } + + @Test + void shouldDeserializeProductWithUnknownEnumValue() { + String json = "{ \"type\": \"UNKNOWN_VALUE\", \"name\": \"Product Name\", \"quantity\": 1, \"unit_price\": 1000 }"; + + Product product = serializer.fromJson(json, Product.class); + + assertNotNull(product); + assertEquals("UNKNOWN_VALUE", product.getTypeAsString()); + assertNull(product.getTypeAsEnum()); + } + + @Test + void shouldDeserializeProductWithNullType() { + String json = "{ \"type\": null, \"name\": \"Product Name\" }"; + + Product product = serializer.fromJson(json, Product.class); + + assertNotNull(product); + assertNull(product.getType()); + } + @Test void shouldDeserializeMultipleDateFormats() { Instant instant = Instant.parse("2021-06-08T00:00:00Z"); diff --git a/src/test/java/com/checkout/payments/GetPaymentsTestIT.java b/src/test/java/com/checkout/payments/GetPaymentsTestIT.java index 7fcc8204..81e17ba4 100644 --- a/src/test/java/com/checkout/payments/GetPaymentsTestIT.java +++ b/src/test/java/com/checkout/payments/GetPaymentsTestIT.java @@ -12,6 +12,8 @@ import com.checkout.payments.sender.PaymentIndividualSender; import org.junit.jupiter.api.Test; +import java.util.Arrays; +import java.util.Collections; import java.util.UUID; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @@ -21,6 +23,7 @@ import static com.checkout.CardSourceHelper.getRequestCardSource; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -48,6 +51,129 @@ void shouldGetCardPayment() { } + @Test + void shouldGetPaymentWithItemsUsingEnumType() { + + Product product = Product.builder() + .type(ProductType.DIGITAL) + .name("Test Product") + .quantity(1L) + .unitPrice(1000L) + .build(); + + PaymentRequest request = PaymentRequest.builder() + .source(getRequestCardSource()) + .reference(UUID.randomUUID().toString()) + .amount(1000L) + .currency(Currency.EUR) + .processingChannelId(System.getenv("CHECKOUT_PROCESSING_CHANNEL_ID")) + .items(Collections.singletonList(product)) + .build(); + + PaymentResponse payment = blocking(() -> paymentsClient.requestPayment(request)); + + GetPaymentResponse paymentReturned = blocking(() -> paymentsClient.getPayment(payment.getId())); + + assertNotNull(paymentReturned); + assertNotNull(paymentReturned.getItems()); + assertEquals(1, paymentReturned.getItems().size()); + + Product returnedProduct = paymentReturned.getItems().get(0); + assertEquals(ProductType.DIGITAL, returnedProduct.getTypeAsEnum()); + assertEquals(ProductType.DIGITAL, returnedProduct.getType()); + assertNull(returnedProduct.getTypeAsString()); + assertEquals("Test Product", returnedProduct.getName()); + assertEquals(1L, returnedProduct.getQuantity()); + assertEquals(1000L, returnedProduct.getUnitPrice()); + } + + @Test + void shouldGetPaymentWithItemsUsingStringType() { + + Product product = Product.builder() + .type("CustomType") + .name("Custom Product") + .quantity(2L) + .unitPrice(2000L) + .build(); + + PaymentRequest request = PaymentRequest.builder() + .source(getRequestCardSource()) + .reference(UUID.randomUUID().toString()) + .amount(2000L) + .currency(Currency.EUR) + .processingChannelId(System.getenv("CHECKOUT_PROCESSING_CHANNEL_ID")) + .items(Collections.singletonList(product)) + .build(); + + PaymentResponse payment = blocking(() -> paymentsClient.requestPayment(request)); + + GetPaymentResponse paymentReturned = blocking(() -> paymentsClient.getPayment(payment.getId())); + + assertNotNull(paymentReturned); + assertNotNull(paymentReturned.getItems()); + assertEquals(1, paymentReturned.getItems().size()); + + Product returnedProduct = paymentReturned.getItems().get(0); + assertEquals("CustomType", returnedProduct.getTypeAsString()); + assertEquals("CustomType", returnedProduct.getType()); + assertNull(returnedProduct.getTypeAsEnum()); + assertEquals("Custom Product", returnedProduct.getName()); + assertEquals(2L, returnedProduct.getQuantity()); + assertEquals(2000L, returnedProduct.getUnitPrice()); + } + + @Test + void shouldGetPaymentWithMultipleItems() { + + Product enumProduct = Product.builder() + .type(ProductType.PHYSICAL) + .name("Physical Product") + .quantity(1L) + .unitPrice(1500L) + .build(); + + Product stringProduct = Product.builder() + .type("CustomType") + .name("Custom Product") + .quantity(2L) + .unitPrice(3000L) + .build(); + + PaymentRequest request = PaymentRequest.builder() + .source(getRequestCardSource()) + .reference(UUID.randomUUID().toString()) + .amount(4500L) + .currency(Currency.EUR) + .processingChannelId(System.getenv("CHECKOUT_PROCESSING_CHANNEL_ID")) + .items(Arrays.asList(enumProduct, stringProduct)) + .build(); + + PaymentResponse payment = blocking(() -> paymentsClient.requestPayment(request)); + + GetPaymentResponse paymentReturned = blocking(() -> paymentsClient.getPayment(payment.getId())); + + assertNotNull(paymentReturned); + assertNotNull(paymentReturned.getItems()); + assertEquals(2, paymentReturned.getItems().size()); + + Product returnedEnumProduct = paymentReturned.getItems().get(0); + assertEquals(ProductType.PHYSICAL, returnedEnumProduct.getTypeAsEnum()); + assertEquals(ProductType.PHYSICAL, returnedEnumProduct.getType()); + assertNull(returnedEnumProduct.getTypeAsString()); + assertEquals("Physical Product", returnedEnumProduct.getName()); + assertEquals(1L, returnedEnumProduct.getQuantity()); + assertEquals(1500L, returnedEnumProduct.getUnitPrice()); + + Product returnedStringProduct = paymentReturned.getItems().get(1); + assertEquals("CustomType", returnedStringProduct.getTypeAsString()); + assertEquals("CustomType", returnedStringProduct.getType()); + assertNull(returnedStringProduct.getTypeAsEnum()); + assertEquals("Custom Product", returnedStringProduct.getName()); + assertEquals(2L, returnedStringProduct.getQuantity()); + assertEquals(3000L, returnedStringProduct.getUnitPrice()); + } + @Test void shouldGetCardPaymentWithMetadata() {