Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 133 additions & 0 deletions src/main/java/com/checkout/GsonSerializer.java
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import com.google.gson.annotations.SerializedName;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import java.io.IOException;
import com.google.gson.reflect.TypeToken;
import com.google.gson.typeadapters.RuntimeTypeAdapterFactory;
import lombok.Getter;
Expand Down Expand Up @@ -108,6 +113,24 @@
.registerTypeAdapter(LocalDate.class, (JsonSerializer<LocalDate>) (LocalDate date, Type typeOfSrc, JsonSerializationContext context) ->
new JsonPrimitive(date.format(DateTimeFormatter.ISO_LOCAL_DATE)))
.registerTypeAdapter(LocalDate.class, getLocalDateJsonDeserializer())
// processing.airline_data[].passenger arrives as an array or as a single object.
// These two read both shapes into a List.
//
// Bound by element type, so the second registration also covers PaymentSetupAirline
// .passengers, which the spec declares array-only. Accepting a bare object there on
// READ is wider than the spec grants but cannot lose data. Writing is handled by
// singleOrArrayPassengerFactory below, which is scoped to the two airline types so
// that setups .passengers keeps emitting an array (the API rejects an object there
// with industry.airline[0].passengers_property_invalid).
.registerTypeAdapter(
new TypeToken<List<com.checkout.payments.Passenger>>() {
}.getType(),
singleOrArrayDeserializer(com.checkout.payments.Passenger.class))
.registerTypeAdapter(
new TypeToken<List<com.checkout.payments.contexts.PaymentContextsPassenger>>() {
}.getType(),
singleOrArrayDeserializer(com.checkout.payments.contexts.PaymentContextsPassenger.class))
.registerTypeAdapterFactory(singleOrArrayPassengerFactory())
// Payments - AbstractSource (polymorphic deserialization)
.registerTypeAdapterFactory(
RuntimeTypeAdapterFactory.of(
Expand Down Expand Up @@ -454,6 +477,116 @@
};
}

/**
* Reads a property the specification declares as {@code oneOf[array, object]} into a list,
* accepting either shape on the wire and normalizing a bare object into a single-element list.
* <p>
* The first property to need this is {@code processing.airline_data[].passenger}.
* {@code AirlineData} declares it as an array, while
* {@code PaymentInterfacesProcessingAirlineData} declares it as {@code oneOf[array, object]}
* with the note "PayPal requires a single object". Both branches resolve to the same object,
* so normalizing to a list loses nothing.
* <p>
* Only a deserializer is registered, never a serializer, so writing still goes through Gson's
* reflective adapter and always emits an array. That is the only valid outbound shape for
* {@code AirlineData}. Element deserialization is delegated to the supplied context, so the
* global {@code LOWER_CASE_WITH_UNDERSCORES} naming policy and the {@code LocalDate} adapter
* still apply; this deserializer never maps property names itself.
*
* @param elementType the list element type
* @param <T> the list element type
* @return a deserializer that accepts a single object or an array
*/
/**
* Writes {@code processing.airline_data[].passenger} as a single object when there is exactly
* one passenger and as an array only when there are several.
*
* <p>The live API does not match the specification in either direction. Verified against the
* sandbox on 2026-09-25 with a complete {@code airline_data} block:
*
* <pre>
* surface passenger: object passenger: array
* POST /payments 201 201
* POST /hosted-payments accepted 422 processing_airline_data_0_passenger_invalid
* POST /payment-links accepted 422 processing_airline_data_0_passenger_invalid
* POST /payment-contexts 201 422 passenger_required
* </pre>
*
* <p>A single object is accepted on every request surface; an array only on
* {@code POST /payments}. {@link com.checkout.payments.ProcessingSettings} is shared by
* {@code POST /payments}, hosted payments and payment links, so always emitting an array
* would break the latter two.
*
* <p>An empty array and an explicit null are both rejected with
* {@code processing_airline_data_0_passenger_invalid}, so an empty list drops the member
* entirely.
*
* <p>Scoped to the two airline types by raw class, so {@code PaymentSetupAirline.passengers}
* is untouched: the API rejects an object there with
* {@code industry.airline[0].passengers_property_invalid}.
*
* @return a factory that fixes up the passenger cardinality on write
*/
private static TypeAdapterFactory singleOrArrayPassengerFactory() {

Check failure on line 530 in src/main/java/com/checkout/GsonSerializer.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this method to reduce its Cognitive Complexity from 17 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=checkout_checkout-sdk-java&issues=AaDYENtA0PXU5AXB06Qc&open=AaDYENtA0PXU5AXB06Qc&pullRequest=676
return new TypeAdapterFactory() {
@Override
public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> type) {
final Class<?> raw = type.getRawType();
if (!com.checkout.payments.AirlineData.class.equals(raw)
&& !com.checkout.payments.contexts.PaymentContextsAirlineData.class.equals(raw)) {
return null;
}

// getDelegateAdapter returns the adapter Gson would otherwise use, so the
// reflective serializer still writes every other field and this cannot recurse.
final TypeAdapter<T> delegate = gson.getDelegateAdapter(this, type);
final TypeAdapter<JsonElement> elements = gson.getAdapter(JsonElement.class);

return new TypeAdapter<T>() {
@Override
public void write(final JsonWriter out, final T value) throws IOException {
final JsonElement tree = delegate.toJsonTree(value);
if (tree.isJsonObject()) {
final JsonObject object = tree.getAsJsonObject();
final JsonElement passenger = object.get("passenger");

Check failure on line 551 in src/main/java/com/checkout/GsonSerializer.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define a constant instead of duplicating this literal "passenger" 3 times.

See more on https://sonarcloud.io/project/issues?id=checkout_checkout-sdk-java&issues=AaDYENtA0PXU5AXB06Qb&open=AaDYENtA0PXU5AXB06Qb&pullRequest=676
if (passenger != null && passenger.isJsonArray()) {
final JsonArray array = passenger.getAsJsonArray();
if (array.size() == 0) {
object.remove("passenger");
} else if (array.size() == 1) {
object.add("passenger", array.get(0));
}
}
}
elements.write(out, tree);
}

@Override
public T read(final JsonReader in) throws IOException {
return delegate.read(in);
}
};
}
};
}

private static <T> JsonDeserializer<List<T>> singleOrArrayDeserializer(final Class<T> elementType) {
return (json, typeOfT, context) -> {
if (json == null || json.isJsonNull()) {
return null;
}
final List<T> values = new ArrayList<>();
if (json.isJsonArray()) {
for (final JsonElement element : json.getAsJsonArray()) {
values.add(context.deserialize(element, elementType));
}
} else {
values.add(context.deserialize(json, elementType));
}
return values;
};
}

private static JsonDeserializer<LocalDate> getLocalDateJsonDeserializer() {
return (json, typeOfT, context) -> {
String dateString = json.getAsString();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.checkout.handlepaymentsandpayouts.setups.entities.industry;

import com.google.gson.annotations.SerializedName;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
Expand All @@ -9,7 +8,7 @@
import java.util.List;

/**
* Industry-specific payment setup information
* Industry-specific information.
*/
@Data
@Builder
Expand All @@ -18,14 +17,21 @@
public final class Industry {

/**
* Airline industry-specific data for flight bookings and related payments
* Airline industry-specific data for flight bookings and related payments.
* [Optional]
* <p>
* Maps the specification property {@code airline}, which is an array. This was previously a
* single object named {@code airlineData}, so it needed an explicit serialized-name override
* to reach the right key at all, and it serialized as an object where the API expects an
* array, meaning the value never reached the gateway.
*/
@SerializedName("airline")
private AirlineData airlineData;
private List<AirlineData> airline;

/**
* Accommodation industry-specific data for hotel and cruise bookings and related payments
* Accommodation industry-specific data for hotel and cruise bookings and related payments.
* [Optional]
* <p>
* Maps the specification property {@code accommodation}.
*/
@SerializedName("accommodation")
private List<AccommodationData> accommodationData;
private List<AccommodationData> accommodation;
}
17 changes: 13 additions & 4 deletions src/main/java/com/checkout/payments/AccommodationData.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.checkout.payments;

import com.checkout.common.Address;
import com.checkout.common.CountryCode;
import com.checkout.common.Phone;
import lombok.AllArgsConstructor;
import lombok.Builder;
Expand All @@ -11,6 +10,9 @@
import java.time.LocalDate;
import java.util.List;

/**
* Contains information about the accommodation booked by the customer.
*/
@Data
@Builder
@NoArgsConstructor
Expand Down Expand Up @@ -44,8 +46,12 @@ public final class AccommodationData {
private LocalDate checkOutDate;

/**
* The address of the accommodation property.
* The address details of the accommodation.
* [Optional]
* <p>
* The specification defines only {@code address_line1} and {@code zip} on this object. The
* wider {@link Address} type is reused for consistency with the rest of the SDK; the
* remaining members are not read by the API on this property.
*/
private Address address;

Expand All @@ -56,10 +62,13 @@ public final class AccommodationData {
private String state;

/**
* The country where the property is located, as an ISO 3166-1 alpha-2 code.
* The ISO country code of the address.
* [Optional]
* <p>
* A free-form string rather than an ISO 3166-1 alpha-2 enum: the specification's example is
* the three-letter code {@code USA}, which no alpha-2 enum can represent. Mapping as string.
*/
private CountryCode country;
private String country;

/**
* The city where the property is located.
Expand Down
3 changes: 3 additions & 0 deletions src/main/java/com/checkout/payments/AccommodationGuest.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@

import java.time.LocalDate;

/**
* Contains information about a guest staying at the accommodation.
*/
@Data
@Builder
@NoArgsConstructor
Expand Down
3 changes: 3 additions & 0 deletions src/main/java/com/checkout/payments/AccommodationRoom.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
import lombok.Data;
import lombok.NoArgsConstructor;

/**
* Contains information about a room booked by the customer.
*/
@Data
@Builder
@NoArgsConstructor
Expand Down
20 changes: 20 additions & 0 deletions src/main/java/com/checkout/payments/AirlineData.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,35 @@

import java.util.List;

/**
* Contains information about the airline ticket and flights booked by the customer.
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public final class AirlineData {

/**
* Contains information about the airline ticket.
* [Optional]
*/
private Ticket ticket;

/**
* Contains information about the passenger(s) on the flight.
* [Optional]
* <p>
* The API returns this as an array. Some payment methods, PayPal among them, send a single
* object instead, which the specification allows on the payment sessions, hosted payments
* and payment links interfaces. Both shapes deserialize here; a single object becomes a
* one-element list. Serialization always emits an array.
*/
private List<Passenger> passenger;

/**
* Contains information about the flight leg(s) booked by the customer.
* [Optional]
*/
private List<FlightLegDetails> flightLegDetails;
}
71 changes: 65 additions & 6 deletions src/main/java/com/checkout/payments/FlightLegDetails.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,87 @@
import lombok.Data;
import lombok.NoArgsConstructor;

import java.time.LocalDate;

/**
* Contains information about a flight leg booked by the customer.
*/
@Data

Check notice

Code scanning / CodeQL

Deprecated method or constructor invocation Note

Invoking
FlightLegDetails.getServiceClass
should be avoided because it has been deprecated.
@Builder
@NoArgsConstructor
@AllArgsConstructor
public final class FlightLegDetails {

private Long flightNumber;
/**
* The flight identifier.
* [Optional]
*/
private String flightNumber;

/**
* The IATA 2-letter accounting code (PAX) that identifies the carrier.
* This field is required if the airline data includes leg details.
* [Optional]
*/
private String carrierCode;

private String serviceClass;
/**
* A one-letter travel class identifier. The following are common:
* F = First class, J = Business class, Y = Economy class, W = Premium economy.
* [Optional]
*/
private String classOfTravelling;

private String departureDate;
/**
* The IATA three-letter airport code of the departure airport.
* This field is required if the airline data includes leg details.
* [Optional]
*/
private String departureAirport;

private String departureTime;
/**
* The date of the scheduled take off.
* [Optional]
* Format: yyyy-MM-dd
*/
private LocalDate departureDate;

private String departureAirport;
/**
* The time of the scheduled take off.
* [Optional]
*/
private String departureTime;

/**
* The IATA 3-letter airport code of the destination airport.
* This field is required if the airline data includes leg details.
* [Optional]
*/
private String arrivalAirport;

private String stopoverCode;
/**
* A one-letter code that indicates whether the passenger is entitled to make a stopover.
* Can be a space, O if the passenger is entitled to make a stopover, or X if they are not.
* [Optional]
*/
private String stopOverCode;

/**
* The fare basis code, alphanumeric.
* [Optional]
*/
private String fareBasisCode;

/**
* Not in the current spec, will be removed in a future version.
* Serializes as {@code service_class}, which the API does not define, so the value is
* discarded by the gateway. Use {@link #getClassOfTravelling()} instead, which maps the
* spec property {@code class_of_travelling}.
*
* @deprecated Not defined by the API, the gateway discards it. Use
* {@code classOfTravelling}, which maps {@code class_of_travelling}.
*/
@Deprecated
private String serviceClass;

Check warning on line 89 in src/main/java/com/checkout/payments/FlightLegDetails.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Do not forget to remove this deprecated code someday.

See more on https://sonarcloud.io/project/issues?id=checkout_checkout-sdk-java&issues=AaDX5LNkk1PEmkU3-tPA&open=AaDX5LNkk1PEmkU3-tPA&pullRequest=676

}
Loading
Loading