Skip to content

Commit

Permalink
Update Product.type to be a string value or enum (#463)
Browse files Browse the repository at this point in the history
  • Loading branch information
armando-rodriguez-cko authored Jan 17, 2025
1 parent bd5401c commit 0fed925
Show file tree
Hide file tree
Showing 4 changed files with 245 additions and 15 deletions.
88 changes: 74 additions & 14 deletions src/main/java/com/checkout/GsonSerializer.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -55,6 +59,7 @@
import java.util.stream.Collectors;
import java.util.stream.IntStream;

@Getter
public class GsonSerializer implements Serializer {

private static final List<DateTimeFormatter> DEFAULT_FORMATTERS = Arrays.asList(
Expand Down Expand Up @@ -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;
Expand All @@ -161,10 +167,6 @@ public GsonSerializer(final Gson gson) {
this.gson = gson;
}

public Gson getGson() {
return gson;
}

@Override
public <T> String toJson(final T object) {
return gson.toJson(object);
Expand Down Expand Up @@ -276,18 +278,16 @@ private static JsonDeserializer<Instant> 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();
Expand All @@ -296,8 +296,7 @@ private static JsonDeserializer<Instant> 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);
Expand All @@ -306,18 +305,79 @@ private static JsonDeserializer<Instant> 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<Product> 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;
};
}
}
12 changes: 11 additions & 1 deletion src/main/java/com/checkout/payments/Product.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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;
}

}
34 changes: 34 additions & 0 deletions src/test/java/com/checkout/GsonSerializerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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");
Expand Down
126 changes: 126 additions & 0 deletions src/test/java/com/checkout/payments/GetPaymentsTestIT.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

Expand Down Expand Up @@ -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() {

Expand Down

0 comments on commit 0fed925

Please sign in to comment.