From 56238b2cc6e706a5e7512d798b6b2e2790813f1e 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, 16 Jan 2025 13:50:45 +0100
Subject: [PATCH] Update Product.type to be a string value or enum

---
 .../java/com/checkout/GsonSerializer.java     |  26 ++++
 .../java/com/checkout/payments/Product.java   |  10 +-
 .../java/com/checkout/GsonSerializerTest.java |  34 +++++
 .../checkout/payments/GetPaymentsTestIT.java  | 122 ++++++++++++++++++
 4 files changed, 191 insertions(+), 1 deletion(-)

diff --git a/src/main/java/com/checkout/GsonSerializer.java b/src/main/java/com/checkout/GsonSerializer.java
index 40a90956..3990aeaf 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;
@@ -149,6 +151,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;
@@ -320,4 +323,27 @@ private static JsonDeserializer<Instant> getInstantJsonDeserializer() {
             }
         };
     }
+
+    private static JsonDeserializer<Product> getProductDeserializer() {
+        return (json, typeOfT, context) -> {
+            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 product = new Gson().fromJson(jsonObject, Product.class);
+            product.setType(typeValue);
+
+            return product;
+        };
+    }
 }
diff --git a/src/main/java/com/checkout/payments/Product.java b/src/main/java/com/checkout/payments/Product.java
index 967dc27a..9aedd555 100644
--- a/src/main/java/com/checkout/payments/Product.java
+++ b/src/main/java/com/checkout/payments/Product.java
@@ -12,7 +12,7 @@
 @AllArgsConstructor
 public final class Product {
 
-    private ProductType type;
+    private Object type;
 
     private String name;
 
@@ -54,4 +54,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..017f430d 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,125 @@ 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());
+        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());
+        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());
+        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());
+        assertNull(returnedStringProduct.getTypeAsEnum());
+        assertEquals("Custom Product", returnedStringProduct.getName());
+        assertEquals(2L, returnedStringProduct.getQuantity());
+        assertEquals(3000L, returnedStringProduct.getUnitPrice());
+    }
+
     @Test
     void shouldGetCardPaymentWithMetadata() {