diff --git a/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/customer/Customer.java b/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/customer/Customer.java index 9879d92d0..759b12038 100644 --- a/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/customer/Customer.java +++ b/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/customer/Customer.java @@ -1,5 +1,6 @@ package com.checkout.handlepaymentsandpayouts.setups.entities.customer; +import com.checkout.common.CountryCode; import com.checkout.common.Phone; import lombok.AllArgsConstructor; import lombok.Builder; @@ -7,7 +8,7 @@ import lombok.NoArgsConstructor; /** - * Customer information for payment setup + * The customer's details. */ @Data @Builder @@ -16,28 +17,52 @@ public final class Customer { /** - * The customer's email information + * Details of the customer's email. + * [Optional] */ private CustomerEmail email; /** - * <= 100 characters - * The customer's full name + * The customer's full name. + * [Optional] + * max 100 characters */ private String name; /** - * The customer's phone number + * The customer's phone number. + * [Optional] */ private Phone phone; /** - * Information about the customer's device + * Details of the customer's device. + * [Optional] */ private CustomerDevice device; /** - * Details of the account the customer holds with the merchant + * Details of the account the customer holds with the merchant. + * [Optional] */ private MerchantAccount merchantAccount; -} \ No newline at end of file + + /** + * The unique identifier of the customer. + * [Optional] + */ + private String id; + + /** + * The two-letter ISO country code of the customer for this payment. + * [Optional] + * min 2 characters, max 2 characters + */ + private CountryCode country; + + /** + * The customer's tax identification number. + * [Optional] + */ + private String taxNumber; +} diff --git a/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/customer/CustomerDevice.java b/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/customer/CustomerDevice.java index 7839347b2..e6ad2fca2 100644 --- a/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/customer/CustomerDevice.java +++ b/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/customer/CustomerDevice.java @@ -1,12 +1,13 @@ package com.checkout.handlepaymentsandpayouts.setups.entities.customer; +import com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.common.OsType; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; /** - * Customer device information + * Details of the customer's device. */ @Data @Builder @@ -15,7 +16,41 @@ public final class CustomerDevice { /** - * The locale setting of the customer's device (e.g., "en-US") + * The locale of the device, for example en_GB. + * [Optional] */ private String locale; -} \ No newline at end of file + + /** + * A unique identifier for the customer's device. + * [Optional] + */ + private String fingerprint; + + /** + * The customer's device IPv4 address, used by some payment methods for risk and eligibility checks. + * [Optional] + */ + private String ipv4; + + /** + * The customer's device IPv6 address, used by some payment methods for risk and eligibility checks. + * [Optional] + */ + private String ipv6; + + /** + * The type of client the customer uses to initiate the payment. Required when using Cash App + * Pay; a Cash App payment setup without it is rejected. + * [Optional] + * Enum: "web" "mobile_web" "app" + */ + private CustomerDeviceClient client; + + /** + * The operating system of the customer's device. + * [Optional] + * Enum: "android" "ios" + */ + private OsType os; +} diff --git a/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/customer/CustomerDeviceClient.java b/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/customer/CustomerDeviceClient.java new file mode 100644 index 000000000..2f3e56386 --- /dev/null +++ b/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/customer/CustomerDeviceClient.java @@ -0,0 +1,27 @@ +package com.checkout.handlepaymentsandpayouts.setups.entities.customer; + +import com.google.gson.annotations.SerializedName; + +/** + * The type of client the customer uses to initiate the payment. + */ +public enum CustomerDeviceClient { + + /** + * A web browser on a desktop device. + */ + @SerializedName("web") + WEB, + + /** + * A web browser on a mobile device. + */ + @SerializedName("mobile_web") + MOBILE_WEB, + + /** + * A native mobile application. + */ + @SerializedName("app") + APP +} diff --git a/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/paymentMethods/PaymentMethods.java b/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/paymentMethods/PaymentMethods.java index 7a685f8f4..8805eeb0b 100644 --- a/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/paymentMethods/PaymentMethods.java +++ b/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/paymentMethods/PaymentMethods.java @@ -12,6 +12,7 @@ import com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.blik.Blik; import com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.card.Card; import com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.cardpresent.CardPresent; +import com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.cashapp.CashApp; import com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.paybybank.PayByBank; import com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.stablecoin.Stablecoin; import com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.dana.Dana; @@ -307,4 +308,12 @@ public final class PaymentMethods { * [Optional] */ private Stablecoin stablecoin; + + /** + * The Cash App Pay payment method's details and configuration. The wire key is the single + * word cashapp, so keep this field name lowercase: the naming policy turns cashApp into + * cash_app. + * [Optional] + */ + private CashApp cashapp; } diff --git a/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/paymentMethods/cashapp/CashApp.java b/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/paymentMethods/cashapp/CashApp.java new file mode 100644 index 000000000..14c6b8200 --- /dev/null +++ b/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/paymentMethods/cashapp/CashApp.java @@ -0,0 +1,53 @@ +package com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.cashapp; + +import com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.common.PaymentMethodBase; +import com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.common.PaymentMethodInitialization; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * Cash App Pay payment method configuration. The customer must be sent to the redirect URL + * returned in the action to authorize the payment with Cash App. The payment setup customer + * device client is required for this method. + */ +@Data +@EqualsAndHashCode(callSuper = true) +public final class CashApp extends PaymentMethodBase { + + /** + * The initialization state of the payment method. Defaults to disabled. + * [Optional] + */ + private PaymentMethodInitialization initialization = PaymentMethodInitialization.DISABLED; + + /** + * Indicates whether the customer consents to share their Cash App customer profile with + * Checkout.com. When enabled, the customer profile is returned once, after the customer + * authorizes the payment. + * [Optional] + */ + private Boolean customerProfileSharing; + + /** + * The customer's Cash App profile that they consented to share. Included in the response when + * customer_profile_sharing is enabled. Cash App releases this profile only once: it is present + * in the first successful response when you get the payment setup after the customer + * authorizes the payment, and every subsequent response omits it, so store it on first read. + * [Optional] readOnly + */ + private CashAppCustomerProfile customerProfile; + + /** + * A reference for the Cash App Pay transaction, returned by the provider. + * [Optional] readOnly + * max 80 characters + */ + private String reference; + + /** + * The next available action for the payment method. When its type is redirect, send the + * customer to its redirect URL to authorize the payment with Cash App. + * [Optional] readOnly + */ + private CashAppAction action; +} diff --git a/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/paymentMethods/cashapp/CashAppAction.java b/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/paymentMethods/cashapp/CashAppAction.java new file mode 100644 index 000000000..48c998ee4 --- /dev/null +++ b/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/paymentMethods/cashapp/CashAppAction.java @@ -0,0 +1,30 @@ +package com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.cashapp; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * The next available action for the Cash App payment method. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public final class CashAppAction { + + /** + * The type of action. + * [Optional] readOnly + * Enum: "redirect" + */ + private CashAppActionType type; + + /** + * The URL to redirect the customer to so they can authorize the payment with Cash App. + * [Optional] readOnly + * Format: uri + */ + private String redirectUrl; +} diff --git a/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/paymentMethods/cashapp/CashAppActionType.java b/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/paymentMethods/cashapp/CashAppActionType.java new file mode 100644 index 000000000..6f7216594 --- /dev/null +++ b/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/paymentMethods/cashapp/CashAppActionType.java @@ -0,0 +1,15 @@ +package com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.cashapp; + +import com.google.gson.annotations.SerializedName; + +/** + * The type of next action for the Cash App payment method. + */ +public enum CashAppActionType { + + /** + * Redirect the customer to Cash App to authorize the payment. + */ + @SerializedName("redirect") + REDIRECT +} diff --git a/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/paymentMethods/cashapp/CashAppAddress.java b/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/paymentMethods/cashapp/CashAppAddress.java new file mode 100644 index 000000000..b2bc5a673 --- /dev/null +++ b/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/paymentMethods/cashapp/CashAppAddress.java @@ -0,0 +1,74 @@ +package com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.cashapp; + +import com.checkout.common.CountryCode; +import com.google.gson.annotations.SerializedName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * The customer's address from their Cash App profile. The keys follow Cash App's naming + * (address_line_1, locality, administrative_district_level_1), not the Checkout.com address, so + * the line and district fields carry an explicit serialized name: the naming policy alone would + * produce address_line1. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public final class CashAppAddress { + + /** + * The first line of the address. + * [Optional] readOnly + */ + @SerializedName("address_line_1") + private String addressLine1; + + /** + * The second line of the address. + * [Optional] readOnly + */ + @SerializedName("address_line_2") + private String addressLine2; + + /** + * The third line of the address. + * [Optional] readOnly + */ + @SerializedName("address_line_3") + private String addressLine3; + + /** + * The address locality, such as the city or town. + * [Optional] readOnly + */ + private String locality; + + /** + * The address sublocality, such as the district or neighborhood. + * [Optional] readOnly + */ + private String sublocality; + + /** + * The address's top-level administrative district, such as the state or province. + * [Optional] readOnly + */ + @SerializedName("administrative_district_level_1") + private String administrativeDistrictLevel1; + + /** + * The postal or zip code. + * [Optional] readOnly + */ + private String postalCode; + + /** + * The address country, in ISO 3166-1 alpha-2 format. + * [Optional] readOnly + * max 2 characters + */ + private CountryCode country; +} diff --git a/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/paymentMethods/cashapp/CashAppCustomerProfile.java b/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/paymentMethods/cashapp/CashAppCustomerProfile.java new file mode 100644 index 000000000..3d90e7eef --- /dev/null +++ b/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/paymentMethods/cashapp/CashAppCustomerProfile.java @@ -0,0 +1,102 @@ +package com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.cashapp; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * The customer's Cash App profile that they consented to share. Included in the response when + * customer_profile_sharing is enabled. Cash App releases this profile only once: it is present in + * the first successful response when you get the payment setup after the customer authorizes the + * payment, and every subsequent response omits it. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public final class CashAppCustomerProfile { + + /** + * Cash App's identifier for the customer. This is not a Checkout.com customer identifier. + * [Optional] readOnly + */ + private String customerId; + + /** + * The customer's $Cashtag. + * [Optional] readOnly + */ + private String cashtag; + + /** + * Cash App's reference for the customer profile. + * [Optional] readOnly + */ + private String referenceId; + + /** + * The customer's full name. + * [Optional] readOnly + */ + private String fullName; + + /** + * The customer's given name. + * [Optional] readOnly + */ + private String givenName; + + /** + * The customer's middle name. + * [Optional] readOnly + */ + private String middleName; + + /** + * The customer's family name. + * [Optional] readOnly + */ + private String familyName; + + /** + * The suffix of the customer's name. + * [Optional] readOnly + */ + private String suffix; + + /** + * The customer's date of birth. Kept as the raw string returned by the API, because the + * provider's value is not always a plain yyyy-MM-dd date. + * [Optional] readOnly + * Format: date + */ + private String birthDate; + + /** + * The customer's address. + * [Optional] readOnly + */ + private CashAppAddress address; + + /** + * The customer's phone number. + * [Optional] readOnly + */ + private String phoneNumber; + + /** + * The customer's email address. + * [Optional] readOnly + */ + private String emailAddress; + + /** + * The date and time the customer's Cash App account was created. Kept as the raw string + * returned by the API, so that a provider format the SDK does not expect cannot fail the + * whole response and lose the profile, which is only returned once. + * [Optional] readOnly + * Format: date-time + */ + private String customerSince; +} diff --git a/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/paymentMethods/common/OsType.java b/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/paymentMethods/common/OsType.java index 293c8ca72..9871071d2 100644 --- a/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/paymentMethods/common/OsType.java +++ b/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/paymentMethods/common/OsType.java @@ -3,13 +3,20 @@ import com.google.gson.annotations.SerializedName; /** - * The customer's operating system type. Required when terminal_type is not web. + * The operating system of the customer's device. Used by the wallet payment methods that take an + * os_type (required there when terminal_type is not web) and by the customer device os. */ public enum OsType { + /** + * Android. + */ @SerializedName("android") ANDROID, + /** + * iOS. + */ @SerializedName("ios") IOS } diff --git a/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/paymentMethods/common/PaymentMethodStatus.java b/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/paymentMethods/common/PaymentMethodStatus.java index 98927ebfb..f4d597f81 100644 --- a/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/paymentMethods/common/PaymentMethodStatus.java +++ b/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/paymentMethods/common/PaymentMethodStatus.java @@ -2,21 +2,53 @@ import com.google.gson.annotations.SerializedName; +/** + * The payment method status. + * Enum: "unavailable" "action_required" "ready" "initialization_required" "invalid" + */ public enum PaymentMethodStatus { + /** + * The payment method is not available for this payment setup. + */ @SerializedName("unavailable") UNAVAILABLE, + /** + * The payment method needs an action before it can be confirmed, for example a redirect. + */ @SerializedName("action_required") ACTION_REQUIRED, + /** + * Not defined by the current API specification. Kept for backward compatibility. + */ @SerializedName("pending") PENDING, + /** + * The payment method is ready to be confirmed. + */ @SerializedName("ready") READY, + /** + * Not defined by the current API specification. Kept for backward compatibility. + */ @SerializedName("available") - AVAILABLE + AVAILABLE, + + /** + * The payment method must be initialized, by setting its initialization to enabled, before + * it can be used. + */ + @SerializedName("initialization_required") + INITIALIZATION_REQUIRED, + + /** + * The payment method details are invalid. The flags describe what is missing or wrong. + */ + @SerializedName("invalid") + INVALID } diff --git a/src/test/java/com/checkout/handlepaymentsandpayouts/setups/CashAppSerializationTest.java b/src/test/java/com/checkout/handlepaymentsandpayouts/setups/CashAppSerializationTest.java new file mode 100644 index 000000000..71af47719 --- /dev/null +++ b/src/test/java/com/checkout/handlepaymentsandpayouts/setups/CashAppSerializationTest.java @@ -0,0 +1,327 @@ +package com.checkout.handlepaymentsandpayouts.setups; + +import com.checkout.GsonSerializer; +import com.checkout.common.CountryCode; +import com.checkout.handlepaymentsandpayouts.setups.entities.customer.Customer; +import com.checkout.handlepaymentsandpayouts.setups.entities.customer.CustomerDevice; +import com.checkout.handlepaymentsandpayouts.setups.entities.customer.CustomerDeviceClient; +import com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.PaymentMethods; +import com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.cashapp.CashApp; +import com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.cashapp.CashAppAction; +import com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.cashapp.CashAppAddress; +import com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.cashapp.CashAppCustomerProfile; +import com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.cashapp.CashAppActionType; +import com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.common.OsType; +import com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.common.PaymentMethodInitialization; +import com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.common.PaymentMethodStatus; +import com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.klarna.Klarna; +import com.checkout.handlepaymentsandpayouts.setups.requests.PaymentSetupsRequest; +import com.checkout.handlepaymentsandpayouts.setups.responses.PaymentSetupsConfirmResponse; +import com.checkout.handlepaymentsandpayouts.setups.responses.PaymentSetupsResponse; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import org.junit.jupiter.api.Test; + +import java.util.Collections; + +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; + +/** + * Wire-format tests for the Cash App Pay payment method and the customer device fields, using the + * SDK serializer so the global naming policy is in play. + */ +class CashAppSerializationTest { + + private final GsonSerializer serializer = new GsonSerializer(); + + private static JsonObject parse(final String json) { + return JsonParser.parseString(json).getAsJsonObject(); + } + + private PaymentSetupsRequest buildRequest(final CustomerDeviceClient client, final OsType os) { + final CashApp cashApp = new CashApp(); + cashApp.setInitialization(PaymentMethodInitialization.ENABLED); + cashApp.setCustomerProfileSharing(true); + return PaymentSetupsRequest.builder() + .paymentMethods(PaymentMethods.builder().cashapp(cashApp).build()) + .customer(Customer.builder().device(CustomerDevice.builder() + .locale("en_GB") + .fingerprint("fp_abc123xyz") + .ipv4("203.0.113.0") + .ipv6("2001:db8:85a3::8a2e:370:7334") + .client(client) + .os(os) + .build()).build()) + .build(); + } + + @Test + void shouldSerializeCashAppUnderSingleWordKey() { + final String json = serializer.toJson(buildRequest(CustomerDeviceClient.MOBILE_WEB, OsType.ANDROID)); + final JsonObject methods = parse(json).getAsJsonObject("payment_methods"); + + assertTrue(methods.has("cashapp")); + assertFalse(json.contains("cash_app")); + assertFalse(json.contains("cashApp")); + final JsonObject cashapp = methods.getAsJsonObject("cashapp"); + assertEquals("enabled", cashapp.get("initialization").getAsString()); + assertTrue(cashapp.get("customer_profile_sharing").getAsBoolean()); + assertFalse(cashapp.has("customerProfileSharing")); + } + + @Test + void shouldSerializeCustomerDeviceWireKeysAndEnumValues() { + final JsonObject device = parse(serializer.toJson(buildRequest(CustomerDeviceClient.MOBILE_WEB, OsType.ANDROID))) + .getAsJsonObject("customer").getAsJsonObject("device"); + + assertEquals("en_GB", device.get("locale").getAsString()); + assertEquals("fp_abc123xyz", device.get("fingerprint").getAsString()); + assertEquals("203.0.113.0", device.get("ipv4").getAsString()); + assertEquals("2001:db8:85a3::8a2e:370:7334", device.get("ipv6").getAsString()); + assertEquals("mobile_web", device.get("client").getAsString()); + assertEquals("android", device.get("os").getAsString()); + assertEquals(6, device.size()); + } + + @Test + void shouldSerializeEveryDeviceClientAndOsValueExactly() { + assertEquals("web", deviceField(CustomerDeviceClient.WEB, OsType.IOS, "client")); + assertEquals("mobile_web", deviceField(CustomerDeviceClient.MOBILE_WEB, OsType.IOS, "client")); + assertEquals("app", deviceField(CustomerDeviceClient.APP, OsType.IOS, "client")); + assertEquals("android", deviceField(CustomerDeviceClient.WEB, OsType.ANDROID, "os")); + assertEquals("ios", deviceField(CustomerDeviceClient.WEB, OsType.IOS, "os")); + } + + private String deviceField(final CustomerDeviceClient client, final OsType os, final String field) { + return parse(serializer.toJson(buildRequest(client, os))) + .getAsJsonObject("customer").getAsJsonObject("device").get(field).getAsString(); + } + + @Test + void shouldRoundTripCustomerDeviceEnums() { + for (final CustomerDeviceClient client : CustomerDeviceClient.values()) { + for (final OsType os : OsType.values()) { + final CustomerDevice original = CustomerDevice.builder() + .fingerprint("fp").ipv4("1.2.3.4").ipv6("::1").client(client).os(os).build(); + final CustomerDevice copy = serializer.fromJson(serializer.toJson(original), CustomerDevice.class); + assertEquals(original, copy); + } + } + } + + @Test + void shouldOmitNewDeviceFieldsWhenNotSet() { + final String json = serializer.toJson(CustomerDevice.builder().locale("en_GB").build()); + + assertEquals("{\"locale\":\"en_GB\"}", json); + } + + @Test + void shouldDeserializeFullCashAppResponse() { + final String json = "{" + + "\"id\":\"pst_cashapp\"," + + "\"customer\":{\"device\":{\"client\":\"app\",\"os\":\"ios\",\"ipv4\":\"203.0.113.0\"," + + "\"ipv6\":\"2001:db8::1\",\"fingerprint\":\"fp_1\",\"locale\":\"en_GB\"}}," + + "\"payment_methods\":{\"cashapp\":{" + + "\"status\":\"action_required\"," + + "\"flags\":[]," + + "\"initialization\":\"enabled\"," + + "\"customer_profile_sharing\":true," + + "\"customer_profile\":{" + + "\"customer_id\":\"CST_AYVkuLzfsRqEhf4OyQFxQNv22m7IjNFjO6f2J5CDE2nxAC4-21wJ2H8_2kvsdIsDZMN4\"," + + "\"cashtag\":\"$CASHTAG_C_TOKEN\"," + + "\"reference_id\":\"value\"," + + "\"full_name\":\"John Middle Doe\"," + + "\"given_name\":\"John\"," + + "\"middle_name\":\"Middle\"," + + "\"family_name\":\"Doe\"," + + "\"suffix\":\"Jr.\"," + + "\"birth_date\":\"1990-01-01T00:00:00.0000000\"," + + "\"address\":{" + + "\"address_line_1\":\"123 Main St\"," + + "\"address_line_2\":\"Apt 2\"," + + "\"address_line_3\":\"Floor 3\"," + + "\"locality\":\"Springfield\"," + + "\"sublocality\":\"Downtown\"," + + "\"administrative_district_level_1\":\"IL\"," + + "\"postal_code\":\"62701\"," + + "\"country\":\"US\"}," + + "\"phone_number\":\"5555555555\"," + + "\"email_address\":\"cash@cash.com\"," + + "\"customer_since\":\"1970-01-18T12:46:04.8000000+00:00\"}," + + "\"reference\":\"ORDER-99\"," + + "\"action\":{\"type\":\"redirect\"," + + "\"redirect_url\":\"https://sandbox.api.cash.app/customer-request/v1/requests/GRR_f5xg6wrxhtv3p4w24g0wrexa/interstitial?validity_token=bap03y\"}" + + "}}}"; + + final PaymentSetupsResponse response = serializer.fromJson(json, PaymentSetupsResponse.class); + + assertEquals(CustomerDeviceClient.APP, response.getCustomer().getDevice().getClient()); + assertEquals(OsType.IOS, response.getCustomer().getDevice().getOs()); + assertEquals("203.0.113.0", response.getCustomer().getDevice().getIpv4()); + assertEquals("2001:db8::1", response.getCustomer().getDevice().getIpv6()); + assertEquals("fp_1", response.getCustomer().getDevice().getFingerprint()); + + final CashApp cashApp = response.getPaymentMethods().getCashapp(); + assertNotNull(cashApp); + assertEquals(PaymentMethodStatus.ACTION_REQUIRED, cashApp.getStatus()); + assertEquals(Collections.emptyList(), cashApp.getFlags()); + assertEquals(PaymentMethodInitialization.ENABLED, cashApp.getInitialization()); + assertEquals(Boolean.TRUE, cashApp.getCustomerProfileSharing()); + assertEquals("ORDER-99", cashApp.getReference()); + + assertNotNull(cashApp.getAction()); + assertEquals(CashAppActionType.REDIRECT, cashApp.getAction().getType()); + assertEquals("https://sandbox.api.cash.app/customer-request/v1/requests/GRR_f5xg6wrxhtv3p4w24g0wrexa/interstitial?validity_token=bap03y", + cashApp.getAction().getRedirectUrl()); + + assertNotNull(cashApp.getCustomerProfile()); + assertEquals("CST_AYVkuLzfsRqEhf4OyQFxQNv22m7IjNFjO6f2J5CDE2nxAC4-21wJ2H8_2kvsdIsDZMN4", + cashApp.getCustomerProfile().getCustomerId()); + assertEquals("$CASHTAG_C_TOKEN", cashApp.getCustomerProfile().getCashtag()); + assertEquals("value", cashApp.getCustomerProfile().getReferenceId()); + assertEquals("John Middle Doe", cashApp.getCustomerProfile().getFullName()); + assertEquals("John", cashApp.getCustomerProfile().getGivenName()); + assertEquals("Middle", cashApp.getCustomerProfile().getMiddleName()); + assertEquals("Doe", cashApp.getCustomerProfile().getFamilyName()); + assertEquals("Jr.", cashApp.getCustomerProfile().getSuffix()); + assertEquals("1990-01-01T00:00:00.0000000", cashApp.getCustomerProfile().getBirthDate()); + assertEquals("5555555555", cashApp.getCustomerProfile().getPhoneNumber()); + assertEquals("cash@cash.com", cashApp.getCustomerProfile().getEmailAddress()); + assertEquals("1970-01-18T12:46:04.8000000+00:00", cashApp.getCustomerProfile().getCustomerSince()); + assertNotNull(cashApp.getCustomerProfile().getAddress()); + assertEquals("123 Main St", cashApp.getCustomerProfile().getAddress().getAddressLine1()); + assertEquals("Apt 2", cashApp.getCustomerProfile().getAddress().getAddressLine2()); + assertEquals("Floor 3", cashApp.getCustomerProfile().getAddress().getAddressLine3()); + assertEquals("Springfield", cashApp.getCustomerProfile().getAddress().getLocality()); + assertEquals("Downtown", cashApp.getCustomerProfile().getAddress().getSublocality()); + assertEquals("IL", cashApp.getCustomerProfile().getAddress().getAdministrativeDistrictLevel1()); + assertEquals("62701", cashApp.getCustomerProfile().getAddress().getPostalCode()); + assertEquals(CountryCode.US, cashApp.getCustomerProfile().getAddress().getCountry()); + } + + @Test + void shouldDeserializeCashAppWithoutProfileOnSubsequentResponses() { + final CashApp cashApp = serializer.fromJson("{\"status\":\"ready\",\"initialization\":\"enabled\"," + + "\"customer_profile_sharing\":true,\"reference\":\"ORDER-99\"}", CashApp.class); + + assertEquals(PaymentMethodStatus.READY, cashApp.getStatus()); + assertNull(cashApp.getCustomerProfile()); + assertNull(cashApp.getAction()); + } + + @Test + void shouldLeaveKlarnaActionUnchangedWhenCashAppIsPresent() { + final String json = "{\"klarna\":{\"status\":\"action_required\",\"initialization\":\"enabled\"," + + "\"action\":{\"type\":\"sdk\",\"client_token\":\"ct_1\",\"session_id\":\"sess_1\"}}," + + "\"cashapp\":{\"initialization\":\"enabled\"}}"; + + final PaymentMethods methods = serializer.fromJson(json, PaymentMethods.class); + + assertEquals("sdk", methods.getKlarna().getAction().getType()); + assertEquals("ct_1", methods.getKlarna().getAction().getClientToken()); + assertEquals("sess_1", methods.getKlarna().getAction().getSessionId()); + assertNull(methods.getKlarna().getAction().getOrderId()); + assertNotNull(methods.getCashapp()); + + final Klarna roundTripped = serializer.fromJson(serializer.toJson(methods.getKlarna()), Klarna.class); + assertEquals(methods.getKlarna(), roundTripped); + final JsonObject action = parse(serializer.toJson(methods.getKlarna())).getAsJsonObject("action"); + assertFalse(action.has("redirect_url")); + assertEquals("sdk", action.get("type").getAsString()); + } + + @Test + void shouldRoundTripCashAppWithEveryProperty() { + final CashApp original = new CashApp(); + original.setStatus(PaymentMethodStatus.ACTION_REQUIRED); + original.setFlags(Collections.emptyList()); + original.setInitialization(PaymentMethodInitialization.ENABLED); + original.setCustomerProfileSharing(true); + original.setReference("ORDER-99"); + original.setAction(CashAppAction.builder() + .type(CashAppActionType.REDIRECT) + .redirectUrl("https://sandbox.api.cash.app/customer-request/v1/requests/GRR_f5xg6wrxhtv3p4w24g0wrexa/interstitial?validity_token=bap03y") + .build()); + original.setCustomerProfile(CashAppCustomerProfile.builder() + .customerId("CST_AYVkuLzfsRqEhf4OyQFxQNv22m7IjNFjO6f2J5CDE2nxAC4-21wJ2H8_2kvsdIsDZMN4") + .cashtag("$CASHTAG_C_TOKEN") + .referenceId("value") + .fullName("John Middle Doe") + .givenName("John") + .middleName("Middle") + .familyName("Doe") + .suffix("Jr.") + .birthDate("1990-01-01T00:00:00.0000000") + .address(CashAppAddress.builder() + .addressLine1("123 Main St") + .addressLine2("Apt 2") + .addressLine3("Floor 3") + .locality("Springfield") + .sublocality("Downtown") + .administrativeDistrictLevel1("IL") + .postalCode("62701") + .country(CountryCode.US) + .build()) + .phoneNumber("5555555555") + .emailAddress("cash@cash.com") + .customerSince("1970-01-18T12:46:04.8000000+00:00") + .build()); + + final String json = serializer.toJson(original); + final JsonObject cashapp = parse(json); + final JsonObject profile = cashapp.getAsJsonObject("customer_profile"); + final JsonObject address = profile.getAsJsonObject("address"); + + assertEquals(7, cashapp.size()); + assertEquals("redirect", cashapp.getAsJsonObject("action").get("type").getAsString()); + assertTrue(cashapp.getAsJsonObject("action").has("redirect_url")); + assertEquals(13, profile.size()); + assertTrue(profile.has("customer_id")); + assertTrue(profile.has("reference_id")); + assertTrue(profile.has("customer_since")); + assertEquals(8, address.size()); + assertEquals("123 Main St", address.get("address_line_1").getAsString()); + assertEquals("Apt 2", address.get("address_line_2").getAsString()); + assertEquals("Floor 3", address.get("address_line_3").getAsString()); + assertEquals("IL", address.get("administrative_district_level_1").getAsString()); + assertFalse(address.has("address_line1")); + assertFalse(address.has("administrative_district_level1")); + + assertEquals(original, serializer.fromJson(json, CashApp.class)); + } + + @Test + void shouldDeserializeCashAppInConfirmResponse() { + final String json = "{\"id\":\"pst_cashapp\",\"payment_methods\":{\"cashapp\":{" + + "\"status\":\"action_required\",\"reference\":\"ORDER-99\"," + + "\"action\":{\"type\":\"redirect\"," + + "\"redirect_url\":\"https://sandbox.api.cash.app/customer-request/v1/requests/GRR_f5xg6wrxhtv3p4w24g0wrexa/interstitial?validity_token=bap03y\"}}}}"; + + final PaymentSetupsConfirmResponse response = serializer.fromJson(json, PaymentSetupsConfirmResponse.class); + final CashApp cashApp = response.getPaymentMethods().getCashapp(); + + assertEquals(PaymentMethodStatus.ACTION_REQUIRED, cashApp.getStatus()); + assertEquals("ORDER-99", cashApp.getReference()); + assertEquals(CashAppActionType.REDIRECT, cashApp.getAction().getType()); + assertEquals("https://sandbox.api.cash.app/customer-request/v1/requests/GRR_f5xg6wrxhtv3p4w24g0wrexa/interstitial?validity_token=bap03y", + cashApp.getAction().getRedirectUrl()); + } + + @Test + void shouldDeserializeEverySpecPaymentMethodStatus() { + assertEquals(PaymentMethodStatus.UNAVAILABLE, statusOf("unavailable")); + assertEquals(PaymentMethodStatus.ACTION_REQUIRED, statusOf("action_required")); + assertEquals(PaymentMethodStatus.READY, statusOf("ready")); + assertEquals(PaymentMethodStatus.INITIALIZATION_REQUIRED, statusOf("initialization_required")); + assertEquals(PaymentMethodStatus.INVALID, statusOf("invalid")); + } + + private PaymentMethodStatus statusOf(final String wireValue) { + return serializer.fromJson("{\"status\":\"" + wireValue + "\"}", CashApp.class).getStatus(); + } +} diff --git a/src/test/java/com/checkout/handlepaymentsandpayouts/setups/PaymentMethodsSerializationTest.java b/src/test/java/com/checkout/handlepaymentsandpayouts/setups/PaymentMethodsSerializationTest.java index 64fe518f1..0951d5827 100644 --- a/src/test/java/com/checkout/handlepaymentsandpayouts/setups/PaymentMethodsSerializationTest.java +++ b/src/test/java/com/checkout/handlepaymentsandpayouts/setups/PaymentMethodsSerializationTest.java @@ -474,6 +474,12 @@ void shouldSerializeAllPaymentMethodStatusValues() { case AVAILABLE: assertTrue(json.contains("\"available\"")); break; + case INITIALIZATION_REQUIRED: + assertTrue(json.contains("\"initialization_required\"")); + break; + case INVALID: + assertTrue(json.contains("\"invalid\"")); + break; default: throw new IllegalStateException("Unhandled status: " + status); } diff --git a/src/test/java/com/checkout/handlepaymentsandpayouts/setups/PaymentSetupsClientImplTest.java b/src/test/java/com/checkout/handlepaymentsandpayouts/setups/PaymentSetupsClientImplTest.java index 3fa48fd37..d533a8475 100644 --- a/src/test/java/com/checkout/handlepaymentsandpayouts/setups/PaymentSetupsClientImplTest.java +++ b/src/test/java/com/checkout/handlepaymentsandpayouts/setups/PaymentSetupsClientImplTest.java @@ -108,4 +108,19 @@ void shouldConfirmPaymentSetup() throws ExecutionException, InterruptedException assertNotNull(future.get()); assertEquals(response, future.get()); } + + @Test + void shouldConfirmPaymentSetupWithCashApp() throws ExecutionException, InterruptedException { + final String paymentSetupId = "ps_test_123456789"; + final PaymentSetupsConfirmResponse response = mock(PaymentSetupsConfirmResponse.class); + + when(apiClient.postAsync(eq("payments/setups/" + paymentSetupId + "/confirm/cashapp"), + eq(authorization), eq(PaymentSetupsConfirmResponse.class), isNull(), isNull())) + .thenReturn(CompletableFuture.completedFuture(response)); + + final CompletableFuture future = + client.confirmPaymentSetup(paymentSetupId, "cashapp"); + + assertEquals(response, future.get()); + } } \ No newline at end of file diff --git a/src/test/java/com/checkout/handlepaymentsandpayouts/setups/PaymentSetupsNewFieldsSerializationTest.java b/src/test/java/com/checkout/handlepaymentsandpayouts/setups/PaymentSetupsNewFieldsSerializationTest.java index 9a5224522..ddc16477d 100644 --- a/src/test/java/com/checkout/handlepaymentsandpayouts/setups/PaymentSetupsNewFieldsSerializationTest.java +++ b/src/test/java/com/checkout/handlepaymentsandpayouts/setups/PaymentSetupsNewFieldsSerializationTest.java @@ -3,7 +3,13 @@ import com.checkout.GsonSerializer; import com.checkout.common.CountryCode; import com.checkout.common.Currency; +import com.checkout.common.Phone; import com.checkout.handlepaymentsandpayouts.setups.entities.billingDescriptor.PaymentSetupBillingDescriptor; +import com.checkout.handlepaymentsandpayouts.setups.entities.customer.Customer; +import com.checkout.handlepaymentsandpayouts.setups.entities.customer.CustomerDevice; +import com.checkout.handlepaymentsandpayouts.setups.entities.customer.CustomerDeviceClient; +import com.checkout.handlepaymentsandpayouts.setups.entities.customer.CustomerEmail; +import com.checkout.handlepaymentsandpayouts.setups.entities.customer.MerchantAccount; import com.checkout.handlepaymentsandpayouts.setups.entities.order.AmountAllocationCommission; import com.checkout.handlepaymentsandpayouts.setups.entities.order.PaymentSetupAmountAllocation; import com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.PaymentMethods; @@ -12,15 +18,21 @@ import com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.bacs.BacsAccountHolderType; import com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.cardpresent.CardPresent; import com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.cardpresent.CardPresentPin; +import com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.common.OsType; import com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.klarna.Klarna; import com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.klarna.KlarnaAccountHolder; import com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.paybybank.PayByBank; import com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.stablecoin.Stablecoin; import com.checkout.handlepaymentsandpayouts.setups.entities.presentmentDetails.PaymentSetupPresentmentDetails; import com.checkout.handlepaymentsandpayouts.setups.entities.terminal.PaymentSetupTerminal; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; 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.assertTrue; @@ -131,4 +143,71 @@ void shouldSerializeOrderAmountFields() { assertTrue(json.contains("\"tax_amount\"")); assertTrue(json.contains("\"tipping_amount\"")); } + + // ------------------------------------------------------------------------ + // Customer + // PaymentSetup.customer: all 8 properties, including id, country and + // tax_number, which the model did not carry before. + // ------------------------------------------------------------------------ + + @Test + void shouldRoundTripCustomerWithEveryProperty() { + final Customer original = Customer.builder() + .id("cus_123456789") + .country(CountryCode.GB) + .taxNumber("GB123456789") + .name("John Smith") + .email(CustomerEmail.builder().address("johnsmith@example.com").verified(true).build()) + .phone(Phone.builder().countryCode("+44").number("207 946 0000").build()) + .device(CustomerDevice.builder() + .locale("en_GB") + .fingerprint("fp_abc123xyz") + .ipv4("203.0.113.0") + .ipv6("2001:db8:85a3::8a2e:370:7334") + .client(CustomerDeviceClient.WEB) + .os(OsType.ANDROID) + .build()) + .merchantAccount(MerchantAccount.builder() + .id("acc_123") + .registrationDate(LocalDate.of(2023, 1, 15)) + .lastModified(LocalDate.of(2024, 3, 10)) + .returningCustomer(true) + .firstTransactionDate(LocalDate.of(2023, 2, 20)) + .lastTransactionDate(LocalDate.of(2024, 3, 9)) + .totalOrderCount(5) + .lastPaymentAmount(1000L) + .build()) + .build(); + + final String json = serializer.toJson(original); + final JsonObject customer = JsonParser.parseString(json).getAsJsonObject(); + + assertEquals(8, customer.size()); + assertEquals("cus_123456789", customer.get("id").getAsString()); + assertEquals("GB", customer.get("country").getAsString()); + assertEquals("GB123456789", customer.get("tax_number").getAsString()); + assertFalse(customer.has("taxNumber")); + assertEquals(original, serializer.fromJson(json, Customer.class)); + } + + @Test + void shouldDeserializeCustomerSwaggerExample() { + final String json = "{\"country\":\"GB\",\"id\":\"cus_123456789\"," + + "\"email\":{\"address\":\"johnsmith@example.com\",\"verified\":true}," + + "\"name\":\"John Smith\",\"tax_number\":\"GB123456789\"," + + "\"phone\":{\"country_code\":\"+44\",\"number\":\"207 946 0000\"}," + + "\"device\":{\"locale\":\"en_GB\"}}"; + + final Customer customer = serializer.fromJson(json, Customer.class); + + assertEquals(CountryCode.GB, customer.getCountry()); + assertEquals("cus_123456789", customer.getId()); + assertEquals("GB123456789", customer.getTaxNumber()); + assertEquals("John Smith", customer.getName()); + assertEquals("johnsmith@example.com", customer.getEmail().getAddress()); + assertEquals(Boolean.TRUE, customer.getEmail().getVerified()); + assertEquals("+44", customer.getPhone().getCountryCode()); + assertEquals("207 946 0000", customer.getPhone().getNumber()); + assertEquals("en_GB", customer.getDevice().getLocale()); + } } diff --git a/src/test/java/com/checkout/handlepaymentsandpayouts/setups/PaymentSetupsTestIT.java b/src/test/java/com/checkout/handlepaymentsandpayouts/setups/PaymentSetupsTestIT.java index ffd7cf9e0..397fc24ca 100644 --- a/src/test/java/com/checkout/handlepaymentsandpayouts/setups/PaymentSetupsTestIT.java +++ b/src/test/java/com/checkout/handlepaymentsandpayouts/setups/PaymentSetupsTestIT.java @@ -1,13 +1,18 @@ package com.checkout.handlepaymentsandpayouts.setups; +import com.checkout.GsonSerializer; import com.checkout.PlatformType; import com.checkout.SandboxTestFixture; +import com.checkout.common.CountryCode; import com.checkout.common.Currency; import com.checkout.common.Phone; import com.checkout.handlepaymentsandpayouts.setups.entities.customer.Customer; import com.checkout.handlepaymentsandpayouts.setups.entities.customer.CustomerDevice; +import com.checkout.handlepaymentsandpayouts.setups.entities.customer.CustomerDeviceClient; import com.checkout.handlepaymentsandpayouts.setups.entities.customer.CustomerEmail; import com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.PaymentMethods; +import com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.cashapp.CashApp; +import com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.common.OsType; import com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.common.PaymentMethodInitialization; import com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.klarna.Klarna; import com.checkout.handlepaymentsandpayouts.setups.entities.settings.Settings; @@ -15,14 +20,21 @@ import com.checkout.handlepaymentsandpayouts.setups.requests.PaymentSetupsRequest; import com.checkout.handlepaymentsandpayouts.setups.responses.PaymentSetupsConfirmResponse; import com.checkout.handlepaymentsandpayouts.setups.responses.PaymentSetupsResponse; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import java.util.Map; import java.util.UUID; import java.util.concurrent.CompletableFuture; 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.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeTrue; class PaymentSetupsTestIT extends SandboxTestFixture { @@ -121,6 +133,95 @@ void confirmPaymentSetup_ShouldReturnValidResponse() { assertEquals(paymentSetupsRequest.getCurrency(), response.getCurrency()); } + @Test + void createPaymentSetupWithDeviceDetails_ShouldEchoDeviceAndReadEveryStatus() { + // Arrange + final PaymentSetupsRequest request = createCashAppPaymentSetupsRequest(); + + // Act + final PaymentSetupsResponse created = + checkoutApi.paymentSetupsClient().createPaymentSetup(request).join(); + final PaymentSetupsResponse fetched = + checkoutApi.paymentSetupsClient().getPaymentSetup(created.getId()).join(); + + // Assert + final CustomerDevice device = fetched.getCustomer().getDevice(); + assertEquals("en_US", device.getLocale()); + assertEquals("fp_abc123xyz", device.getFingerprint()); + assertEquals("203.0.113.0", device.getIpv4()); + assertEquals(CustomerDeviceClient.WEB, device.getClient()); + assertEquals(OsType.IOS, device.getOs()); + + // A status value the SDK does not model is read as null and then dropped on write, + // so every payment method the API returned must still carry its status here. + final JsonObject methods = JsonParser.parseString(new GsonSerializer().toJson(fetched.getPaymentMethods())) + .getAsJsonObject(); + assertFalse(methods.entrySet().isEmpty()); + for (final Map.Entry method : methods.entrySet()) { + assertTrue(method.getValue().getAsJsonObject().has("status"), method.getKey() + " lost its status"); + } + } + + @Test + void createPaymentSetupWithCustomerIdentifiers_ShouldEchoThem() { + // Arrange + final PaymentSetupsRequest request = createValidPaymentSetupsRequest(); + request.getCustomer().setId("cus_123456789"); + request.getCustomer().setCountry(CountryCode.GB); + request.getCustomer().setTaxNumber("GB123456789"); + + // Act + final PaymentSetupsResponse created = + checkoutApi.paymentSetupsClient().createPaymentSetup(request).join(); + final PaymentSetupsResponse fetched = + checkoutApi.paymentSetupsClient().getPaymentSetup(created.getId()).join(); + + // Assert + assertEquals("cus_123456789", fetched.getCustomer().getId()); + assertEquals(CountryCode.GB, fetched.getCustomer().getCountry()); + assertEquals("GB123456789", fetched.getCustomer().getTaxNumber()); + } + + @Test + void createPaymentSetupWithCashApp_ShouldReturnCashAppDetails() { + // Arrange + final PaymentSetupsRequest request = createCashAppPaymentSetupsRequest(); + + // Act + final PaymentSetupsResponse created = + checkoutApi.paymentSetupsClient().createPaymentSetup(request).join(); + assumeTrue(created.getAvailablePaymentMethods() != null + && created.getAvailablePaymentMethods().contains("cashapp"), + "Cash App Pay is not enabled on the sandbox processing channel"); + final PaymentSetupsResponse fetched = + checkoutApi.paymentSetupsClient().getPaymentSetup(created.getId()).join(); + + // Assert + final CashApp cashApp = fetched.getPaymentMethods().getCashapp(); + assertNotNull(cashApp); + assertNotNull(cashApp.getStatus()); + assertEquals(PaymentMethodInitialization.ENABLED, cashApp.getInitialization()); + assertEquals(Boolean.TRUE, cashApp.getCustomerProfileSharing()); + } + + private PaymentSetupsRequest createCashAppPaymentSetupsRequest() { + final CashApp cashApp = new CashApp(); + cashApp.setInitialization(PaymentMethodInitialization.ENABLED); + cashApp.setCustomerProfileSharing(true); + + final PaymentSetupsRequest request = createValidPaymentSetupsRequest(); + request.setCurrency(Currency.USD); + request.setPaymentMethods(PaymentMethods.builder().cashapp(cashApp).build()); + request.getCustomer().setDevice(CustomerDevice.builder() + .locale("en_US") + .fingerprint("fp_abc123xyz") + .ipv4("203.0.113.0") + .client(CustomerDeviceClient.WEB) + .os(OsType.IOS) + .build()); + return request; + } + private PaymentSetupsRequest createValidPaymentSetupsRequest() { final Klarna klarna = new Klarna(); klarna.setInitialization(PaymentMethodInitialization.DISABLED);