diff --git a/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/customer/MerchantAccount.java b/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/customer/MerchantAccount.java index c2811a84..b88e9cf8 100644 --- a/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/customer/MerchantAccount.java +++ b/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/customer/MerchantAccount.java @@ -6,7 +6,7 @@ import lombok.Data; import lombok.NoArgsConstructor; -import java.time.Instant; +import java.time.LocalDate; /** * Merchant account information @@ -18,48 +18,60 @@ public final class MerchantAccount { /** - * The unique identifier of the merchant account + * The unique identifier of the merchant account. + * [Optional] */ private String id; /** - * The date when the customer's account was first registered with the merchant + * The date the customer registered their account with the merchant. + * [Optional] + * Format: yyyy-MM-dd */ @SerializedName("registration_date") - private Instant registrationDate; + private LocalDate registrationDate; /** - * The date when the customer's account was last modified + * The date the customer's account with the merchant was last modified. + * [Optional] + * Format: yyyy-MM-dd */ @SerializedName("last_modified") - private Instant lastModified; + private LocalDate lastModified; /** - * Indicates whether this is a returning customer + * Indicates whether this is a returning customer. + * [Optional] */ @SerializedName("returning_customer") private Boolean returningCustomer; /** - * The date of the customer's first transaction with the merchant + * The date of the customer's first transaction with the merchant. + * [Optional] + * Format: yyyy-MM-dd */ @SerializedName("first_transaction_date") - private Instant firstTransactionDate; + private LocalDate firstTransactionDate; /** - * The date of the customer's most recent transaction with the merchant + * The date of the customer's most recent transaction with the merchant. + * [Optional] + * Format: yyyy-MM-dd */ @SerializedName("last_transaction_date") - private Instant lastTransactionDate; + private LocalDate lastTransactionDate; /** - * The total number of orders the customer has placed with the merchant + * The total number of orders the customer has placed with the merchant. + * [Optional] */ @SerializedName("total_order_count") private Integer totalOrderCount; /** - * The amount of the customer's last payment with the merchant + * The amount of the customer's last payment with the merchant. + * [Optional] */ @SerializedName("last_payment_amount") private Long lastPaymentAmount; diff --git a/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/order/OrderSubMerchant.java b/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/order/OrderSubMerchant.java index a949f8df..e5fe5bb6 100644 --- a/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/order/OrderSubMerchant.java +++ b/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/order/OrderSubMerchant.java @@ -6,7 +6,7 @@ import lombok.Data; import lombok.NoArgsConstructor; -import java.time.Instant; +import java.time.LocalDate; /** * Sub-merchant information for order @@ -35,8 +35,10 @@ public final class OrderSubMerchant { private Integer numberOfTrades; /** - * The date when the sub-merchant was registered in YYYY-MM-DD format + * The date the sub-merchant was registered. + * [Optional] + * Format: yyyy-MM-dd */ @SerializedName("registration_date") - private Instant registrationDate; + private LocalDate registrationDate; } \ No newline at end of file diff --git a/src/main/java/com/checkout/instruments/create/InstrumentData.java b/src/main/java/com/checkout/instruments/create/InstrumentData.java index 3c21d9f5..fdca4aa6 100644 --- a/src/main/java/com/checkout/instruments/create/InstrumentData.java +++ b/src/main/java/com/checkout/instruments/create/InstrumentData.java @@ -9,7 +9,7 @@ import lombok.Data; import lombok.NoArgsConstructor; -import java.time.Instant; +import java.time.LocalDate; @Data @Builder @@ -17,20 +17,45 @@ @NoArgsConstructor public final class InstrumentData { + /** + * The SEPA account number. + * [Optional] + */ @SerializedName("account_number") private String accoountNumber; + /** + * The country of the SEPA account. + * [Optional] + */ private CountryCode country; + /** + * The currency of the SEPA account. + * [Optional] + */ private Currency currency; + /** + * The payment type for this instrument. + * [Optional] + */ @SerializedName("payment_type") private PaymentType paymentType; + /** + * The unique identifier of the SEPA mandate. + * [Optional] + */ @SerializedName("mandate_id") private String mandateId; + /** + * The date the mandate was signed. + * [Optional] + * Format: yyyy-MM-dd + */ @SerializedName("date_of_signature") - private Instant dateOfSignature; + private LocalDate dateOfSignature; } diff --git a/src/main/java/com/checkout/payments/ProductRequest.java b/src/main/java/com/checkout/payments/ProductRequest.java index 29c9ceb8..2b2dbc88 100644 --- a/src/main/java/com/checkout/payments/ProductRequest.java +++ b/src/main/java/com/checkout/payments/ProductRequest.java @@ -9,7 +9,7 @@ import lombok.Data; import lombok.NoArgsConstructor; -import java.time.Instant; +import java.time.LocalDate; @Data @Builder @@ -17,54 +17,131 @@ @NoArgsConstructor public final class ProductRequest { + /** + * The item type. + * [Optional] + */ private ItemType type; + /** + * The item sub-type. + * [Optional] + */ @SerializedName("sub_type") private ItemSubType subType; + /** + * The item name. + * [Optional] + */ private String name; + /** + * The number of items. + * [Optional] + */ private Long quantity; + /** + * The price of the item. + * [Optional] + */ @SerializedName("unit_price") private Long unitPrice; + /** + * A reference for the item. + * [Optional] + */ private String reference; + /** + * The commodity code. + * [Optional] + */ @SerializedName("commodity_code") private String commodityCode; + /** + * The unit of measure. + * [Optional] + */ @SerializedName("unit_of_measure") private String unitOfMeasure; + /** + * The total amount of the item. + * [Optional] + */ @SerializedName("total_amount") private Long totalAmount; + /** + * The tax rate applied to the item. + * [Optional] + */ @SerializedName("tax_rate") private Long taxRate; + /** + * The amount of tax applied to the item. + * [Optional] + */ @SerializedName("tax_amount") private Long taxAmount; + /** + * The amount discounted from the item. + * [Optional] + */ @SerializedName("discount_amount") private Long discountAmount; + /** + * The WeChat Pay goods ID. + * [Optional] + */ @SerializedName("wxpay_goods_id") private String wxpayGoodsId; + /** + * The URL of the product image. + * [Optional] + */ @SerializedName("image_url") private String imageUrl; + /** + * The URL of the product page. + * [Optional] + */ private String url; + /** + * The stock keeping unit. + * [Optional] + */ private String sku; + /** + * Maximum date for the service to be rendered or ended. + * [Optional] + * Format: yyyy-MM-dd + */ @SerializedName("service_ends_on") - private Instant serviceEndsOn; + private LocalDate serviceEndsOn; + /** + * The country in which the purchase was made. + * [Optional] + */ @SerializedName("purchase_country") private CountryCode purchaseCountry; + /** + * The amount in a foreign retailer's local currency. + * [Optional] + */ @SerializedName("foreign_retailer_amount") private Long foreignRetailerAmount; diff --git a/src/main/java/com/checkout/payments/ProductResponse.java b/src/main/java/com/checkout/payments/ProductResponse.java index 8c79b947..5156ed4f 100644 --- a/src/main/java/com/checkout/payments/ProductResponse.java +++ b/src/main/java/com/checkout/payments/ProductResponse.java @@ -6,7 +6,7 @@ import lombok.Data; import lombok.NoArgsConstructor; -import java.time.Instant; +import java.time.LocalDate; @Data @Builder @@ -54,8 +54,13 @@ public final class ProductResponse { private String sku; + /** + * Maximum date for the service to be rendered or ended. + * [Optional] + * Format: yyyy-MM-dd + */ @SerializedName("service_ends_on") - private Instant serviceEndsOn; + private LocalDate serviceEndsOn; public ProductType getTypeAsEnum() { return type instanceof ProductType ? (ProductType) type : null; diff --git a/src/main/java/com/checkout/payments/contexts/PaymentContextsAccommodationData.java b/src/main/java/com/checkout/payments/contexts/PaymentContextsAccommodationData.java index 0c48f5c9..63390f11 100644 --- a/src/main/java/com/checkout/payments/contexts/PaymentContextsAccommodationData.java +++ b/src/main/java/com/checkout/payments/contexts/PaymentContextsAccommodationData.java @@ -8,7 +8,7 @@ import lombok.Data; import lombok.NoArgsConstructor; -import java.time.Instant; +import java.time.LocalDate; import java.util.List; @Data @@ -17,31 +17,77 @@ @AllArgsConstructor public final class PaymentContextsAccommodationData { + /** + * The name of the accommodation. + * [Optional] + */ private String name; + /** + * The booking reference. + * [Optional] + */ @SerializedName("booking_reference") private String bookingReference; + /** + * The actual or scheduled check-in date. For cruise: the cruise departure (sail) date. + * [Optional] + * Format: yyyy-MM-dd + */ @SerializedName("check_in_date") - private Instant checkInDate; + private LocalDate checkInDate; + /** + * The actual or scheduled check-out date. For cruise: the cruise return (sail end) date. + * [Optional] + * Format: yyyy-MM-dd + */ @SerializedName("check_out_date") - private Instant checkOutDate; + private LocalDate checkOutDate; + /** + * The address of the accommodation. + * [Optional] + */ private Address address; + /** + * The state of the accommodation. + * [Optional] + */ private CountryCode state; + /** + * The country of the accommodation. + * [Optional] + */ private CountryCode country; + /** + * The city of the accommodation. + * [Optional] + */ private String city; + /** + * The number of rooms booked. + * [Optional] + */ @SerializedName("number_of_rooms") private Integer numberOfRooms; + /** + * Information about the guests staying at the accommodation. + * [Optional] + */ @SerializedName("guests") private List guests; + /** + * Information about the rooms booked by the customer. + * [Optional] + */ @SerializedName("room") private List room; } diff --git a/src/main/java/com/checkout/payments/contexts/PaymentContextsCustomerSummary.java b/src/main/java/com/checkout/payments/contexts/PaymentContextsCustomerSummary.java index 61202736..17943096 100644 --- a/src/main/java/com/checkout/payments/contexts/PaymentContextsCustomerSummary.java +++ b/src/main/java/com/checkout/payments/contexts/PaymentContextsCustomerSummary.java @@ -6,7 +6,7 @@ import lombok.Data; import lombok.NoArgsConstructor; -import java.time.Instant; +import java.time.LocalDate; @Data @Builder @@ -14,18 +14,41 @@ @NoArgsConstructor public final class PaymentContextsCustomerSummary { + /** + * The date the customer registered. + * [Optional] + * Format: yyyy-MM-dd + */ @SerializedName("registration_date") - private Instant registrationDate; + private LocalDate registrationDate; + /** + * The date of the customer's first transaction. + * [Optional] + * Format: yyyy-MM-dd + */ @SerializedName("first_transaction_date") - private Instant firstTransactionDate; + private LocalDate firstTransactionDate; + /** + * The date of the customer's last payment. + * [Optional] + * Format: yyyy-MM-dd + */ @SerializedName("last_payment_date") - private Instant lastPaymentDate; + private LocalDate lastPaymentDate; + /** + * The total number of orders the customer has placed. + * [Optional] + */ @SerializedName("total_order_count") private Integer totalOrderCount; + /** + * The amount of the customer's last payment. + * [Optional] + */ @SerializedName("last_payment_amount") private Float lastPaymentAmount; diff --git a/src/main/java/com/checkout/payments/contexts/PaymentContextsFlightLegDetails.java b/src/main/java/com/checkout/payments/contexts/PaymentContextsFlightLegDetails.java index d7040620..d5a91618 100644 --- a/src/main/java/com/checkout/payments/contexts/PaymentContextsFlightLegDetails.java +++ b/src/main/java/com/checkout/payments/contexts/PaymentContextsFlightLegDetails.java @@ -6,7 +6,7 @@ import lombok.Data; import lombok.NoArgsConstructor; -import java.time.Instant; +import java.time.LocalDate; @Data @Builder @@ -14,30 +14,67 @@ @AllArgsConstructor public final class PaymentContextsFlightLegDetails { + /** + * The flight number. + * [Optional] + */ @SerializedName("flight_number") private String flightNumber; + /** + * The carrier code. + * [Optional] + */ @SerializedName("carrier_code") private String carrierCode; + /** + * The class of travelling. + * [Optional] + */ @SerializedName("class_of_travelling") private String classOfTravelling; + /** + * The departure airport. + * [Optional] + */ @SerializedName("departure_airport") private String departureAirport; + /** + * The date of the scheduled take off. + * [Optional] + * Format: yyyy-MM-dd + */ @SerializedName("departure_date") - private Instant departureDate; + private LocalDate departureDate; + /** + * The departure time. + * [Optional] + */ @SerializedName("departure_time") private String departureTime; + /** + * The arrival airport. + * [Optional] + */ @SerializedName("arrival_airport") private String arrivalAirport; + /** + * A one-letter code indicating stopover entitlement: space, O (entitled), or X (not entitled). + * [Optional] + */ @SerializedName("stop_over_code") private String stopOverCode; + /** + * The fare basis code, alphanumeric. + * [Optional] + */ @SerializedName("fare_basis_code") private String fareBasisCode; } diff --git a/src/main/java/com/checkout/payments/contexts/PaymentContextsGuests.java b/src/main/java/com/checkout/payments/contexts/PaymentContextsGuests.java index 2f5abc2e..41a349e7 100644 --- a/src/main/java/com/checkout/payments/contexts/PaymentContextsGuests.java +++ b/src/main/java/com/checkout/payments/contexts/PaymentContextsGuests.java @@ -6,7 +6,7 @@ import lombok.Data; import lombok.NoArgsConstructor; -import java.time.Instant; +import java.time.LocalDate; @Data @Builder @@ -14,12 +14,25 @@ @AllArgsConstructor public final class PaymentContextsGuests { + /** + * The first name of the guest. + * [Optional] + */ @SerializedName("first_name") private String firstName; + /** + * The last name of the guest. + * [Optional] + */ @SerializedName("last_name") private String lastName; + /** + * The date of birth of the guest. + * [Optional] + * Format: yyyy-MM-dd + */ @SerializedName("date_of_birth") - private Instant dateOfBirth; + private LocalDate dateOfBirth; } diff --git a/src/main/java/com/checkout/payments/contexts/PaymentContextsPassenger.java b/src/main/java/com/checkout/payments/contexts/PaymentContextsPassenger.java index ad3c47a0..139ce735 100644 --- a/src/main/java/com/checkout/payments/contexts/PaymentContextsPassenger.java +++ b/src/main/java/com/checkout/payments/contexts/PaymentContextsPassenger.java @@ -7,7 +7,7 @@ import lombok.Data; import lombok.NoArgsConstructor; -import java.time.Instant; +import java.time.LocalDate; @Data @Builder @@ -15,14 +15,31 @@ @AllArgsConstructor public final class PaymentContextsPassenger { + /** + * The passenger's first name. + * [Optional] + */ @SerializedName("first_name") private String firstName; + /** + * The passenger's last name. + * [Optional] + */ @SerializedName("last_name") private String lastName; + /** + * The passenger's date of birth. + * [Optional] + * Format: yyyy-MM-dd + */ @SerializedName("date_of_birth") - private Instant dateOfBirth; + private LocalDate dateOfBirth; + /** + * The passenger's address. + * [Optional] + */ private Address address; } diff --git a/src/main/java/com/checkout/payments/contexts/PaymentContextsTicket.java b/src/main/java/com/checkout/payments/contexts/PaymentContextsTicket.java index 2262ffff..1b7ea89a 100644 --- a/src/main/java/com/checkout/payments/contexts/PaymentContextsTicket.java +++ b/src/main/java/com/checkout/payments/contexts/PaymentContextsTicket.java @@ -6,7 +6,7 @@ import lombok.Data; import lombok.NoArgsConstructor; -import java.time.Instant; +import java.time.LocalDate; @Data @Builder @@ -14,20 +14,46 @@ @AllArgsConstructor public final class PaymentContextsTicket { + /** + * The ticket's unique identifier. + * [Optional] + */ private String number; + /** + * Date the airline ticket was issued. + * [Optional] + * Format: yyyy-MM-dd + */ @SerializedName("issue_date") - private Instant issueDate; + private LocalDate issueDate; + /** + * Carrier code of the ticket issuer. + * [Optional] + */ @SerializedName("issuing_carrier_code") private String issuingCarrierCode; + /** + * C = Car rental reservation, A = Airline flight reservation, + * B = Both car rental and airline flight reservations included, N = Unknown. + * [Optional] + */ @SerializedName("travel_package_indicator") private String travelPackageIndicator; + /** + * The name of the travel agency. + * [Optional] + */ @SerializedName("travel_agency_name") private String travelAgencyName; + /** + * The unique identifier from IATA or ARC for the travel agency that issues the ticket. + * [Optional] + */ @SerializedName("travel_agency_code") private String travelAgencyCode; } diff --git a/src/main/java/com/checkout/payments/sender/PaymentIndividualSender.java b/src/main/java/com/checkout/payments/sender/PaymentIndividualSender.java index 0e7a487d..13f946fa 100644 --- a/src/main/java/com/checkout/payments/sender/PaymentIndividualSender.java +++ b/src/main/java/com/checkout/payments/sender/PaymentIndividualSender.java @@ -10,40 +10,89 @@ import lombok.Setter; import lombok.ToString; +import java.time.LocalDate; + @Getter @Setter @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public final class PaymentIndividualSender extends PaymentSender { + /** + * The sender's first name. + * [Required] + */ @SerializedName("first_name") private String firstName; + /** + * The sender's middle name. + * [Optional] + */ @SerializedName("middle_name") private String middleName; + /** + * The sender's last name. + * [Required] + */ @SerializedName("last_name") private String lastName; + /** + * The sender's date of birth. + * [Optional] + * Format: yyyy-MM-dd + */ @SerializedName("dob") private String dob; + /** + * The sender's address. + * [Required] + */ private Address address; + /** + * The sender's identification details. + * [Optional] + */ private AccountHolderIdentification identification; + /** + * The reference type for the sender. + * [Optional] + */ @SerializedName("reference_type") private String referenceType; + /** + * The source of funds for the sender. + * [Optional] + */ @SerializedName("source_of_funds") private SourceOfFunds sourceOfFunds; + /** + * The sender's date of birth (yyyy-MM-dd). + * [Optional] + * Format: yyyy-MM-dd + * <= 10 characters + */ @SerializedName("date_of_birth") - private String dateOfBirth; + private LocalDate dateOfBirth; + /** + * The sender's country of birth. + * [Optional] + */ @SerializedName("country_of_birth") private CountryCode countryOfBirth; + /** + * The sender's nationality. + * [Optional] + */ private CountryCode nationality; @Builder @@ -56,7 +105,7 @@ private PaymentIndividualSender(final String reference, final AccountHolderIdentification identification, final String referenceType, final SourceOfFunds sourceOfFunds, - final String dateOfBirth, + final LocalDate dateOfBirth, final CountryCode countryOfBirth, final CountryCode nationality ) { diff --git a/src/test/java/com/checkout/payments/PayoutsTestIT.java b/src/test/java/com/checkout/payments/PayoutsTestIT.java index 98628406..846b4538 100644 --- a/src/test/java/com/checkout/payments/PayoutsTestIT.java +++ b/src/test/java/com/checkout/payments/PayoutsTestIT.java @@ -121,7 +121,7 @@ private PaymentIndividualSender createPayoutSender() { .address(createBillingAddress()) .reference("1234567ABCDEFG") .referenceType("other") - .dateOfBirth("1939-05-05") + .dateOfBirth(java.time.LocalDate.of(1939, 5, 5)) .sourceOfFunds(SourceOfFunds.CREDIT) .build(); } diff --git a/src/test/java/com/checkout/payments/contexts/PaymentContextsDateSerializationTest.java b/src/test/java/com/checkout/payments/contexts/PaymentContextsDateSerializationTest.java new file mode 100644 index 00000000..26d5cd6b --- /dev/null +++ b/src/test/java/com/checkout/payments/contexts/PaymentContextsDateSerializationTest.java @@ -0,0 +1,272 @@ +package com.checkout.payments.contexts; + +import com.checkout.GsonSerializer; +import org.junit.jupiter.api.Test; + +import java.time.LocalDate; +import java.util.Arrays; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Regression tests for date fields in payment-contexts classes. + * + *

These fields were previously typed as {@link java.time.Instant}, which caused + * {@code sender_invalid} / validation errors because the API expects {@code yyyy-MM-dd} + * (format: date) but {@code Instant} serializes as ISO-8601 datetime + * (e.g. {@code 1990-05-26T00:00:00Z}). + * + *

Affected classes fixed: {@link PaymentContextsPassenger}, + * {@link PaymentContextsGuests}, {@link PaymentContextsTicket}, + * {@link PaymentContextsFlightLegDetails}, {@link PaymentContextsAccommodationData}, + * {@link PaymentContextsCustomerSummary}. + */ +class PaymentContextsDateSerializationTest { + + private final GsonSerializer serializer = new GsonSerializer(); + + // ─── PaymentContextsPassenger ──────────────────────────────────────────── + + @Test + void passengerDateOfBirthShouldSerializeAsDateNotDatetime() { + final PaymentContextsPassenger passenger = PaymentContextsPassenger.builder() + .firstName("John") + .lastName("White") + .dateOfBirth(LocalDate.of(1990, 5, 26)) + .build(); + + final String json = serializer.toJson(passenger); + + assertTrue(json.contains("\"date_of_birth\":\"1990-05-26\""), + "Expected yyyy-MM-dd format but got: " + json); + assertFalse(json.contains("T00:00:00Z"), + "date_of_birth must NOT be an ISO-8601 datetime: " + json); + } + + @Test + void passengerDateOfBirthShouldRoundTrip() { + final PaymentContextsPassenger original = PaymentContextsPassenger.builder() + .firstName("John") + .lastName("White") + .dateOfBirth(LocalDate.of(1990, 5, 26)) + .build(); + + final String json = serializer.toJson(original); + final PaymentContextsPassenger deserialized = serializer.fromJson(json, PaymentContextsPassenger.class); + + assertNotNull(deserialized); + assertEquals(LocalDate.of(1990, 5, 26), deserialized.getDateOfBirth()); + assertEquals("John", deserialized.getFirstName()); + assertEquals("White", deserialized.getLastName()); + } + + // ─── PaymentContextsGuests ─────────────────────────────────────────────── + + @Test + void guestDateOfBirthShouldSerializeAsDateNotDatetime() { + final PaymentContextsGuests guest = PaymentContextsGuests.builder() + .firstName("Jane") + .lastName("Doe") + .dateOfBirth(LocalDate.of(1985, 7, 14)) + .build(); + + final String json = serializer.toJson(guest); + + assertTrue(json.contains("\"date_of_birth\":\"1985-07-14\""), + "Expected yyyy-MM-dd format but got: " + json); + assertFalse(json.contains("T00:00:00Z"), + "date_of_birth must NOT be an ISO-8601 datetime: " + json); + } + + @Test + void guestDateOfBirthShouldRoundTrip() { + final PaymentContextsGuests original = PaymentContextsGuests.builder() + .firstName("Jane") + .lastName("Doe") + .dateOfBirth(LocalDate.of(1985, 7, 14)) + .build(); + + final String json = serializer.toJson(original); + final PaymentContextsGuests deserialized = serializer.fromJson(json, PaymentContextsGuests.class); + + assertNotNull(deserialized); + assertEquals(LocalDate.of(1985, 7, 14), deserialized.getDateOfBirth()); + } + + // ─── PaymentContextsTicket ─────────────────────────────────────────────── + + @Test + void ticketIssueDateShouldSerializeAsDateNotDatetime() { + final PaymentContextsTicket ticket = PaymentContextsTicket.builder() + .number("045-21351455613") + .issueDate(LocalDate.of(2023, 5, 20)) + .issuingCarrierCode("AI") + .build(); + + final String json = serializer.toJson(ticket); + + assertTrue(json.contains("\"issue_date\":\"2023-05-20\""), + "Expected yyyy-MM-dd format but got: " + json); + assertFalse(json.contains("T00:00:00Z"), + "issue_date must NOT be an ISO-8601 datetime: " + json); + } + + @Test + void ticketIssueDateShouldRoundTrip() { + final PaymentContextsTicket original = PaymentContextsTicket.builder() + .number("045-21351455613") + .issueDate(LocalDate.of(2023, 5, 20)) + .issuingCarrierCode("AI") + .travelPackageIndicator("B") + .travelAgencyName("World Tours") + .travelAgencyCode("01") + .build(); + + final String json = serializer.toJson(original); + final PaymentContextsTicket deserialized = serializer.fromJson(json, PaymentContextsTicket.class); + + assertNotNull(deserialized); + assertEquals(LocalDate.of(2023, 5, 20), deserialized.getIssueDate()); + assertEquals("045-21351455613", deserialized.getNumber()); + } + + // ─── PaymentContextsFlightLegDetails ───────────────────────────────────── + + @Test + void flightLegDepartureDateShouldSerializeAsDateNotDatetime() { + final PaymentContextsFlightLegDetails leg = PaymentContextsFlightLegDetails.builder() + .flightNumber("AA100") + .carrierCode("AA") + .departureAirport("LHR") + .departureDate(LocalDate.of(2023, 6, 19)) + .departureTime("09:00") + .arrivalAirport("JFK") + .build(); + + final String json = serializer.toJson(leg); + + assertTrue(json.contains("\"departure_date\":\"2023-06-19\""), + "Expected yyyy-MM-dd format but got: " + json); + assertFalse(json.contains("T00:00:00Z"), + "departure_date must NOT be an ISO-8601 datetime: " + json); + } + + @Test + void flightLegDepartureDateShouldRoundTrip() { + final PaymentContextsFlightLegDetails original = PaymentContextsFlightLegDetails.builder() + .flightNumber("AA100") + .carrierCode("AA") + .departureAirport("LHR") + .departureDate(LocalDate.of(2023, 6, 19)) + .departureTime("09:00") + .arrivalAirport("JFK") + .stopOverCode("X") + .fareBasisCode("SPRSVR") + .build(); + + final String json = serializer.toJson(original); + final PaymentContextsFlightLegDetails deserialized = + serializer.fromJson(json, PaymentContextsFlightLegDetails.class); + + assertNotNull(deserialized); + assertEquals(LocalDate.of(2023, 6, 19), deserialized.getDepartureDate()); + assertEquals("AA100", deserialized.getFlightNumber()); + } + + // ─── PaymentContextsAccommodationData ──────────────────────────────────── + + @Test + void accommodationCheckInOutDatesShouldSerializeAsDateNotDatetime() { + final PaymentContextsAccommodationData accommodation = PaymentContextsAccommodationData.builder() + .name("Grand Hotel") + .checkInDate(LocalDate.of(2023, 6, 20)) + .checkOutDate(LocalDate.of(2023, 6, 23)) + .numberOfRooms(2) + .guests(Arrays.asList( + PaymentContextsGuests.builder() + .firstName("Jane") + .lastName("Doe") + .dateOfBirth(LocalDate.of(1985, 7, 14)) + .build())) + .build(); + + final String json = serializer.toJson(accommodation); + + assertTrue(json.contains("\"check_in_date\":\"2023-06-20\""), + "check_in_date expected yyyy-MM-dd but got: " + json); + assertTrue(json.contains("\"check_out_date\":\"2023-06-23\""), + "check_out_date expected yyyy-MM-dd but got: " + json); + assertTrue(json.contains("\"date_of_birth\":\"1985-07-14\""), + "guest date_of_birth expected yyyy-MM-dd but got: " + json); + assertFalse(json.contains("T00:00:00Z"), + "No date field should serialize as ISO-8601 datetime: " + json); + } + + @Test + void accommodationDatesShouldRoundTrip() { + final PaymentContextsAccommodationData original = PaymentContextsAccommodationData.builder() + .name("Grand Hotel") + .bookingReference("BK-12345") + .checkInDate(LocalDate.of(2023, 6, 20)) + .checkOutDate(LocalDate.of(2023, 6, 23)) + .numberOfRooms(2) + .build(); + + final String json = serializer.toJson(original); + final PaymentContextsAccommodationData deserialized = + serializer.fromJson(json, PaymentContextsAccommodationData.class); + + assertNotNull(deserialized); + assertEquals(LocalDate.of(2023, 6, 20), deserialized.getCheckInDate()); + assertEquals(LocalDate.of(2023, 6, 23), deserialized.getCheckOutDate()); + assertEquals("Grand Hotel", deserialized.getName()); + } + + // ─── PaymentContextsCustomerSummary ────────────────────────────────────── + + @Test + void customerSummaryDatesShouldSerializeAsDateNotDatetime() { + final PaymentContextsCustomerSummary summary = PaymentContextsCustomerSummary.builder() + .registrationDate(LocalDate.of(2023, 5, 1)) + .firstTransactionDate(LocalDate.of(2023, 7, 1)) + .lastPaymentDate(LocalDate.of(2023, 8, 1)) + .totalOrderCount(5) + .lastPaymentAmount(99.99f) + .build(); + + final String json = serializer.toJson(summary); + + assertTrue(json.contains("\"registration_date\":\"2023-05-01\""), + "registration_date expected yyyy-MM-dd but got: " + json); + assertTrue(json.contains("\"first_transaction_date\":\"2023-07-01\""), + "first_transaction_date expected yyyy-MM-dd but got: " + json); + assertTrue(json.contains("\"last_payment_date\":\"2023-08-01\""), + "last_payment_date expected yyyy-MM-dd but got: " + json); + assertFalse(json.contains("T00:00:00Z"), + "No date field should serialize as ISO-8601 datetime: " + json); + } + + @Test + void customerSummaryDatesShouldRoundTrip() { + final PaymentContextsCustomerSummary original = PaymentContextsCustomerSummary.builder() + .registrationDate(LocalDate.of(2023, 5, 1)) + .firstTransactionDate(LocalDate.of(2023, 7, 1)) + .lastPaymentDate(LocalDate.of(2023, 8, 1)) + .totalOrderCount(5) + .lastPaymentAmount(99.99f) + .build(); + + final String json = serializer.toJson(original); + final PaymentContextsCustomerSummary deserialized = + serializer.fromJson(json, PaymentContextsCustomerSummary.class); + + assertNotNull(deserialized); + assertEquals(LocalDate.of(2023, 5, 1), deserialized.getRegistrationDate()); + assertEquals(LocalDate.of(2023, 7, 1), deserialized.getFirstTransactionDate()); + assertEquals(LocalDate.of(2023, 8, 1), deserialized.getLastPaymentDate()); + assertEquals(5, deserialized.getTotalOrderCount()); + } +} diff --git a/src/test/java/com/checkout/payments/sender/PaymentIndividualSenderSerializationTest.java b/src/test/java/com/checkout/payments/sender/PaymentIndividualSenderSerializationTest.java new file mode 100644 index 00000000..616b2e50 --- /dev/null +++ b/src/test/java/com/checkout/payments/sender/PaymentIndividualSenderSerializationTest.java @@ -0,0 +1,167 @@ +package com.checkout.payments.sender; + +import com.checkout.GsonSerializer; +import com.checkout.common.AccountHolderIdentification; +import com.checkout.common.AccountHolderIdentificationType; +import com.checkout.common.Address; +import com.checkout.common.CountryCode; +import org.junit.jupiter.api.Test; + +import java.time.LocalDate; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Schema serialization tests for PaymentIndividualSender. + * + *

Verifies that {@code dateOfBirth} serializes as {@code yyyy-MM-dd} (not an ISO 8601 + * timestamp), matching the Checkout.com API expectation for the {@code date_of_birth} field + * (swagger format: date). This was the root cause of issue #522.

+ */ +class PaymentIndividualSenderSerializationTest { + + private final GsonSerializer serializer = new GsonSerializer(); + + @Test + void shouldSerializeWithRequiredFields() { + final PaymentIndividualSender sender = PaymentIndividualSender.builder() + .firstName("John") + .lastName("Doe") + .address(Address.builder() + .addressLine1("123 Main St") + .city("London") + .country(CountryCode.GB) + .build()) + .build(); + + assertDoesNotThrow(() -> serializer.toJson(sender)); + } + + @Test + void shouldSerializeWithAllOptionalFields() { + final PaymentIndividualSender sender = PaymentIndividualSender.builder() + .firstName("John") + .middleName("James") + .lastName("Doe") + .dob("1990-05-15") + .dateOfBirth(LocalDate.of(1990, 5, 15)) + .address(Address.builder() + .addressLine1("123 Main St") + .addressLine2("Apt 4B") + .city("London") + .zip("SW1A 1AA") + .country(CountryCode.GB) + .build()) + .identification(AccountHolderIdentification.builder() + .type(AccountHolderIdentificationType.DRIVING_LICENSE) + .number("DL1234567") + .issuingCountry(CountryCode.GB) + .build()) + .referenceType("pep") + .sourceOfFunds(SourceOfFunds.CREDIT) + .countryOfBirth(CountryCode.GB) + .nationality(CountryCode.GB) + .reference("ref-001") + .build(); + + assertDoesNotThrow(() -> serializer.toJson(sender)); + } + + /** + * This is the core regression test for issue #522. + * + *

When {@code dateOfBirth} was {@code Instant}, it serialized as + * {@code "1990-05-15T00:00:00Z"} which the API rejected with a 422 sender_invalid error. + * Now that it is {@code LocalDate}, the GsonSerializer formats it as {@code "1990-05-15"}, + * which is the format the API expects.

+ */ + @Test + void shouldSerializeDateOfBirthAsYyyyMmDdNotAsTimestamp() { + final PaymentIndividualSender sender = PaymentIndividualSender.builder() + .firstName("John") + .lastName("Doe") + .dateOfBirth(LocalDate.of(1990, 5, 15)) + .address(Address.builder() + .addressLine1("123 Main St") + .city("London") + .country(CountryCode.GB) + .build()) + .build(); + + final String json = serializer.toJson(sender); + + assertTrue(json.contains("\"1990-05-15\""), + "dateOfBirth must serialize as yyyy-MM-dd, not as ISO timestamp. Got: " + json); + assertTrue(json.contains("date_of_birth"), + "Should use snake_case field name 'date_of_birth'. Got: " + json); + } + + @Test + void shouldRoundTripSerialize() { + final PaymentIndividualSender original = PaymentIndividualSender.builder() + .firstName("John") + .lastName("Doe") + .dateOfBirth(LocalDate.of(1985, 5, 15)) + .address(Address.builder() + .addressLine1("123 Main St") + .city("London") + .country(CountryCode.GB) + .build()) + .build(); + + final String json = serializer.toJson(original); + final PaymentIndividualSender deserialized = serializer.fromJson(json, PaymentIndividualSender.class); + + assertNotNull(deserialized); + assertEquals(original.getFirstName(), deserialized.getFirstName()); + assertEquals(original.getLastName(), deserialized.getLastName()); + assertEquals(original.getDateOfBirth(), deserialized.getDateOfBirth()); + } + + @Test + void shouldDeserializeSwaggerExample() { + final String swaggerJson = "{" + + "\"type\":\"individual\"," + + "\"first_name\":\"John\"," + + "\"last_name\":\"Jones\"," + + "\"date_of_birth\":\"1985-05-15\"," + + "\"address\":{" + + " \"address_line1\":\"123 Main St\"," + + " \"city\":\"London\"," + + " \"country\":\"GB\"" + + "}," + + "\"reference\":\"ref-001\"" + + "}"; + + final PaymentIndividualSender sender = serializer.fromJson(swaggerJson, PaymentIndividualSender.class); + + assertNotNull(sender); + assertEquals("John", sender.getFirstName()); + assertEquals("Jones", sender.getLastName()); + assertEquals(LocalDate.of(1985, 5, 15), sender.getDateOfBirth()); + } + + @Test + void shouldHandleNullDateOfBirth() { + final PaymentIndividualSender sender = PaymentIndividualSender.builder() + .firstName("John") + .lastName("Doe") + .dateOfBirth(null) + .address(Address.builder() + .addressLine1("123 Main St") + .city("London") + .country(CountryCode.GB) + .build()) + .build(); + + final String json = serializer.toJson(sender); + + assertDoesNotThrow(() -> serializer.toJson(sender)); + // null dateOfBirth should be omitted from the serialized JSON + assertTrue(!json.contains("date_of_birth") || json.contains("\"date_of_birth\":null"), + "null dateOfBirth should be omitted or null in JSON. Got: " + json); + } +} diff --git a/src/test/java/com/checkout/schema/LocalDateFieldsRegressionTest.java b/src/test/java/com/checkout/schema/LocalDateFieldsRegressionTest.java new file mode 100644 index 00000000..f7de6548 --- /dev/null +++ b/src/test/java/com/checkout/schema/LocalDateFieldsRegressionTest.java @@ -0,0 +1,370 @@ +package com.checkout.schema; + +import com.checkout.GsonSerializer; +import com.checkout.common.CountryCode; +import com.checkout.common.Currency; +import com.checkout.handlepaymentsandpayouts.setups.entities.customer.MerchantAccount; +import com.checkout.handlepaymentsandpayouts.setups.entities.order.OrderSubMerchant; +import com.checkout.instruments.create.InstrumentData; +import com.checkout.payments.PaymentType; +import com.checkout.payments.ProductRequest; +import com.checkout.payments.ProductResponse; +import com.checkout.payments.request.ItemSubType; +import com.checkout.payments.request.ItemType; +import org.junit.jupiter.api.Test; + +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; + +/** + * Regression tests for {@code format: date} (yyyy-MM-dd) fields that were previously + * typed as {@link java.time.Instant}. + * + *

An {@code Instant} serializes as an ISO-8601 datetime (e.g. {@code 2025-01-01T00:00:00Z}), + * which the API rejects with a 422. These tests verify that each corrected field serializes + * as a plain date string ({@code yyyy-MM-dd}). + * + *

Every round-trip test sets and asserts all properties of the class, + * not only the corrected field — as required by the review-integrity checklist. + * + *

Classes covered: + *

+ */ +class LocalDateFieldsRegressionTest { + + private final GsonSerializer serializer = new GsonSerializer(); + + // ── ProductRequest — ALL properties ────────────────────────────────────── + + @Test + void productRequest_allProperties_roundTrip() { + ProductRequest original = ProductRequest.builder() + .type(ItemType.PHYSICAL) + .subType(ItemSubType.BLOCKCHAIN) + .name("Hotel Room — Deluxe") + .quantity(2L) + .unitPrice(15000L) + .reference("REF-001") + .commodityCode("12345678") + .unitOfMeasure("night") + .totalAmount(30000L) + .taxRate(2000L) + .taxAmount(6000L) + .discountAmount(1000L) + .wxpayGoodsId("WX-GOODS-001") + .imageUrl("https://example.com/room.jpg") + .url("https://example.com/product/room") + .sku("SKU-DELUXE-001") + .serviceEndsOn(LocalDate.of(2025, 6, 30)) + .purchaseCountry(CountryCode.GB) + .foreignRetailerAmount(500L) + .build(); + + String json = serializer.toJson(original); + ProductRequest deserialized = serializer.fromJson(json, ProductRequest.class); + + assertNotNull(deserialized); + assertEquals(ItemType.PHYSICAL, deserialized.getType()); + assertEquals(ItemSubType.BLOCKCHAIN, deserialized.getSubType()); + assertEquals("Hotel Room — Deluxe", deserialized.getName()); + assertEquals(2L, deserialized.getQuantity()); + assertEquals(15000L, deserialized.getUnitPrice()); + assertEquals("REF-001", deserialized.getReference()); + assertEquals("12345678", deserialized.getCommodityCode()); + assertEquals("night", deserialized.getUnitOfMeasure()); + assertEquals(30000L, deserialized.getTotalAmount()); + assertEquals(2000L, deserialized.getTaxRate()); + assertEquals(6000L, deserialized.getTaxAmount()); + assertEquals(1000L, deserialized.getDiscountAmount()); + assertEquals("WX-GOODS-001", deserialized.getWxpayGoodsId()); + assertEquals("https://example.com/room.jpg", deserialized.getImageUrl()); + assertEquals("https://example.com/product/room", deserialized.getUrl()); + assertEquals("SKU-DELUXE-001", deserialized.getSku()); + assertEquals(LocalDate.of(2025, 6, 30), deserialized.getServiceEndsOn()); + assertEquals(CountryCode.GB, deserialized.getPurchaseCountry()); + assertEquals(500L, deserialized.getForeignRetailerAmount()); + } + + @Test + void productRequest_serviceEndsOn_shouldSerializeAsDateOnly() { + ProductRequest request = ProductRequest.builder() + .name("Subscription") + .serviceEndsOn(LocalDate.of(2025, 1, 1)) + .build(); + + String json = serializer.toJson(request); + + assertTrue(json.contains("\"service_ends_on\":\"2025-01-01\""), + "Expected yyyy-MM-dd, got: " + json); + assertFalse(json.contains("T00:00:00Z"), "Must not contain ISO-8601 timestamp: " + json); + } + + @Test + void productRequest_serviceEndsOn_shouldDeserializeFromSwaggerExample() { + String json = "{\"name\":\"Item\",\"quantity\":1,\"unit_price\":1000,\"service_ends_on\":\"2025-01-01\"}"; + + ProductRequest request = serializer.fromJson(json, ProductRequest.class); + + assertNotNull(request); + assertEquals(LocalDate.of(2025, 1, 1), request.getServiceEndsOn()); + assertEquals("Item", request.getName()); + } + + @Test + void productRequest_withoutServiceEndsOn_shouldNotFail() { + ProductRequest request = ProductRequest.builder().name("Item").quantity(1L).build(); + String json = serializer.toJson(request); + ProductRequest deserialized = serializer.fromJson(json, ProductRequest.class); + assertNull(deserialized.getServiceEndsOn()); + } + + // ── ProductResponse — ALL properties ───────────────────────────────────── + + @Test + void productResponse_allProperties_roundTrip() { + ProductResponse original = ProductResponse.builder() + .name("Annual License") + .quantity(1L) + .unitPrice(10000L) + .reference("RESP-REF-001") + .commodityCode("99998888") + .unitOfMeasure("license") + .totalAmount(10000L) + .taxRate(1000L) + .taxAmount(1000L) + .discountAmount(500L) + .wxpayGoodsId("WX-RESP-001") + .imageUrl("https://example.com/license.png") + .url("https://example.com/product/license") + .sku("SKU-ANNUAL-001") + .serviceEndsOn(LocalDate.of(2025, 12, 31)) + .build(); + + String json = serializer.toJson(original); + ProductResponse deserialized = serializer.fromJson(json, ProductResponse.class); + + assertNotNull(deserialized); + assertEquals("Annual License", deserialized.getName()); + assertEquals(1L, deserialized.getQuantity()); + assertEquals(10000L, deserialized.getUnitPrice()); + assertEquals("RESP-REF-001", deserialized.getReference()); + assertEquals("99998888", deserialized.getCommodityCode()); + assertEquals("license", deserialized.getUnitOfMeasure()); + assertEquals(10000L, deserialized.getTotalAmount()); + assertEquals(1000L, deserialized.getTaxRate()); + assertEquals(1000L, deserialized.getTaxAmount()); + assertEquals(500L, deserialized.getDiscountAmount()); + assertEquals("WX-RESP-001", deserialized.getWxpayGoodsId()); + assertEquals("https://example.com/license.png", deserialized.getImageUrl()); + assertEquals("https://example.com/product/license", deserialized.getUrl()); + assertEquals("SKU-ANNUAL-001", deserialized.getSku()); + assertEquals(LocalDate.of(2025, 12, 31), deserialized.getServiceEndsOn()); + } + + @Test + void productResponse_serviceEndsOn_shouldSerializeAsDateOnly() { + ProductResponse response = ProductResponse.builder() + .name("Service") + .serviceEndsOn(LocalDate.of(2025, 12, 31)) + .build(); + + String json = serializer.toJson(response); + + assertTrue(json.contains("\"service_ends_on\":\"2025-12-31\""), + "Expected yyyy-MM-dd, got: " + json); + assertFalse(json.contains("T00:00:00Z"), "Must not contain ISO-8601 timestamp: " + json); + } + + @Test + void productResponse_serviceEndsOn_shouldDeserializeFromSwaggerExample() { + String json = "{\"name\":\"Service\",\"quantity\":1,\"service_ends_on\":\"2025-01-01\"}"; + + ProductResponse response = serializer.fromJson(json, ProductResponse.class); + + assertNotNull(response); + assertEquals(LocalDate.of(2025, 1, 1), response.getServiceEndsOn()); + } + + // ── InstrumentData — ALL properties ────────────────────────────────────── + + @Test + void instrumentData_allProperties_roundTrip() { + InstrumentData original = InstrumentData.builder() + .accoountNumber("DE89370400440532013000") + .country(CountryCode.DE) + .currency(Currency.EUR) + .paymentType(PaymentType.REGULAR) + .mandateId("MANDATE-XYZ-999") + .dateOfSignature(LocalDate.of(2021, 3, 15)) + .build(); + + String json = serializer.toJson(original); + InstrumentData deserialized = serializer.fromJson(json, InstrumentData.class); + + assertNotNull(deserialized); + assertEquals("DE89370400440532013000", deserialized.getAccoountNumber()); + assertEquals(CountryCode.DE, deserialized.getCountry()); + assertEquals(Currency.EUR, deserialized.getCurrency()); + assertEquals(PaymentType.REGULAR, deserialized.getPaymentType()); + assertEquals("MANDATE-XYZ-999", deserialized.getMandateId()); + assertEquals(LocalDate.of(2021, 3, 15), deserialized.getDateOfSignature()); + } + + @Test + void instrumentData_dateOfSignature_shouldSerializeAsDateOnly() { + InstrumentData data = InstrumentData.builder() + .mandateId("MND-001") + .dateOfSignature(LocalDate.of(2021, 1, 1)) + .build(); + + String json = serializer.toJson(data); + + assertTrue(json.contains("\"date_of_signature\":\"2021-01-01\""), + "Expected yyyy-MM-dd, got: " + json); + assertFalse(json.contains("T00:00:00Z"), "Must not contain ISO-8601 timestamp: " + json); + } + + @Test + void instrumentData_dateOfSignature_shouldDeserializeFromSwaggerExample() { + String json = "{\"mandate_id\":\"abc123\",\"date_of_signature\":\"2021-01-01\"}"; + + InstrumentData data = serializer.fromJson(json, InstrumentData.class); + + assertNotNull(data); + assertEquals(LocalDate.of(2021, 1, 1), data.getDateOfSignature()); + assertEquals("abc123", data.getMandateId()); + } + + // ── MerchantAccount — ALL properties ───────────────────────────────────── + + @Test + void merchantAccount_allProperties_roundTrip() { + MerchantAccount original = MerchantAccount.builder() + .id("ACC-FULL-001") + .registrationDate(LocalDate.of(2023, 5, 1)) + .lastModified(LocalDate.of(2023, 11, 20)) + .returningCustomer(true) + .firstTransactionDate(LocalDate.of(2023, 9, 15)) + .lastTransactionDate(LocalDate.of(2025, 3, 28)) + .totalOrderCount(42) + .lastPaymentAmount(75000L) + .build(); + + String json = serializer.toJson(original); + MerchantAccount deserialized = serializer.fromJson(json, MerchantAccount.class); + + assertNotNull(deserialized); + assertEquals("ACC-FULL-001", deserialized.getId()); + assertEquals(LocalDate.of(2023, 5, 1), deserialized.getRegistrationDate()); + assertEquals(LocalDate.of(2023, 11, 20), deserialized.getLastModified()); + assertEquals(true, deserialized.getReturningCustomer()); + assertEquals(LocalDate.of(2023, 9, 15), deserialized.getFirstTransactionDate()); + assertEquals(LocalDate.of(2025, 3, 28), deserialized.getLastTransactionDate()); + assertEquals(42, deserialized.getTotalOrderCount()); + assertEquals(75000L, deserialized.getLastPaymentAmount()); + } + + @Test + void merchantAccount_allDateFields_shouldSerializeAsDateOnly() { + MerchantAccount account = MerchantAccount.builder() + .registrationDate(LocalDate.of(2023, 5, 1)) + .lastModified(LocalDate.of(2023, 5, 1)) + .firstTransactionDate(LocalDate.of(2023, 9, 15)) + .lastTransactionDate(LocalDate.of(2025, 3, 28)) + .build(); + + String json = serializer.toJson(account); + + assertTrue(json.contains("\"registration_date\":\"2023-05-01\""), "registration_date: " + json); + assertTrue(json.contains("\"last_modified\":\"2023-05-01\""), "last_modified: " + json); + assertTrue(json.contains("\"first_transaction_date\":\"2023-09-15\""), "first_transaction_date: " + json); + assertTrue(json.contains("\"last_transaction_date\":\"2025-03-28\""), "last_transaction_date: " + json); + assertFalse(json.contains("T00:00:00Z"), "Must not contain ISO-8601 timestamp: " + json); + } + + @Test + void merchantAccount_allDateFields_shouldDeserializeFromSwaggerExample() { + String json = "{" + + "\"id\":\"ACC-123\"," + + "\"registration_date\":\"2023-05-01\"," + + "\"last_modified\":\"2023-05-01\"," + + "\"first_transaction_date\":\"2023-09-15\"," + + "\"last_transaction_date\":\"2025-03-28\"," + + "\"returning_customer\":true," + + "\"total_order_count\":10," + + "\"last_payment_amount\":20000" + + "}"; + + MerchantAccount account = serializer.fromJson(json, MerchantAccount.class); + + assertNotNull(account); + assertEquals("ACC-123", account.getId()); + assertEquals(LocalDate.of(2023, 5, 1), account.getRegistrationDate()); + assertEquals(LocalDate.of(2023, 5, 1), account.getLastModified()); + assertEquals(LocalDate.of(2023, 9, 15), account.getFirstTransactionDate()); + assertEquals(LocalDate.of(2025, 3, 28), account.getLastTransactionDate()); + assertEquals(true, account.getReturningCustomer()); + assertEquals(10, account.getTotalOrderCount()); + assertEquals(20000L, account.getLastPaymentAmount()); + } + + // ── OrderSubMerchant — ALL properties ──────────────────────────────────── + + @Test + void orderSubMerchant_allProperties_roundTrip() { + OrderSubMerchant original = OrderSubMerchant.builder() + .id("SUB-FULL-001") + .productCategory("Electronics & Gadgets") + .numberOfTrades(250) + .registrationDate(LocalDate.of(2021, 8, 22)) + .build(); + + String json = serializer.toJson(original); + OrderSubMerchant deserialized = serializer.fromJson(json, OrderSubMerchant.class); + + assertNotNull(deserialized); + assertEquals("SUB-FULL-001", deserialized.getId()); + assertEquals("Electronics & Gadgets", deserialized.getProductCategory()); + assertEquals(250, deserialized.getNumberOfTrades()); + assertEquals(LocalDate.of(2021, 8, 22), deserialized.getRegistrationDate()); + } + + @Test + void orderSubMerchant_registrationDate_shouldSerializeAsDateOnly() { + OrderSubMerchant subMerchant = OrderSubMerchant.builder() + .id("SUB-001") + .registrationDate(LocalDate.of(2023, 1, 15)) + .build(); + + String json = serializer.toJson(subMerchant); + + assertTrue(json.contains("\"registration_date\":\"2023-01-15\""), + "Expected yyyy-MM-dd, got: " + json); + assertFalse(json.contains("T00:00:00Z"), "Must not contain ISO-8601 timestamp: " + json); + } + + @Test + void orderSubMerchant_registrationDate_shouldDeserializeFromSwaggerExample() { + String json = "{\"id\":\"SUB-123\",\"product_category\":\"Fashion\",\"number_of_trades\":50,\"registration_date\":\"2023-01-15\"}"; + + OrderSubMerchant subMerchant = serializer.fromJson(json, OrderSubMerchant.class); + + assertNotNull(subMerchant); + assertEquals("SUB-123", subMerchant.getId()); + assertEquals("Fashion", subMerchant.getProductCategory()); + assertEquals(50, subMerchant.getNumberOfTrades()); + assertEquals(LocalDate.of(2023, 1, 15), subMerchant.getRegistrationDate()); + } +}