diff --git a/src/main/java/com/checkout/ApacheHttpClientTransport.java b/src/main/java/com/checkout/ApacheHttpClientTransport.java index 588fca2e2..c36a7a94d 100644 --- a/src/main/java/com/checkout/ApacheHttpClientTransport.java +++ b/src/main/java/com/checkout/ApacheHttpClientTransport.java @@ -152,7 +152,7 @@ public Response invokeSync(final ClientOperation clientOperation, public Response submitFileSync(final String path, final SdkAuthorization authorization, final AbstractFileRequest fileRequest) { final HttpPost request = new HttpPost(getRequestUrl(path)); request.setEntity(getMultipartFileEntity(fileRequest)); - + final Supplier callSupplier = () -> performCall(authorization, null, request, POST); return executeWithResilience4j(callSupplier); } diff --git a/src/main/java/com/checkout/CheckoutApi.java b/src/main/java/com/checkout/CheckoutApi.java index 0ec65e95c..6340fcfde 100644 --- a/src/main/java/com/checkout/CheckoutApi.java +++ b/src/main/java/com/checkout/CheckoutApi.java @@ -22,6 +22,7 @@ import com.checkout.issuing.IssuingClient; import com.checkout.metadata.MetadataClient; import com.checkout.networktokens.NetworkTokensClient; +import com.checkout.onboardingsimulator.OnboardingSimulatorClient; import com.checkout.paymentmethods.PaymentMethodsClient; import com.checkout.payments.PaymentsClient; import com.checkout.payments.contexts.PaymentContextsClient; @@ -109,4 +110,9 @@ public interface CheckoutApi extends CheckoutApmApi { */ AgenticCommerceClient agenticCommerceClient(); + /** + * Returns the client for the Onboarding Simulator (Sandbox only). + */ + OnboardingSimulatorClient onboardingSimulatorClient(); + } diff --git a/src/main/java/com/checkout/CheckoutApiImpl.java b/src/main/java/com/checkout/CheckoutApiImpl.java index 6ff3f5760..7678ee1fa 100644 --- a/src/main/java/com/checkout/CheckoutApiImpl.java +++ b/src/main/java/com/checkout/CheckoutApiImpl.java @@ -44,6 +44,8 @@ import com.checkout.metadata.MetadataClientImpl; import com.checkout.networktokens.NetworkTokensClient; import com.checkout.networktokens.NetworkTokensClientImpl; +import com.checkout.onboardingsimulator.OnboardingSimulatorClient; +import com.checkout.onboardingsimulator.OnboardingSimulatorClientImpl; import com.checkout.paymentmethods.PaymentMethodsClient; import com.checkout.paymentmethods.PaymentMethodsClientImpl; import com.checkout.payments.PaymentsClient; @@ -107,6 +109,7 @@ public class CheckoutApiImpl extends AbstractCheckoutApmApi implements CheckoutA private final NetworkTokensClient networkTokensClient; private final StandaloneAccountUpdaterClient standaloneAccountUpdaterClient; private final AgenticCommerceClient agenticCommerceClient; + private final OnboardingSimulatorClient onboardingSimulatorClient; public CheckoutApiImpl(final CheckoutConfiguration configuration) { super(configuration); @@ -146,6 +149,7 @@ public CheckoutApiImpl(final CheckoutConfiguration configuration) { this.networkTokensClient = new NetworkTokensClientImpl(this.apiClient, configuration); this.standaloneAccountUpdaterClient = new StandaloneAccountUpdaterClientImpl(this.apiClient, configuration); this.agenticCommerceClient = new AgenticCommerceClientImpl(this.apiClient, configuration); + this.onboardingSimulatorClient = new OnboardingSimulatorClientImpl(this.apiClient, configuration); } @Override @@ -284,6 +288,9 @@ public MetadataClient metadataClient() { @Override public AgenticCommerceClient agenticCommerceClient() { return agenticCommerceClient; } + @Override + public OnboardingSimulatorClient onboardingSimulatorClient() { return onboardingSimulatorClient; } + private ApiClient getFilesClient(final CheckoutConfiguration configuration) { return new ApiClientImpl(configuration, new FilesApiUriStrategy(configuration)); } diff --git a/src/main/java/com/checkout/GsonSerializer.java b/src/main/java/com/checkout/GsonSerializer.java index f40577af2..e5e6bb882 100644 --- a/src/main/java/com/checkout/GsonSerializer.java +++ b/src/main/java/com/checkout/GsonSerializer.java @@ -93,6 +93,10 @@ public final class GsonSerializer implements Serializer { }.getType(); private static final Type PAYMENT_ACTIONS_TYPE = new TypeToken>() { }.getType(); + private static final Type SIMULATOR_AVAILABLE_REQUIREMENTS_TYPE = new TypeToken>() { + }.getType(); + private static final Type SIMULATOR_SCENARIOS_TYPE = new TypeToken>() { + }.getType(); private static final Gson DEFAULT_GSON = new GsonBuilder() .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) @@ -118,6 +122,7 @@ public final class GsonSerializer implements Serializer { .registerSubtype(com.checkout.handlepaymentsandpayouts.payments.common.source.almasource.AlmaSource.class, identifier(com.checkout.handlepaymentsandpayouts.payments.common.source.SourceType.ALMA)) .registerSubtype(com.checkout.handlepaymentsandpayouts.payments.common.source.bancontactsource.BancontactSource.class, identifier(com.checkout.handlepaymentsandpayouts.payments.common.source.SourceType.BANCONTACT)) .registerSubtype(com.checkout.handlepaymentsandpayouts.payments.common.source.benefitsource.BenefitSource.class, identifier(com.checkout.handlepaymentsandpayouts.payments.common.source.SourceType.BENEFIT)) + .registerSubtype(com.checkout.handlepaymentsandpayouts.payments.common.source.bliksource.BlikSource.class, identifier(com.checkout.handlepaymentsandpayouts.payments.common.source.SourceType.BLIK)) .registerSubtype(com.checkout.handlepaymentsandpayouts.payments.common.source.currencyaccountsource.CurrencyAccountSource.class, identifier(com.checkout.handlepaymentsandpayouts.payments.common.source.SourceType.CURRENCY_ACCOUNT)) .registerSubtype(com.checkout.handlepaymentsandpayouts.payments.common.source.cvconnectsource.CvconnectSource.class, identifier(com.checkout.handlepaymentsandpayouts.payments.common.source.SourceType.CVCONNECT)) .registerSubtype(com.checkout.handlepaymentsandpayouts.payments.common.source.danasource.DanaSource.class, identifier(com.checkout.handlepaymentsandpayouts.payments.common.source.SourceType.DANA)) @@ -233,6 +238,8 @@ public final class GsonSerializer implements Serializer { .registerTypeAdapter(WEBHOOKS_TYPE, webhooksResponseDeserializer()) .registerTypeAdapter(PREVIOUS_PAYMENT_ACTIONS_TYPE, paymentActionsResponsePreviousDeserializer()) .registerTypeAdapter(PAYMENT_ACTIONS_TYPE, paymentActionsResponseDeserializer()) + .registerTypeAdapter(SIMULATOR_AVAILABLE_REQUIREMENTS_TYPE, simulatorAvailableRequirementsResponseDeserializer()) + .registerTypeAdapter(SIMULATOR_SCENARIOS_TYPE, simulatorScenariosResponseDeserializer()) .registerTypeAdapter(ProductResponse.class, getProductDeserializer()) .create(); @@ -344,6 +351,26 @@ private static JsonDeserializer> simulatorAvailableRequirementsResponseDeserializer() { + return (json, typeOfT, context) -> { + final ItemsResponse response = new ItemsResponse<>(); + if (json.isJsonArray()) { + response.setItems(deserializeJsonArray(json, com.checkout.onboardingsimulator.entities.SimulatorAvailableRequirement.class)); + } + return response; + }; + } + + private static JsonDeserializer> simulatorScenariosResponseDeserializer() { + return (json, typeOfT, context) -> { + final ItemsResponse response = new ItemsResponse<>(); + if (json.isJsonArray()) { + response.setItems(deserializeJsonArray(json, com.checkout.onboardingsimulator.entities.SimulatorScenario.class)); + } + return response; + }; + } + private static List deserializeJsonArray(final JsonElement json, final Class itemsType) { final JsonArray jsonArray = json.getAsJsonArray(); return IntStream diff --git a/src/main/java/com/checkout/accounts/AccountsClient.java b/src/main/java/com/checkout/accounts/AccountsClient.java index 96c79d3d6..4854d5d76 100644 --- a/src/main/java/com/checkout/accounts/AccountsClient.java +++ b/src/main/java/com/checkout/accounts/AccountsClient.java @@ -63,6 +63,33 @@ CompletableFuture updateReserveRule(String entityId, CompletableFuture getReserveRules(String entityId); + /** + * Retrieves the list of pending requirements that the sub-entity must resolve. + * + * @param entityId The sub-entity ID. + * @return a {@link CompletableFuture} resolving to the requirements list. + */ + CompletableFuture getEntityRequirements(String entityId); + + /** + * Retrieves detailed information for a single requirement. + * + * @param entityId The sub-entity ID. + * @param requirementId The requirement ID. + * @return a {@link CompletableFuture} resolving to the requirement details. + */ + CompletableFuture getEntityRequirementDetails(String entityId, String requirementId); + + /** + * Submits a response to resolve a requirement. + * + * @param entityId The sub-entity ID. + * @param requirementId The requirement ID. + * @param updateRequest The response payload. + * @return a {@link CompletableFuture} resolving to the resolve response. + */ + CompletableFuture resolveEntityRequirement(String entityId, String requirementId, EntityRequirementUpdateRequest updateRequest); + // Synchronous methods IdResponse submitFileSync(final AccountsFileRequest accountsFileRequest); @@ -103,4 +130,19 @@ IdResponse updatePaymentInstrumentDetailsSync(final String entityId, ReserveRulesResponse getReserveRulesSync(final String entityId); + /** + * Synchronous variant of {@link #getEntityRequirements(String)}. + */ + EntityRequirementListResponse getEntityRequirementsSync(final String entityId); + + /** + * Synchronous variant of {@link #getEntityRequirementDetails(String, String)}. + */ + EntityRequirementDetailsResponse getEntityRequirementDetailsSync(final String entityId, final String requirementId); + + /** + * Synchronous variant of {@link #resolveEntityRequirement(String, String, EntityRequirementUpdateRequest)}. + */ + EntityRequirementUpdateResponse resolveEntityRequirementSync(final String entityId, final String requirementId, final EntityRequirementUpdateRequest updateRequest); + } diff --git a/src/main/java/com/checkout/accounts/AccountsClientImpl.java b/src/main/java/com/checkout/accounts/AccountsClientImpl.java index 92e3ee034..b42d43c18 100644 --- a/src/main/java/com/checkout/accounts/AccountsClientImpl.java +++ b/src/main/java/com/checkout/accounts/AccountsClientImpl.java @@ -34,6 +34,7 @@ public class AccountsClientImpl extends AbstractClient implements AccountsClient private static final String PAYMENT_INSTRUMENTS_PATH = "payment-instruments"; private static final String MEMBERS_PATH = "members"; private static final String RESERVE_RULES_PATH = "reserve-rules"; + private static final String REQUIREMENTS_PATH = "requirements"; private final ApiClient filesClient; @@ -240,6 +241,34 @@ public CompletableFuture getReserveRules(final String enti ReserveRulesResponse.class); } + @Override + public CompletableFuture getEntityRequirements(final String entityId) { + validateEntityId(entityId); + return apiClient.getAsync( + buildPath(ACCOUNTS_PATH, ENTITIES_PATH, entityId, REQUIREMENTS_PATH), + sdkAuthorization(), + EntityRequirementListResponse.class); + } + + @Override + public CompletableFuture getEntityRequirementDetails(final String entityId, final String requirementId) { + validateParams("entityId", entityId, "requirementId", requirementId); + return apiClient.getAsync( + buildPath(ACCOUNTS_PATH, ENTITIES_PATH, entityId, REQUIREMENTS_PATH, requirementId), + sdkAuthorization(), + EntityRequirementDetailsResponse.class); + } + + @Override + public CompletableFuture resolveEntityRequirement(final String entityId, final String requirementId, final EntityRequirementUpdateRequest updateRequest) { + validateParams("entityId", entityId, "requirementId", requirementId, "updateRequest", updateRequest); + return apiClient.putAsync( + buildPath(ACCOUNTS_PATH, ENTITIES_PATH, entityId, REQUIREMENTS_PATH, requirementId), + sdkAuthorization(), + EntityRequirementUpdateResponse.class, + updateRequest); + } + // Synchronous methods @Override public IdResponse submitFileSync(final AccountsFileRequest accountsFileRequest) { @@ -434,6 +463,34 @@ public ReserveRulesResponse getReserveRulesSync(final String entityId) { ReserveRulesResponse.class); } + @Override + public EntityRequirementListResponse getEntityRequirementsSync(final String entityId) { + validateEntityId(entityId); + return apiClient.get( + buildPath(ACCOUNTS_PATH, ENTITIES_PATH, entityId, REQUIREMENTS_PATH), + sdkAuthorization(), + EntityRequirementListResponse.class); + } + + @Override + public EntityRequirementDetailsResponse getEntityRequirementDetailsSync(final String entityId, final String requirementId) { + validateParams("entityId", entityId, "requirementId", requirementId); + return apiClient.get( + buildPath(ACCOUNTS_PATH, ENTITIES_PATH, entityId, REQUIREMENTS_PATH, requirementId), + sdkAuthorization(), + EntityRequirementDetailsResponse.class); + } + + @Override + public EntityRequirementUpdateResponse resolveEntityRequirementSync(final String entityId, final String requirementId, final EntityRequirementUpdateRequest updateRequest) { + validateParams("entityId", entityId, "requirementId", requirementId, "updateRequest", updateRequest); + return apiClient.put( + buildPath(ACCOUNTS_PATH, ENTITIES_PATH, entityId, REQUIREMENTS_PATH, requirementId), + sdkAuthorization(), + EntityRequirementUpdateResponse.class, + updateRequest); + } + // Common methods private Map buildScheduleRequestMap(final Currency currency, final UpdateScheduleRequest updateScheduleRequest) { final Map request = new EnumMap<>(Currency.class); diff --git a/src/main/java/com/checkout/accounts/EntityRequirementDetailsResponse.java b/src/main/java/com/checkout/accounts/EntityRequirementDetailsResponse.java new file mode 100644 index 000000000..621afab5a --- /dev/null +++ b/src/main/java/com/checkout/accounts/EntityRequirementDetailsResponse.java @@ -0,0 +1,35 @@ +package com.checkout.accounts; + +import com.google.gson.annotations.SerializedName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.util.Map; + +/** + * Detailed information about a single requirement. + */ +@Data +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public final class EntityRequirementDetailsResponse extends EntityRequirementListItem { + + /** + * A user-facing explanation of what is needed to resolve the requirement. + * [Optional] + */ + private String message; + + /** + * JSON Schema that the value supplied to PUT /accounts/entities/{id}/requirements/{requirementId} + * must conform to. The shape varies by requirement type (for example: an identity document upload, + * a free-text response, or a structured object). May be {@code null} if no schema is registered + * for the requirement's field. + * [Optional] + */ + @SerializedName("_schema") + private Map schema; +} diff --git a/src/main/java/com/checkout/accounts/EntityRequirementListItem.java b/src/main/java/com/checkout/accounts/EntityRequirementListItem.java new file mode 100644 index 000000000..28f6212ab --- /dev/null +++ b/src/main/java/com/checkout/accounts/EntityRequirementListItem.java @@ -0,0 +1,96 @@ +package com.checkout.accounts; + +import com.checkout.common.Resource; +import com.google.gson.annotations.SerializedName; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.time.Instant; +import java.util.Map; + +/** + * A pending requirement that the sub-entity must resolve to remain compliant or unlock capabilities. + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class EntityRequirementListItem extends Resource { + + /** + * The unique identifier of the requirement. + * [Optional] + */ + private String id; + + /** + * The ID of the resource (sub-entity or representative) the requirement applies to. + * [Optional] + */ + private String resource; + + /** + * The type of resource the requirement applies to derived from the resource's URN. + * Common values include {@code company}, {@code individual}, and {@code representative}. + * Defaults to {@code entity} if the URN cannot be parsed. + * [Optional] + */ + @SerializedName("resource_type") + private String resourceType; + + /** + * The reason the requirement was raised. + * [Optional] + * Enum: "periodic_review" "attestation" + */ + private EntityRequirementReason reason; + + /** + * Priority level of this requirement. {@code high} by default, {@code critical} if deadline + * is within 7 days. + * [Optional] + * Enum: "high" "critical" + */ + private EntityRequirementPriority priority; + + /** + * The date and time, in ISO 8601 UTC format, by which the requirement must be resolved. + * [Optional] + * Format: date-time (RFC 3339) + */ + private Instant deadline; + + /** + * The schema-registry URN identifying the field this requirement applies to. + * Format: {@code urn:object:{resource_type}#{resource_id}#field:{field_name}}. + * [Optional] + */ + private String urn; + + /** + * Dot-notation path of the field on the resource that needs to be supplied or updated. + * May be {@code null} for ad-hoc requirements (such as additional documents or free-text + * responses) that do not map to a specific field on the entity. + * [Optional] + */ + @SerializedName("field_path") + private String fieldPath; + + /** + * The schema-registry URN of the field, from the public mapping definition. + * May be {@code null} if no mapping exists. + * [Optional] + */ + @SerializedName("field_urn") + private String fieldUrn; + + /** + * Optional metadata from the property mapping definition. Shape varies by requirement. + * [Optional] + */ + private Map metadata; +} diff --git a/src/main/java/com/checkout/accounts/EntityRequirementListResponse.java b/src/main/java/com/checkout/accounts/EntityRequirementListResponse.java new file mode 100644 index 000000000..4b5de99dd --- /dev/null +++ b/src/main/java/com/checkout/accounts/EntityRequirementListResponse.java @@ -0,0 +1,27 @@ +package com.checkout.accounts; + +import com.checkout.common.Resource; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.util.List; + +/** + * The list of pending requirements for a sub-entity. + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public final class EntityRequirementListResponse extends Resource { + + /** + * The list of pending requirements for the sub-entity. Empty when no requirements are outstanding. + * [Optional] + */ + private List data; +} diff --git a/src/main/java/com/checkout/accounts/EntityRequirementPriority.java b/src/main/java/com/checkout/accounts/EntityRequirementPriority.java new file mode 100644 index 000000000..f40f16025 --- /dev/null +++ b/src/main/java/com/checkout/accounts/EntityRequirementPriority.java @@ -0,0 +1,12 @@ +package com.checkout.accounts; + +import com.google.gson.annotations.SerializedName; + +public enum EntityRequirementPriority { + + @SerializedName("high") + HIGH, + + @SerializedName("critical") + CRITICAL +} diff --git a/src/main/java/com/checkout/accounts/EntityRequirementReason.java b/src/main/java/com/checkout/accounts/EntityRequirementReason.java new file mode 100644 index 000000000..ab856be76 --- /dev/null +++ b/src/main/java/com/checkout/accounts/EntityRequirementReason.java @@ -0,0 +1,12 @@ +package com.checkout.accounts; + +import com.google.gson.annotations.SerializedName; + +public enum EntityRequirementReason { + + @SerializedName("periodic_review") + PERIODIC_REVIEW, + + @SerializedName("attestation") + ATTESTATION +} diff --git a/src/main/java/com/checkout/accounts/EntityRequirementUpdateRequest.java b/src/main/java/com/checkout/accounts/EntityRequirementUpdateRequest.java new file mode 100644 index 000000000..855092cea --- /dev/null +++ b/src/main/java/com/checkout/accounts/EntityRequirementUpdateRequest.java @@ -0,0 +1,25 @@ +package com.checkout.accounts; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Request body used to resolve a requirement. The shape of {@code value} is defined by the + * requirement's {@code _schema} (returned from GET /accounts/entities/{id}/requirements/{requirementId}). + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public final class EntityRequirementUpdateRequest { + + /** + * The response to the requirement. The expected shape depends on the requirement and is defined + * by the JSON Schema returned in the requirement details response. Common shapes include a file + * reference (for document uploads), a primitive value, or a structured object. + * [Required] + */ + private Object value; +} diff --git a/src/main/java/com/checkout/accounts/EntityRequirementUpdateResponse.java b/src/main/java/com/checkout/accounts/EntityRequirementUpdateResponse.java new file mode 100644 index 000000000..d51e4df13 --- /dev/null +++ b/src/main/java/com/checkout/accounts/EntityRequirementUpdateResponse.java @@ -0,0 +1,43 @@ +package com.checkout.accounts; + +import com.checkout.common.Resource; +import com.google.gson.annotations.SerializedName; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.time.Instant; + +/** + * Acknowledges that a requirement response has been accepted for processing. + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public final class EntityRequirementUpdateResponse extends Resource { + + /** + * The unique identifier of the requirement. + * [Optional] + */ + private String id; + + /** + * Processing status of the submitted response. + * [Optional] + * Enum: "processing" + */ + private EntityRequirementUpdateStatus status; + + /** + * The date and time, in ISO 8601 UTC format, the response was accepted. + * [Optional] + * Format: date-time (RFC 3339) + */ + @SerializedName("submitted_at") + private Instant submittedAt; +} diff --git a/src/main/java/com/checkout/accounts/EntityRequirementUpdateStatus.java b/src/main/java/com/checkout/accounts/EntityRequirementUpdateStatus.java new file mode 100644 index 000000000..78453daaa --- /dev/null +++ b/src/main/java/com/checkout/accounts/EntityRequirementUpdateStatus.java @@ -0,0 +1,9 @@ +package com.checkout.accounts; + +import com.google.gson.annotations.SerializedName; + +public enum EntityRequirementUpdateStatus { + + @SerializedName("processing") + PROCESSING +} diff --git a/src/main/java/com/checkout/accounts/OnboardEntityRequest.java b/src/main/java/com/checkout/accounts/OnboardEntityRequest.java index 458d0b570..381c823eb 100644 --- a/src/main/java/com/checkout/accounts/OnboardEntityRequest.java +++ b/src/main/java/com/checkout/accounts/OnboardEntityRequest.java @@ -33,4 +33,21 @@ public final class OnboardEntityRequest { @SerializedName("additional_info") private AdditionalInfo additionalInfo; + /** + * Identifier of a seller category configured on your platform during onboarding. + * Categories define the pricing, capabilities, and risk profile applied to sub-entities; + * ask your Checkout.com contact for the list available to your platform. + * Used for US ISV onboarding variants. + * [Optional] + */ + @SerializedName("seller_category") + private String sellerCategory; + + /** + * Captures evidence of the end-user's consent to onboarding. + * Used for US ISV onboarding variants. + * [Optional] + */ + private Submitter submitter; + } diff --git a/src/main/java/com/checkout/accounts/Submitter.java b/src/main/java/com/checkout/accounts/Submitter.java new file mode 100644 index 000000000..d31d6c571 --- /dev/null +++ b/src/main/java/com/checkout/accounts/Submitter.java @@ -0,0 +1,24 @@ +package com.checkout.accounts; + +import com.google.gson.annotations.SerializedName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Captured as evidence of the end-user's consent to onboarding. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public final class Submitter { + + /** + * IP address of the end-user (the sub-entity's representative) submitting the onboarding request. + * [Required] + */ + @SerializedName("ip_address") + private String ipAddress; +} diff --git a/src/main/java/com/checkout/balances/Balances.java b/src/main/java/com/checkout/balances/Balances.java index d1fa087e0..eaaa235a8 100644 --- a/src/main/java/com/checkout/balances/Balances.java +++ b/src/main/java/com/checkout/balances/Balances.java @@ -1,5 +1,6 @@ package com.checkout.balances; +import com.google.gson.annotations.SerializedName; import lombok.Data; @Data @@ -13,4 +14,11 @@ public final class Balances { private Long collateral; + /** + * A breakdown of the funds held in the {@code collateral} balance. + * [Optional] + */ + @SerializedName("collateral_breakdown") + private CollateralBreakdown collateralBreakdown; + } diff --git a/src/main/java/com/checkout/balances/BalancesQuery.java b/src/main/java/com/checkout/balances/BalancesQuery.java index 0fe419406..36bef43c6 100644 --- a/src/main/java/com/checkout/balances/BalancesQuery.java +++ b/src/main/java/com/checkout/balances/BalancesQuery.java @@ -1,10 +1,13 @@ package com.checkout.balances; +import com.google.gson.annotations.SerializedName; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; +import java.time.Instant; + @Data @Builder @AllArgsConstructor @@ -12,4 +15,21 @@ public final class BalancesQuery { private String query; + + /** + * When set to {@code true}, the response includes the {@code currency_account_id} on each + * {@link CurrencyAccountBalance}. + * [Optional] + */ + @SerializedName("withCurrencyAccountId") + private Boolean withCurrencyAccountId; + + /** + * A UTC datetime to retrieve historical balances at a specific point in time. + * Must be in the past. If omitted, the response returns live balances. + * [Optional] + * Format: date-time (RFC 3339) + */ + @SerializedName("balancesAt") + private Instant balancesAt; } diff --git a/src/main/java/com/checkout/balances/CollateralBreakdown.java b/src/main/java/com/checkout/balances/CollateralBreakdown.java new file mode 100644 index 000000000..40c1026e3 --- /dev/null +++ b/src/main/java/com/checkout/balances/CollateralBreakdown.java @@ -0,0 +1,25 @@ +package com.checkout.balances; + +import com.google.gson.annotations.SerializedName; +import lombok.Data; + +/** + * A breakdown of the funds held in the {@code collateral} balance. + */ +@Data +public final class CollateralBreakdown { + + /** + * The portion of the {@code collateral} balance held as a fixed reserve. + * [Required] + */ + @SerializedName("fixed_reserve") + private Long fixedReserve; + + /** + * The portion of the {@code collateral} balance held as a rolling reserve. + * [Required] + */ + @SerializedName("rolling_reserve") + private Long rollingReserve; +} diff --git a/src/main/java/com/checkout/balances/CurrencyAccountBalance.java b/src/main/java/com/checkout/balances/CurrencyAccountBalance.java index 347a1d894..e7d87794e 100644 --- a/src/main/java/com/checkout/balances/CurrencyAccountBalance.java +++ b/src/main/java/com/checkout/balances/CurrencyAccountBalance.java @@ -4,9 +4,19 @@ import com.google.gson.annotations.SerializedName; import lombok.Data; +import java.time.Instant; + @Data public final class CurrencyAccountBalance { + /** + * The unique identifier of the currency account (sub-account). + * Returned only when the request is made with {@code withCurrencyAccountId=true}. + * [Optional] + */ + @SerializedName("currency_account_id") + private String currencyAccountId; + private String descriptor; @SerializedName("holding_currency") @@ -14,4 +24,14 @@ public final class CurrencyAccountBalance { private Balances balances; + /** + * The UTC datetime reflecting when the balance values were fetched. If the request includes a + * {@code balancesAt} query parameter, this matches that value; otherwise, it is the time the + * request was processed. + * [Optional] + * Format: date-time (RFC 3339) + */ + @SerializedName("balances_as_of") + private Instant balancesAsOf; + } diff --git a/src/main/java/com/checkout/handlepaymentsandpayouts/payments/common/source/SourceType.java b/src/main/java/com/checkout/handlepaymentsandpayouts/payments/common/source/SourceType.java index 4dada2d90..2c78a26f9 100644 --- a/src/main/java/com/checkout/handlepaymentsandpayouts/payments/common/source/SourceType.java +++ b/src/main/java/com/checkout/handlepaymentsandpayouts/payments/common/source/SourceType.java @@ -28,6 +28,9 @@ public enum SourceType { @SerializedName("benefit") BENEFIT, + @SerializedName("blik") + BLIK, + @SerializedName("cvconnect") CVCONNECT, diff --git a/src/main/java/com/checkout/handlepaymentsandpayouts/payments/common/source/bliksource/BlikSource.java b/src/main/java/com/checkout/handlepaymentsandpayouts/payments/common/source/bliksource/BlikSource.java new file mode 100644 index 000000000..9118b83bb --- /dev/null +++ b/src/main/java/com/checkout/handlepaymentsandpayouts/payments/common/source/bliksource/BlikSource.java @@ -0,0 +1,48 @@ +package com.checkout.handlepaymentsandpayouts.payments.common.source.bliksource; + +import com.checkout.handlepaymentsandpayouts.payments.common.source.AbstractSource; +import com.checkout.handlepaymentsandpayouts.payments.common.source.SourceType; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +/** + * blik source Class. + * Use this to process Blik payments in Poland. + * When {@code source.type} is {@code blik}: currency must be {@code PLN}, amount must not exceed + * 5,000,000 (minor unit), reference is limited to 35 characters. For customer-initiated payments, + * provide the 6-digit Blik code in {@code processing.partner_code}. For merchant-initiated recurring + * payments, use either {@code source.type: id} with a previous {@code source.id}, or + * {@code source.type: blik} with {@code partner_agreement_id}. + */ +@Getter +@Setter +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public final class BlikSource extends AbstractSource { + + /** + * Initializes a new instance of the BlikSource class. + */ + public BlikSource() { + super(SourceType.BLIK); + } + + /** + * The Checkout.com source identifier for the partner agreement created during a Blik recurring payment. + * Use this value as {@code source.id} (with {@code source.type: id}) to process subsequent + * merchant-initiated payments against the same agreement. + * [Optional] response-only + */ + private String id; + + /** + * The Blik PAYID identifying an external partner agreement created with another PSP. + * Only used when processing merchant-initiated recurring payments + * ({@code merchant_initiated: true}) without a stored Checkout.com source. + * [Optional] + * max 64 characters + */ + private String partnerAgreementId; +} diff --git a/src/main/java/com/checkout/handlepaymentsandpayouts/payments/postpayments/responses/requestapaymentorpayoutresponsecreated/retry/Retry.java b/src/main/java/com/checkout/handlepaymentsandpayouts/payments/postpayments/responses/requestapaymentorpayoutresponsecreated/retry/Retry.java index 3c058074c..1edf1ae10 100644 --- a/src/main/java/com/checkout/handlepaymentsandpayouts/payments/postpayments/responses/requestapaymentorpayoutresponsecreated/retry/Retry.java +++ b/src/main/java/com/checkout/handlepaymentsandpayouts/payments/postpayments/responses/requestapaymentorpayoutresponsecreated/retry/Retry.java @@ -19,6 +19,12 @@ @AllArgsConstructor public final class Retry { + /** + * Indicates whether asynchronous retries are enabled for the payment. + * [Optional] + */ + private Boolean enabled; + /** * Default: 6 The maximum number of authorization retry attempts, excluding the initial authorization. * [Optional] diff --git a/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/accountFundingTransaction/AccountFundingTransactionIdentification.java b/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/accountFundingTransaction/AccountFundingTransactionIdentification.java new file mode 100644 index 000000000..cda5cc1a7 --- /dev/null +++ b/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/accountFundingTransaction/AccountFundingTransactionIdentification.java @@ -0,0 +1,35 @@ +package com.checkout.handlepaymentsandpayouts.setups.entities.accountFundingTransaction; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Sender identification details. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public final class AccountFundingTransactionIdentification { + + /** + * The type of identification used to identify the sender. + * [Optional] + * Enum: "passport" "driving_license" "national_id" + */ + private AccountFundingTransactionIdentificationType type; + + /** + * The identification number. + * [Optional] + */ + private String number; + + /** + * The two-letter ISO country code of the country that issued the identification. + * [Optional] + */ + private String issuingCountry; +} diff --git a/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/accountFundingTransaction/AccountFundingTransactionIdentificationType.java b/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/accountFundingTransaction/AccountFundingTransactionIdentificationType.java new file mode 100644 index 000000000..c6c600496 --- /dev/null +++ b/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/accountFundingTransaction/AccountFundingTransactionIdentificationType.java @@ -0,0 +1,15 @@ +package com.checkout.handlepaymentsandpayouts.setups.entities.accountFundingTransaction; + +import com.google.gson.annotations.SerializedName; + +public enum AccountFundingTransactionIdentificationType { + + @SerializedName("passport") + PASSPORT, + + @SerializedName("driving_license") + DRIVING_LICENSE, + + @SerializedName("national_id") + NATIONAL_ID +} diff --git a/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/accountFundingTransaction/AccountFundingTransactionPurpose.java b/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/accountFundingTransaction/AccountFundingTransactionPurpose.java new file mode 100644 index 000000000..92a1e5c3e --- /dev/null +++ b/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/accountFundingTransaction/AccountFundingTransactionPurpose.java @@ -0,0 +1,63 @@ +package com.checkout.handlepaymentsandpayouts.setups.entities.accountFundingTransaction; + +import com.google.gson.annotations.SerializedName; + +public enum AccountFundingTransactionPurpose { + + @SerializedName("donations") + DONATIONS, + + @SerializedName("education") + EDUCATION, + + @SerializedName("emergency_need") + EMERGENCY_NEED, + + @SerializedName("expatriation") + EXPATRIATION, + + @SerializedName("family_support") + FAMILY_SUPPORT, + + @SerializedName("financial_services") + FINANCIAL_SERVICES, + + @SerializedName("gifts") + GIFTS, + + @SerializedName("income") + INCOME, + + @SerializedName("insurance") + INSURANCE, + + @SerializedName("investment") + INVESTMENT, + + @SerializedName("it_services") + IT_SERVICES, + + @SerializedName("leisure") + LEISURE, + + @SerializedName("loan_payment") + LOAN_PAYMENT, + + @SerializedName("medical_treatment") + MEDICAL_TREATMENT, + + @SerializedName("other") + OTHER, + + @SerializedName("pension") + PENSION, + + @SerializedName("royalties") + ROYALTIES, + + @SerializedName("savings") + SAVINGS, + + @SerializedName("travel_and_tourism") + TRAVEL_AND_TOURISM +} diff --git a/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/accountFundingTransaction/AccountFundingTransactionRecipient.java b/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/accountFundingTransaction/AccountFundingTransactionRecipient.java new file mode 100644 index 000000000..7efd110d6 --- /dev/null +++ b/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/accountFundingTransaction/AccountFundingTransactionRecipient.java @@ -0,0 +1,54 @@ +package com.checkout.handlepaymentsandpayouts.setups.entities.accountFundingTransaction; + +import com.checkout.common.Address; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDate; + +/** + * Account funding transaction recipient details. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public final class AccountFundingTransactionRecipient { + + /** + * Date of birth of the recipient. + * [Optional] + * Format: yyyy-MM-dd + */ + private LocalDate dateOfBirth; + + /** + * Any identifier like part of the PAN (first six digits and last four digits), an IBAN, + * an internal account number, or a phone number related to the primary recipient's account. + * [Optional] + * max 34 characters + */ + private String accountNumber; + + /** + * The recipient's first name. + * [Optional] + * max 50 characters + */ + private String firstName; + + /** + * The recipient's last name. + * [Optional] + * max 50 characters + */ + private String lastName; + + /** + * The recipient's address. + * [Optional] + */ + private Address address; +} diff --git a/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/accountFundingTransaction/AccountFundingTransactionSender.java b/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/accountFundingTransaction/AccountFundingTransactionSender.java new file mode 100644 index 000000000..0566e00e8 --- /dev/null +++ b/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/accountFundingTransaction/AccountFundingTransactionSender.java @@ -0,0 +1,37 @@ +package com.checkout.handlepaymentsandpayouts.setups.entities.accountFundingTransaction; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDate; + +/** + * Account funding transaction sender details. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public final class AccountFundingTransactionSender { + + /** + * Date of birth of the sender. + * [Optional] + * Format: yyyy-MM-dd + */ + private LocalDate dateOfBirth; + + /** + * The unique reference for the sender of the payment. + * [Optional] + */ + private String reference; + + /** + * Sender identification details. + * [Optional] + */ + private AccountFundingTransactionIdentification identification; +} diff --git a/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/accountFundingTransaction/PaymentSetupAccountFundingTransaction.java b/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/accountFundingTransaction/PaymentSetupAccountFundingTransaction.java new file mode 100644 index 000000000..23b707ebb --- /dev/null +++ b/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/accountFundingTransaction/PaymentSetupAccountFundingTransaction.java @@ -0,0 +1,44 @@ +package com.checkout.handlepaymentsandpayouts.setups.entities.accountFundingTransaction; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Account funding transaction details for the payment. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public final class PaymentSetupAccountFundingTransaction { + + /** + * Whether to process this payment as an account funding transaction. + * [Optional] + */ + private Boolean enabled; + + /** + * Specifies the purpose of the account funding transaction. + * [Optional] + * Enum: "donations" "education" "emergency_need" "expatriation" "family_support" + * "financial_services" "gifts" "income" "insurance" "investment" "it_services" + * "leisure" "loan_payment" "medical_treatment" "other" "pension" "royalties" + * "savings" "travel_and_tourism" + */ + private AccountFundingTransactionPurpose purpose; + + /** + * Account funding transaction sender details. + * [Optional] + */ + private AccountFundingTransactionSender sender; + + /** + * Account funding transaction recipient details. + * [Optional] + */ + private AccountFundingTransactionRecipient recipient; +} 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 4e8ed4bf8..9223c799f 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 @@ -8,6 +8,7 @@ import com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.bancontact.Bancontact; import com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.benefit.Benefit; import com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.bizum.Bizum; +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.dana.Dana; import com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.eps.Eps; @@ -87,6 +88,12 @@ public final class PaymentMethods { */ private Bizum bizum; + /** + * Blik payment method configuration. + * [Optional] + */ + private Blik blik; + /** * PayNow payment method configuration. * [Optional] diff --git a/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/paymentMethods/blik/Blik.java b/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/paymentMethods/blik/Blik.java new file mode 100644 index 000000000..f463d2fd3 --- /dev/null +++ b/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/paymentMethods/blik/Blik.java @@ -0,0 +1,20 @@ +package com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.blik; + +import com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.common.PaymentMethodBase; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * The Blik payment method's details and configuration. + */ +@Data +@EqualsAndHashCode(callSuper = true) +public final class Blik extends PaymentMethodBase { + + /** + * The 6-digit BLIK code generated by the customer's banking app. + * [Optional] writeOnly + * Pattern: ^[0-9]{6}$ + */ + private String partnerCode; +} diff --git a/src/main/java/com/checkout/handlepaymentsandpayouts/setups/requests/PaymentSetupsRequest.java b/src/main/java/com/checkout/handlepaymentsandpayouts/setups/requests/PaymentSetupsRequest.java index 4676f78d8..3a9684cab 100644 --- a/src/main/java/com/checkout/handlepaymentsandpayouts/setups/requests/PaymentSetupsRequest.java +++ b/src/main/java/com/checkout/handlepaymentsandpayouts/setups/requests/PaymentSetupsRequest.java @@ -2,6 +2,7 @@ import com.checkout.common.Currency; import com.checkout.payments.PaymentType; +import com.checkout.handlepaymentsandpayouts.setups.entities.accountFundingTransaction.PaymentSetupAccountFundingTransaction; import com.checkout.handlepaymentsandpayouts.setups.entities.billing.PaymentSetupBilling; import com.checkout.handlepaymentsandpayouts.setups.entities.customer.Customer; import com.checkout.handlepaymentsandpayouts.setups.entities.industry.Industry; @@ -97,4 +98,10 @@ public final class PaymentSetupsRequest { * [Optional] */ private PaymentSetupBilling billing; + + /** + * Account funding transaction details for the payment. + * [Optional] + */ + private PaymentSetupAccountFundingTransaction accountFundingTransaction; } \ No newline at end of file diff --git a/src/main/java/com/checkout/handlepaymentsandpayouts/setups/responses/PaymentSetupsResponse.java b/src/main/java/com/checkout/handlepaymentsandpayouts/setups/responses/PaymentSetupsResponse.java index 868b8b9c1..9d74774c5 100644 --- a/src/main/java/com/checkout/handlepaymentsandpayouts/setups/responses/PaymentSetupsResponse.java +++ b/src/main/java/com/checkout/handlepaymentsandpayouts/setups/responses/PaymentSetupsResponse.java @@ -2,6 +2,7 @@ import com.checkout.common.Resource; import com.checkout.common.Currency; +import com.checkout.handlepaymentsandpayouts.setups.entities.accountFundingTransaction.PaymentSetupAccountFundingTransaction; import com.checkout.handlepaymentsandpayouts.setups.entities.billing.PaymentSetupBilling; import com.checkout.handlepaymentsandpayouts.setups.entities.customer.Customer; import com.checkout.handlepaymentsandpayouts.setups.entities.industry.Industry; @@ -113,4 +114,10 @@ public final class PaymentSetupsResponse extends Resource { * [Optional] */ private List availablePaymentMethods; + + /** + * Account funding transaction details for the payment. + * [Optional] + */ + private PaymentSetupAccountFundingTransaction accountFundingTransaction; } diff --git a/src/main/java/com/checkout/instruments/InstrumentsClient.java b/src/main/java/com/checkout/instruments/InstrumentsClient.java index f98e2dbe6..7d89fdd7e 100644 --- a/src/main/java/com/checkout/instruments/InstrumentsClient.java +++ b/src/main/java/com/checkout/instruments/InstrumentsClient.java @@ -23,6 +23,15 @@ public interface InstrumentsClient { CompletableFuture delete(String instrumentId); + /** + * Revokes a payment instrument. The instrument status is set to {@code INVALID} with the reason + * {@code revoked_by_merchant}. The instrument record is retained for audit purposes. + * + * @param instrumentId The payment instrument ID. Pattern: ^(src_)[a-z0-9]{26}$. + * @return a {@link CompletableFuture} that resolves to an empty response on success. + */ + CompletableFuture revoke(String instrumentId); + CompletableFuture getBankAccountFieldFormatting(CountryCode country, Currency currency, BankAccountFieldQuery query); // Synchronous methods @@ -34,5 +43,13 @@ public interface InstrumentsClient { EmptyResponse deleteSync(String instrumentId); + /** + * Synchronous variant of {@link #revoke(String)}. + * + * @param instrumentId The payment instrument ID. Pattern: ^(src_)[a-z0-9]{26}$. + * @return an empty response on success. + */ + EmptyResponse revokeSync(String instrumentId); + BankAccountFieldResponse getBankAccountFieldFormattingSync(CountryCode country, Currency currency, BankAccountFieldQuery query); } diff --git a/src/main/java/com/checkout/instruments/InstrumentsClientImpl.java b/src/main/java/com/checkout/instruments/InstrumentsClientImpl.java index d6b07fb67..86b93b385 100644 --- a/src/main/java/com/checkout/instruments/InstrumentsClientImpl.java +++ b/src/main/java/com/checkout/instruments/InstrumentsClientImpl.java @@ -57,6 +57,12 @@ public CompletableFuture delete(final String instrumentId) { return apiClient.deleteAsync(buildPath(INSTRUMENTS_PATH, instrumentId), sdkAuthorization()); } + @Override + public CompletableFuture revoke(final String instrumentId) { + validateInstrumentId(instrumentId); + return apiClient.patchAsync(buildPath(INSTRUMENTS_PATH, instrumentId, "revoke"), sdkAuthorization(), EmptyResponse.class, null, null); + } + @Override public CompletableFuture getBankAccountFieldFormatting(final CountryCode country, final Currency currency, final BankAccountFieldQuery query) { validateBankAccountFieldParams(country, currency, query); @@ -88,6 +94,12 @@ public EmptyResponse deleteSync(final String instrumentId) { return apiClient.delete(buildPath(INSTRUMENTS_PATH, instrumentId), sdkAuthorization()); } + @Override + public EmptyResponse revokeSync(final String instrumentId) { + validateInstrumentId(instrumentId); + return apiClient.patch(buildPath(INSTRUMENTS_PATH, instrumentId, "revoke"), sdkAuthorization(), EmptyResponse.class, null, null); + } + @Override public BankAccountFieldResponse getBankAccountFieldFormattingSync(final CountryCode country, final Currency currency, final BankAccountFieldQuery query) { validateBankAccountFieldParams(country, currency, query); diff --git a/src/main/java/com/checkout/issuing/IssuingClient.java b/src/main/java/com/checkout/issuing/IssuingClient.java index 1bdf03845..37a8b0b5e 100644 --- a/src/main/java/com/checkout/issuing/IssuingClient.java +++ b/src/main/java/com/checkout/issuing/IssuingClient.java @@ -163,6 +163,10 @@ CompletableFuture simulateReversal( CompletableFuture escalateDispute(final String disputeId, String idempotencyKey, final EscalateDisputeRequest escalateDisputeRequest); + /** + * @deprecated POST /issuing/disputes/{disputeId}/submit was removed from the API on 2026-04-15. + */ + @Deprecated CompletableFuture submitDispute(final String disputeId, String idempotencyKey, final SubmitDisputeRequest submitDisputeRequest); CompletableFuture getListTransactions(final TransactionsQuery queryFilter); @@ -285,6 +289,10 @@ CardAuthorizationReversalResponse simulateReversalSync( VoidResponse escalateDisputeSync(String disputeId, String idempotencyKey, EscalateDisputeRequest escalateDisputeRequest); + /** + * @deprecated POST /issuing/disputes/{disputeId}/submit was removed from the API on 2026-04-15. + */ + @Deprecated DisputeResponse submitDisputeSync(String disputeId, String idempotencyKey, SubmitDisputeRequest submitDisputeRequest); TransactionsListResponse getListTransactionsSync(TransactionsQuery queryFilter); diff --git a/src/main/java/com/checkout/issuing/disputes/requests/CreateDisputeRequest.java b/src/main/java/com/checkout/issuing/disputes/requests/CreateDisputeRequest.java index 39fb0d5ba..0d4c78105 100644 --- a/src/main/java/com/checkout/issuing/disputes/requests/CreateDisputeRequest.java +++ b/src/main/java/com/checkout/issuing/disputes/requests/CreateDisputeRequest.java @@ -58,7 +58,10 @@ public final class CreateDisputeRequest extends Resource { * • Immediately – Set to true. * • Later – Set to false. * Default: false + * + * @deprecated Removed from the API on 2026-04-15. Use the Submit an Issuing Dispute endpoint instead. */ + @Deprecated @Builder.Default private Boolean isReadyForSubmission = false; diff --git a/src/main/java/com/checkout/onboardingsimulator/OnboardingSimulatorClient.java b/src/main/java/com/checkout/onboardingsimulator/OnboardingSimulatorClient.java new file mode 100644 index 000000000..dcacfb7de --- /dev/null +++ b/src/main/java/com/checkout/onboardingsimulator/OnboardingSimulatorClient.java @@ -0,0 +1,39 @@ +package com.checkout.onboardingsimulator; + +import com.checkout.ItemsResponse; +import com.checkout.onboardingsimulator.entities.SimulatorAvailableRequirement; +import com.checkout.onboardingsimulator.entities.SimulatorScenario; +import com.checkout.onboardingsimulator.requests.SimulatorSetRequirementsDueRequest; +import com.checkout.onboardingsimulator.requests.SimulatorSetStatusRequest; +import com.checkout.onboardingsimulator.responses.SimulatorRunScenarioResponse; +import com.checkout.onboardingsimulator.responses.SimulatorSetRequirementsDueResponse; +import com.checkout.onboardingsimulator.responses.SimulatorSetStatusResponse; + +import java.util.concurrent.CompletableFuture; + +/** + * Onboarding Simulator client. Sandbox only — endpoints are not registered in production. + */ +public interface OnboardingSimulatorClient { + + CompletableFuture setRequirementsDue(String entityId, SimulatorSetRequirementsDueRequest request); + + CompletableFuture runScenario(String entityId, String scenarioId); + + CompletableFuture setEntityStatus(String entityId, SimulatorSetStatusRequest request); + + CompletableFuture> listAvailableRequirements(); + + CompletableFuture> listScenarios(); + + // Synchronous methods + SimulatorSetRequirementsDueResponse setRequirementsDueSync(String entityId, SimulatorSetRequirementsDueRequest request); + + SimulatorRunScenarioResponse runScenarioSync(String entityId, String scenarioId); + + SimulatorSetStatusResponse setEntityStatusSync(String entityId, SimulatorSetStatusRequest request); + + ItemsResponse listAvailableRequirementsSync(); + + ItemsResponse listScenariosSync(); +} diff --git a/src/main/java/com/checkout/onboardingsimulator/OnboardingSimulatorClientImpl.java b/src/main/java/com/checkout/onboardingsimulator/OnboardingSimulatorClientImpl.java new file mode 100644 index 000000000..4b84a5c75 --- /dev/null +++ b/src/main/java/com/checkout/onboardingsimulator/OnboardingSimulatorClientImpl.java @@ -0,0 +1,103 @@ +package com.checkout.onboardingsimulator; + +import com.checkout.AbstractClient; +import com.checkout.ApiClient; +import com.checkout.CheckoutConfiguration; +import com.checkout.ItemsResponse; +import com.checkout.SdkAuthorizationType; +import com.checkout.onboardingsimulator.entities.SimulatorAvailableRequirement; +import com.checkout.onboardingsimulator.entities.SimulatorScenario; +import com.checkout.onboardingsimulator.requests.SimulatorSetRequirementsDueRequest; +import com.checkout.onboardingsimulator.requests.SimulatorSetStatusRequest; +import com.checkout.onboardingsimulator.responses.SimulatorRunScenarioResponse; +import com.checkout.onboardingsimulator.responses.SimulatorSetRequirementsDueResponse; +import com.checkout.onboardingsimulator.responses.SimulatorSetStatusResponse; +import com.google.gson.reflect.TypeToken; + +import java.lang.reflect.Type; +import java.util.concurrent.CompletableFuture; + +import static com.checkout.common.CheckoutUtils.validateParams; + +public class OnboardingSimulatorClientImpl extends AbstractClient implements OnboardingSimulatorClient { + + private static final String SIMULATE_PATH = "simulate"; + private static final String ENTITIES_PATH = "entities"; + private static final String REQUIREMENTS_DUE_PATH = "requirements-due"; + private static final String SCENARIOS_PATH = "scenarios"; + private static final String STATUS_PATH = "status"; + + private static final Type AVAILABLE_REQUIREMENTS_TYPE = new TypeToken>() {}.getType(); + private static final Type SCENARIOS_TYPE = new TypeToken>() {}.getType(); + + public OnboardingSimulatorClientImpl(final ApiClient apiClient, final CheckoutConfiguration configuration) { + super(apiClient, configuration, SdkAuthorizationType.OAUTH); + } + + @Override + public CompletableFuture setRequirementsDue(final String entityId, final SimulatorSetRequirementsDueRequest request) { + validateParams("entityId", entityId, "request", request); + return apiClient.postAsync(buildPath(SIMULATE_PATH, ENTITIES_PATH, entityId, REQUIREMENTS_DUE_PATH), + sdkAuthorization(), SimulatorSetRequirementsDueResponse.class, request, null); + } + + @Override + public CompletableFuture runScenario(final String entityId, final String scenarioId) { + validateParams("entityId", entityId, "scenarioId", scenarioId); + return apiClient.postAsync(buildPath(SIMULATE_PATH, ENTITIES_PATH, entityId, SCENARIOS_PATH, scenarioId), + sdkAuthorization(), SimulatorRunScenarioResponse.class, null, null); + } + + @Override + public CompletableFuture setEntityStatus(final String entityId, final SimulatorSetStatusRequest request) { + validateParams("entityId", entityId, "request", request); + return apiClient.postAsync(buildPath(SIMULATE_PATH, ENTITIES_PATH, entityId, STATUS_PATH), + sdkAuthorization(), SimulatorSetStatusResponse.class, request, null); + } + + @Override + public CompletableFuture> listAvailableRequirements() { + return apiClient.getAsync(buildPath(SIMULATE_PATH, REQUIREMENTS_DUE_PATH), + sdkAuthorization(), AVAILABLE_REQUIREMENTS_TYPE); + } + + @Override + public CompletableFuture> listScenarios() { + return apiClient.getAsync(buildPath(SIMULATE_PATH, SCENARIOS_PATH), + sdkAuthorization(), SCENARIOS_TYPE); + } + + // Synchronous methods + @Override + public SimulatorSetRequirementsDueResponse setRequirementsDueSync(final String entityId, final SimulatorSetRequirementsDueRequest request) { + validateParams("entityId", entityId, "request", request); + return apiClient.post(buildPath(SIMULATE_PATH, ENTITIES_PATH, entityId, REQUIREMENTS_DUE_PATH), + sdkAuthorization(), SimulatorSetRequirementsDueResponse.class, request, null); + } + + @Override + public SimulatorRunScenarioResponse runScenarioSync(final String entityId, final String scenarioId) { + validateParams("entityId", entityId, "scenarioId", scenarioId); + return apiClient.post(buildPath(SIMULATE_PATH, ENTITIES_PATH, entityId, SCENARIOS_PATH, scenarioId), + sdkAuthorization(), SimulatorRunScenarioResponse.class, null, null); + } + + @Override + public SimulatorSetStatusResponse setEntityStatusSync(final String entityId, final SimulatorSetStatusRequest request) { + validateParams("entityId", entityId, "request", request); + return apiClient.post(buildPath(SIMULATE_PATH, ENTITIES_PATH, entityId, STATUS_PATH), + sdkAuthorization(), SimulatorSetStatusResponse.class, request, null); + } + + @Override + public ItemsResponse listAvailableRequirementsSync() { + return apiClient.get(buildPath(SIMULATE_PATH, REQUIREMENTS_DUE_PATH), + sdkAuthorization(), AVAILABLE_REQUIREMENTS_TYPE); + } + + @Override + public ItemsResponse listScenariosSync() { + return apiClient.get(buildPath(SIMULATE_PATH, SCENARIOS_PATH), + sdkAuthorization(), SCENARIOS_TYPE); + } +} diff --git a/src/main/java/com/checkout/onboardingsimulator/entities/SimulatorAvailableRequirement.java b/src/main/java/com/checkout/onboardingsimulator/entities/SimulatorAvailableRequirement.java new file mode 100644 index 000000000..a034b5455 --- /dev/null +++ b/src/main/java/com/checkout/onboardingsimulator/entities/SimulatorAvailableRequirement.java @@ -0,0 +1,28 @@ +package com.checkout.onboardingsimulator.entities; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * A requirement field available to mark as due in the Onboarding Simulator. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public final class SimulatorAvailableRequirement { + + /** + * The public path of the requirement field. + * [Optional] + */ + private String field; + + /** + * The data type of the field. + * [Optional] + */ + private String type; +} diff --git a/src/main/java/com/checkout/onboardingsimulator/entities/SimulatorEntityStatus.java b/src/main/java/com/checkout/onboardingsimulator/entities/SimulatorEntityStatus.java new file mode 100644 index 000000000..d48ba87ec --- /dev/null +++ b/src/main/java/com/checkout/onboardingsimulator/entities/SimulatorEntityStatus.java @@ -0,0 +1,27 @@ +package com.checkout.onboardingsimulator.entities; + +import com.google.gson.annotations.SerializedName; + +public enum SimulatorEntityStatus { + + @SerializedName("draft") + DRAFT, + + @SerializedName("requirements_due") + REQUIREMENTS_DUE, + + @SerializedName("pending") + PENDING, + + @SerializedName("active") + ACTIVE, + + @SerializedName("restricted") + RESTRICTED, + + @SerializedName("rejected") + REJECTED, + + @SerializedName("inactive") + INACTIVE +} diff --git a/src/main/java/com/checkout/onboardingsimulator/entities/SimulatorScenario.java b/src/main/java/com/checkout/onboardingsimulator/entities/SimulatorScenario.java new file mode 100644 index 000000000..6d0d18c5a --- /dev/null +++ b/src/main/java/com/checkout/onboardingsimulator/entities/SimulatorScenario.java @@ -0,0 +1,56 @@ +package com.checkout.onboardingsimulator.entities; + +import com.google.gson.annotations.SerializedName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * A pre-defined scenario available in the Onboarding Simulator. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public final class SimulatorScenario { + + /** + * The unique identifier of the scenario. + * [Optional] + */ + private String id; + + /** + * Human-readable name of the scenario. + * [Optional] + */ + private String name; + + /** + * Description of what the scenario does. + * [Optional] + */ + private String description; + + /** + * The action type performed by this scenario. + * [Optional] + */ + private String action; + + /** + * The resulting entity status after the scenario runs. Empty string for non-status actions. + * [Optional] + */ + private String status; + + /** + * Requirement fields applied when the scenario runs, if any. + * [Optional] + */ + @SerializedName("requirements_due") + private List requirementsDue; +} diff --git a/src/main/java/com/checkout/onboardingsimulator/requests/SimulatorSetRequirementsDueRequest.java b/src/main/java/com/checkout/onboardingsimulator/requests/SimulatorSetRequirementsDueRequest.java new file mode 100644 index 000000000..8415a7561 --- /dev/null +++ b/src/main/java/com/checkout/onboardingsimulator/requests/SimulatorSetRequirementsDueRequest.java @@ -0,0 +1,25 @@ +package com.checkout.onboardingsimulator.requests; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * Request body for marking requirement fields as due on an entity. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public final class SimulatorSetRequirementsDueRequest { + + /** + * The requirement fields to mark as due. Call the List available requirements endpoint + * for a list of valid values. + * [Required] + */ + private List fields; +} diff --git a/src/main/java/com/checkout/onboardingsimulator/requests/SimulatorSetStatusRequest.java b/src/main/java/com/checkout/onboardingsimulator/requests/SimulatorSetStatusRequest.java new file mode 100644 index 000000000..8aa4d27ff --- /dev/null +++ b/src/main/java/com/checkout/onboardingsimulator/requests/SimulatorSetStatusRequest.java @@ -0,0 +1,24 @@ +package com.checkout.onboardingsimulator.requests; + +import com.checkout.onboardingsimulator.entities.SimulatorEntityStatus; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Request body for forcing the entity to a specific status. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public final class SimulatorSetStatusRequest { + + /** + * The status to set on the entity. + * [Required] + * Enum: "draft" "requirements_due" "pending" "active" "restricted" "rejected" "inactive" + */ + private SimulatorEntityStatus status; +} diff --git a/src/main/java/com/checkout/onboardingsimulator/responses/SimulatorRunScenarioResponse.java b/src/main/java/com/checkout/onboardingsimulator/responses/SimulatorRunScenarioResponse.java new file mode 100644 index 000000000..ad8bbcbeb --- /dev/null +++ b/src/main/java/com/checkout/onboardingsimulator/responses/SimulatorRunScenarioResponse.java @@ -0,0 +1,64 @@ +package com.checkout.onboardingsimulator.responses; + +import com.checkout.common.Resource; +import com.google.gson.annotations.SerializedName; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.util.List; + +/** + * Result of running a simulator scenario. + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public final class SimulatorRunScenarioResponse extends Resource { + + /** + * The ID of the entity. + * [Optional] + */ + @SerializedName("entity_id") + private String entityId; + + /** + * The ID of the scenario that was run. + * [Optional] + */ + @SerializedName("scenario_id") + private String scenarioId; + + /** + * The name of the scenario that was run. + * [Optional] + */ + @SerializedName("scenario_name") + private String scenarioName; + + /** + * The entity status before the scenario ran. + * [Optional] + */ + @SerializedName("previous_status") + private String previousStatus; + + /** + * The entity status after the scenario ran. + * [Optional] + */ + @SerializedName("current_status") + private String currentStatus; + + /** + * Requirement fields applied by the scenario, if any. + * [Optional] + */ + @SerializedName("requirements_due") + private List requirementsDue; +} diff --git a/src/main/java/com/checkout/onboardingsimulator/responses/SimulatorSetRequirementsDueResponse.java b/src/main/java/com/checkout/onboardingsimulator/responses/SimulatorSetRequirementsDueResponse.java new file mode 100644 index 000000000..ebe1bf0e4 --- /dev/null +++ b/src/main/java/com/checkout/onboardingsimulator/responses/SimulatorSetRequirementsDueResponse.java @@ -0,0 +1,50 @@ +package com.checkout.onboardingsimulator.responses; + +import com.checkout.common.Resource; +import com.google.gson.annotations.SerializedName; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.util.List; + +/** + * Response from the Set requirements due simulator endpoint. + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public final class SimulatorSetRequirementsDueResponse extends Resource { + + /** + * The ID of the entity. + * [Optional] + */ + @SerializedName("entity_id") + private String entityId; + + /** + * The status before the update. + * [Optional] + */ + @SerializedName("previous_status") + private String previousStatus; + + /** + * The status after the update. + * [Optional] + */ + @SerializedName("current_status") + private String currentStatus; + + /** + * The requirement fields that are now marked as due. + * [Optional] + */ + @SerializedName("requirements_due") + private List requirementsDue; +} diff --git a/src/main/java/com/checkout/onboardingsimulator/responses/SimulatorSetStatusResponse.java b/src/main/java/com/checkout/onboardingsimulator/responses/SimulatorSetStatusResponse.java new file mode 100644 index 000000000..a99ef94cf --- /dev/null +++ b/src/main/java/com/checkout/onboardingsimulator/responses/SimulatorSetStatusResponse.java @@ -0,0 +1,41 @@ +package com.checkout.onboardingsimulator.responses; + +import com.checkout.common.Resource; +import com.google.gson.annotations.SerializedName; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +/** + * Response from the Set entity status simulator endpoint. + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public final class SimulatorSetStatusResponse extends Resource { + + /** + * The ID of the entity. + * [Optional] + */ + @SerializedName("entity_id") + private String entityId; + + /** + * The status before the update. + * [Optional] + */ + @SerializedName("previous_status") + private String previousStatus; + + /** + * The status after the update. + * [Optional] + */ + @SerializedName("current_status") + private String currentStatus; +} diff --git a/src/main/java/com/checkout/payments/Passenger.java b/src/main/java/com/checkout/payments/Passenger.java index cfe806308..23f143936 100644 --- a/src/main/java/com/checkout/payments/Passenger.java +++ b/src/main/java/com/checkout/payments/Passenger.java @@ -1,23 +1,47 @@ package com.checkout.payments; -import com.checkout.common.CountryCode; import com.google.gson.annotations.SerializedName; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; +import java.time.LocalDate; + +/** + * Contains information about a passenger on the flight. + */ @Data @Builder @NoArgsConstructor @AllArgsConstructor public final class Passenger { - private PassengerName name; + /** + * 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 String dateOfBirth; + private LocalDate dateOfBirth; - @SerializedName("country_code") - private CountryCode countryCode; + /** + * Contains information about the passenger's address. + * [Optional] + */ + private PassengerAddress address; } diff --git a/src/main/java/com/checkout/payments/PassengerAddress.java b/src/main/java/com/checkout/payments/PassengerAddress.java new file mode 100644 index 000000000..642ccaf67 --- /dev/null +++ b/src/main/java/com/checkout/payments/PassengerAddress.java @@ -0,0 +1,23 @@ +package com.checkout.payments; + +import com.checkout.common.CountryCode; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Contains information about a passenger's address. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public final class PassengerAddress { + + /** + * The two-letter ISO country code of the passenger's country of residence. + * [Optional] + */ + private CountryCode country; +} diff --git a/src/main/java/com/checkout/payments/PassengerName.java b/src/main/java/com/checkout/payments/PassengerName.java deleted file mode 100644 index d38a78ea5..000000000 --- a/src/main/java/com/checkout/payments/PassengerName.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.checkout.payments; - -import com.google.gson.annotations.SerializedName; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public final class PassengerName { - - @SerializedName("full_name") - private String fullName; - -} diff --git a/src/main/java/com/checkout/payments/PaymentPlan.java b/src/main/java/com/checkout/payments/PaymentPlan.java index 0ad24fe9b..c6fbdb836 100644 --- a/src/main/java/com/checkout/payments/PaymentPlan.java +++ b/src/main/java/com/checkout/payments/PaymentPlan.java @@ -17,7 +17,15 @@ public final class PaymentPlan { // Installment private Boolean financing; - private String amount; + /** + * The amount to charge for each payment in the plan, in the minor currency unit. + * Required when {@code source.type} is {@code blik}, {@code payment_plan.amount_variability} + * is {@code Fixed}, and the recurring agreement is created without an initial payment + * ({@code amount} set to {@code 0}). + * [Optional] + * min 1 + */ + private Long amount; // Common properties @SerializedName("days_between_payments") @@ -32,4 +40,22 @@ public final class PaymentPlan { @SerializedName("expiry") private Instant expiry; + /** + * The name of the payment plan. Required when {@code source.type} is {@code blik}. + * For Blik merchant-initiated requests using an external {@code partner_agreement_id}, + * this value is used as the Blik Alias Label. + * [Optional] + * max 35 characters + */ + private String name; + + /** + * The date on which the first payment will be taken, in {@code YYYYMMDD} format. + * Required when {@code source.type} is {@code blik} and the recurring agreement is + * created without an initial payment ({@code amount} set to {@code 0}). + * [Optional] + */ + @SerializedName("start_date") + private String startDate; + } diff --git a/src/main/java/com/checkout/payments/ProcessingSettings.java b/src/main/java/com/checkout/payments/ProcessingSettings.java index b3dd84f98..f44fa9974 100644 --- a/src/main/java/com/checkout/payments/ProcessingSettings.java +++ b/src/main/java/com/checkout/payments/ProcessingSettings.java @@ -300,4 +300,14 @@ public final class ProcessingSettings { */ private String affiliateUrl; + /** + * The customer's 6-digit Blik code. Required when {@code source.type} is {@code blik} and + * {@code merchant_initiated} is {@code false} (for example, for {@code Regular} payments and + * the initial payment of a {@code Recurring} agreement). + * [Optional] + * Pattern: ^\d{6}$ + * 6 characters + */ + private String partnerCode; + } diff --git a/src/main/java/com/checkout/payments/ReverseResponse.java b/src/main/java/com/checkout/payments/ReverseResponse.java index f0b3bc4ba..b5119d484 100644 --- a/src/main/java/com/checkout/payments/ReverseResponse.java +++ b/src/main/java/com/checkout/payments/ReverseResponse.java @@ -16,4 +16,14 @@ public final class ReverseResponse extends Resource { private String reference; + /** + * The payment action performed during the reversal. + * Determined by the payment's state at the time of processing: + * "Refund" if the payment has been fully or partially captured; + * "Void" if the payment is in an Authorized state. + * [Optional] + */ + @SerializedName("action_type") + private String actionType; + } diff --git a/src/main/java/com/checkout/payments/contexts/PaymentContextDetailsResponse.java b/src/main/java/com/checkout/payments/contexts/PaymentContextDetailsResponse.java index 97d7280a3..7d44bed86 100644 --- a/src/main/java/com/checkout/payments/contexts/PaymentContextDetailsResponse.java +++ b/src/main/java/com/checkout/payments/contexts/PaymentContextDetailsResponse.java @@ -12,6 +12,12 @@ @ToString(callSuper = true) public final class PaymentContextDetailsResponse extends HttpMetadata { + /** + * The unique identifier of the payment context. + * [Optional] + */ + private String id; + private PaymentContextDetailsStatusType status; @SerializedName("payment_request") diff --git a/src/main/java/com/checkout/payments/response/PaymentRetryResponse.java b/src/main/java/com/checkout/payments/response/PaymentRetryResponse.java index 56216f8fb..760add92a 100644 --- a/src/main/java/com/checkout/payments/response/PaymentRetryResponse.java +++ b/src/main/java/com/checkout/payments/response/PaymentRetryResponse.java @@ -10,6 +10,12 @@ @Builder public final class PaymentRetryResponse { + /** + * Indicates whether asynchronous retries are enabled for the payment. + * [Optional] + */ + private Boolean enabled; + @SerializedName("max_attempts") private Integer maxAttempts; diff --git a/src/main/java/com/checkout/payments/response/ProcessingData.java b/src/main/java/com/checkout/payments/response/ProcessingData.java index 66e9664fe..754a5bcfe 100644 --- a/src/main/java/com/checkout/payments/response/ProcessingData.java +++ b/src/main/java/com/checkout/payments/response/ProcessingData.java @@ -78,4 +78,36 @@ public final class ProcessingData { @SerializedName("cko_network_token_available") private Boolean ckoNetworkTokenAvailable; + /** + * Indicates whether the {@code fallback_source} field was used for the payment. + * [Optional] + */ + @SerializedName("fallback_source_used") + private Boolean fallbackSourceUsed; + + /** + * A high-level failure category returned by the payment provider when a payment is declined or fails. + * Not all payment methods return this field. + * [Optional] + */ + @SerializedName("failure_code") + private String failureCode; + + /** + * The 6-digit partner code returned by the payment provider. Returned when {@code source.type} is {@code blik}. + * [Optional] + * Pattern: ^\d{6}$ + * 6 characters + */ + @SerializedName("partner_code") + private String partnerCode; + + /** + * The raw response code returned by the payment provider when a payment is declined or fails. + * Not all payment methods return this field. + * [Optional] + */ + @SerializedName("partner_response_code") + private String partnerResponseCode; + } diff --git a/src/main/java/com/checkout/sessions/DeviceInformation.java b/src/main/java/com/checkout/sessions/DeviceInformation.java new file mode 100644 index 000000000..ab4b74eaa --- /dev/null +++ b/src/main/java/com/checkout/sessions/DeviceInformation.java @@ -0,0 +1,32 @@ +package com.checkout.sessions; + +import com.google.gson.annotations.SerializedName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Details of the device from which the authentication originated. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public final class DeviceInformation { + + /** + * The unique identifier for the device. + * [Optional] + */ + @SerializedName("device_id") + private String deviceId; + + /** + * Device session ID collected from the standalone Risk.js package. + * [Optional] + * Pattern: ^(dsid)_(\w{26})$ + */ + @SerializedName("device_session_id") + private String deviceSessionId; +} diff --git a/src/main/java/com/checkout/sessions/SessionRequest.java b/src/main/java/com/checkout/sessions/SessionRequest.java index da929cc73..f2f21dbde 100644 --- a/src/main/java/com/checkout/sessions/SessionRequest.java +++ b/src/main/java/com/checkout/sessions/SessionRequest.java @@ -82,4 +82,11 @@ public final class SessionRequest { @SerializedName("initial_transaction") private InitialTransaction initialTransaction; + /** + * Details of the device from which the authentication originated. + * [Optional] + */ + @SerializedName("device_information") + private DeviceInformation deviceInformation; + } diff --git a/src/main/java/com/checkout/tokens/TokenMetadataBillingAddress.java b/src/main/java/com/checkout/tokens/TokenMetadataBillingAddress.java new file mode 100644 index 000000000..cc02aece2 --- /dev/null +++ b/src/main/java/com/checkout/tokens/TokenMetadataBillingAddress.java @@ -0,0 +1,29 @@ +package com.checkout.tokens; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Partial billing address — city and country only. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public final class TokenMetadataBillingAddress { + + /** + * The address city. + * [Optional] + */ + private String city; + + /** + * The billing address country (two-letter ISO code). + * [Optional] + * 2 characters + */ + private String country; +} diff --git a/src/main/java/com/checkout/tokens/TokenMetadataResponse.java b/src/main/java/com/checkout/tokens/TokenMetadataResponse.java new file mode 100644 index 000000000..fa8f95b86 --- /dev/null +++ b/src/main/java/com/checkout/tokens/TokenMetadataResponse.java @@ -0,0 +1,129 @@ +package com.checkout.tokens; + +import com.checkout.common.CardCategory; +import com.checkout.common.CardType; +import com.checkout.common.Resource; +import com.google.gson.annotations.SerializedName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import java.time.Instant; + +/** + * Token metadata response returned by GET /tokens/{tokenId}/metadata. + */ +@Data +@EqualsAndHashCode(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public final class TokenMetadataResponse extends Resource { + + /** + * The token ID. + * [Required] + */ + private String token; + + /** + * The token type. + * [Required] + */ + private String type; + + /** + * The date/time the token will expire. + * [Required] + * Format: date-time (RFC 3339) + */ + @SerializedName("expires_on") + private Instant expiresOn; + + /** + * The card expiry month. + * [Required] + * min 1, max 12 + */ + @SerializedName("expiry_month") + private Integer expiryMonth; + + /** + * The card expiry year. + * [Required] + */ + @SerializedName("expiry_year") + private Integer expiryYear; + + /** + * The card scheme. + * [Optional] + */ + private String scheme; + + /** + * The last four digits of the card number. + * [Required] + */ + private String last4; + + /** + * The card issuer BIN. + * [Required] + */ + private String bin; + + /** + * The card type. + * [Optional] + * Enum: "CREDIT" "DEBIT" "PREPAID" "CHARGE" "DEFERRED DEBIT" + */ + @SerializedName("card_type") + private CardType cardType; + + /** + * The card category. + * [Optional] + * Enum: "CONSUMER" "COMMERCIAL" + */ + @SerializedName("card_category") + private CardCategory cardCategory; + + /** + * The name of the card issuer. + * [Optional] + */ + private String issuer; + + /** + * The card issuer's country (two-letter ISO code). + * [Optional] + * min 2 characters + * max 2 characters + */ + @SerializedName("issuer_country") + private String issuerCountry; + + /** + * The issuer or card scheme's product identifier. + * [Optional] + */ + @SerializedName("product_id") + private String productId; + + /** + * The issuer or card scheme's product type. + * [Optional] + */ + @SerializedName("product_type") + private String productType; + + /** + * Partial billing address — city and country only. + * [Optional] + */ + @SerializedName("billing_address") + private TokenMetadataBillingAddress billingAddress; +} diff --git a/src/main/java/com/checkout/tokens/TokensClient.java b/src/main/java/com/checkout/tokens/TokensClient.java index 5c6e1120f..52ce1630d 100644 --- a/src/main/java/com/checkout/tokens/TokensClient.java +++ b/src/main/java/com/checkout/tokens/TokensClient.java @@ -8,9 +8,26 @@ public interface TokensClient { CompletableFuture requestWalletToken(WalletTokenRequest walletTokenRequest); + /** + * Returns the details for an active token without consuming it. The token remains usable + * after this call. + * + * @param tokenId The token ID. Pattern: ^(tok)_(\w{26})$. + * @return a {@link CompletableFuture} that resolves to the token metadata. + */ + CompletableFuture getTokenMetadata(String tokenId); + // Synchronous methods CardTokenResponse requestCardTokenSync(CardTokenRequest cardTokenRequest); TokenResponse requestWalletTokenSync(WalletTokenRequest walletTokenRequest); + /** + * Synchronous variant of {@link #getTokenMetadata(String)}. + * + * @param tokenId The token ID. Pattern: ^(tok)_(\w{26})$. + * @return the token metadata. + */ + TokenMetadataResponse getTokenMetadataSync(String tokenId); + } diff --git a/src/main/java/com/checkout/tokens/TokensClientImpl.java b/src/main/java/com/checkout/tokens/TokensClientImpl.java index fb3b7ba2b..67d30d54c 100644 --- a/src/main/java/com/checkout/tokens/TokensClientImpl.java +++ b/src/main/java/com/checkout/tokens/TokensClientImpl.java @@ -29,6 +29,13 @@ public CompletableFuture requestWalletToken(final WalletTokenRequ return apiClient.postAsync(TOKENS_PATH, sdkAuthorization(), TokenResponse.class, walletTokenRequest, null); } + @Override + public CompletableFuture getTokenMetadata(final String tokenId) { + validateParams("tokenId", tokenId); + return apiClient.getAsync(buildPath(TOKENS_PATH, tokenId, "metadata"), + sdkAuthorization(SdkAuthorizationType.SECRET_KEY_OR_OAUTH), TokenMetadataResponse.class); + } + // Synchronous methods @Override public CardTokenResponse requestCardTokenSync(final CardTokenRequest cardTokenRequest) { @@ -42,6 +49,13 @@ public TokenResponse requestWalletTokenSync(final WalletTokenRequest walletToken return apiClient.post(TOKENS_PATH, sdkAuthorization(), TokenResponse.class, walletTokenRequest, null); } + @Override + public TokenMetadataResponse getTokenMetadataSync(final String tokenId) { + validateParams("tokenId", tokenId); + return apiClient.get(buildPath(TOKENS_PATH, tokenId, "metadata"), + sdkAuthorization(SdkAuthorizationType.SECRET_KEY_OR_OAUTH), TokenMetadataResponse.class); + } + // Common methods protected void validateCardTokenRequest(final CardTokenRequest cardTokenRequest) { validateParams("cardTokenRequest", cardTokenRequest); diff --git a/src/test/java/com/checkout/CheckoutApiImplTest.java b/src/test/java/com/checkout/CheckoutApiImplTest.java index e180cca55..7e9da8faa 100644 --- a/src/test/java/com/checkout/CheckoutApiImplTest.java +++ b/src/test/java/com/checkout/CheckoutApiImplTest.java @@ -37,6 +37,7 @@ void shouldInstantiateAndRetrieveClients() { assertNotNull(checkoutApi.forexClient()); assertNotNull(checkoutApi.metadataClient()); assertNotNull(checkoutApi.agenticCommerceClient()); + assertNotNull(checkoutApi.onboardingSimulatorClient()); // APMs assertNotNull(checkoutApi.idealClient()); } diff --git a/src/test/java/com/checkout/CheckoutSdkTelemetryIntegrationTest.java b/src/test/java/com/checkout/CheckoutSdkTelemetryIntegrationTest.java index 76af3495e..d6061afe6 100644 --- a/src/test/java/com/checkout/CheckoutSdkTelemetryIntegrationTest.java +++ b/src/test/java/com/checkout/CheckoutSdkTelemetryIntegrationTest.java @@ -4,6 +4,7 @@ import lombok.val; import org.apache.http.Header; import org.apache.http.ProtocolVersion; +import org.apache.http.HttpRequestInterceptor; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.entity.StringEntity; @@ -57,6 +58,7 @@ private CloseableHttpClient setupHttpClientMock() throws Exception { private CheckoutApi buildCheckoutApi(CloseableHttpClient httpClientMock, boolean telemetryEnabled) { HttpClientBuilder httpClientBuilderMock = mock(HttpClientBuilder.class); when(httpClientBuilderMock.setRedirectStrategy(any())).thenReturn(httpClientBuilderMock); + when(httpClientBuilderMock.addInterceptorLast(any(HttpRequestInterceptor.class))).thenReturn(httpClientBuilderMock); when(httpClientBuilderMock.build()).thenReturn(httpClientMock); return CheckoutSdk.builder() diff --git a/src/test/java/com/checkout/accounts/AccountsClientImplTest.java b/src/test/java/com/checkout/accounts/AccountsClientImplTest.java index 8015673c6..bc6ad2198 100644 --- a/src/test/java/com/checkout/accounts/AccountsClientImplTest.java +++ b/src/test/java/com/checkout/accounts/AccountsClientImplTest.java @@ -617,6 +617,43 @@ void shouldGetReserveRules() throws ExecutionException, InterruptedException { validateResponse(expectedResponse, future.get()); } + @Test + void shouldGetEntityRequirements() throws ExecutionException, InterruptedException { + final EntityRequirementListResponse expectedResponse = createEntityRequirementListResponse(); + + when(apiClient.getAsync("accounts/entities/entity_id/requirements", authorization, EntityRequirementListResponse.class)) + .thenReturn(CompletableFuture.completedFuture(expectedResponse)); + + final CompletableFuture future = accountsClient.getEntityRequirements("entity_id"); + + validateResponse(expectedResponse, future.get()); + } + + @Test + void shouldGetEntityRequirementDetails() throws ExecutionException, InterruptedException { + final EntityRequirementDetailsResponse expectedResponse = createEntityRequirementDetailsResponse(); + + when(apiClient.getAsync("accounts/entities/entity_id/requirements/requirement_id", authorization, EntityRequirementDetailsResponse.class)) + .thenReturn(CompletableFuture.completedFuture(expectedResponse)); + + final CompletableFuture future = accountsClient.getEntityRequirementDetails("entity_id", "requirement_id"); + + validateResponse(expectedResponse, future.get()); + } + + @Test + void shouldResolveEntityRequirement() throws ExecutionException, InterruptedException { + final EntityRequirementUpdateRequest request = EntityRequirementUpdateRequest.builder().value("Acme Holdings Limited").build(); + final EntityRequirementUpdateResponse expectedResponse = createEntityRequirementUpdateResponse(); + + when(apiClient.putAsync(eq("accounts/entities/entity_id/requirements/requirement_id"), eq(authorization), eq(EntityRequirementUpdateResponse.class), eq(request))) + .thenReturn(CompletableFuture.completedFuture(expectedResponse)); + + final CompletableFuture future = accountsClient.resolveEntityRequirement("entity_id", "requirement_id", request); + + validateResponse(expectedResponse, future.get()); + } + // Synchronous methods @Test void shouldGetEntityMembersSync() { @@ -692,6 +729,43 @@ void shouldGetReserveRulesSync() { validateResponse(expectedResponse, actualResponse); } + @Test + void shouldGetEntityRequirementsSync() { + final EntityRequirementListResponse expectedResponse = createEntityRequirementListResponse(); + + when(apiClient.get("accounts/entities/entity_id/requirements", authorization, EntityRequirementListResponse.class)) + .thenReturn(expectedResponse); + + final EntityRequirementListResponse actualResponse = accountsClient.getEntityRequirementsSync("entity_id"); + + validateResponse(expectedResponse, actualResponse); + } + + @Test + void shouldGetEntityRequirementDetailsSync() { + final EntityRequirementDetailsResponse expectedResponse = createEntityRequirementDetailsResponse(); + + when(apiClient.get("accounts/entities/entity_id/requirements/requirement_id", authorization, EntityRequirementDetailsResponse.class)) + .thenReturn(expectedResponse); + + final EntityRequirementDetailsResponse actualResponse = accountsClient.getEntityRequirementDetailsSync("entity_id", "requirement_id"); + + validateResponse(expectedResponse, actualResponse); + } + + @Test + void shouldResolveEntityRequirementSync() { + final EntityRequirementUpdateRequest request = EntityRequirementUpdateRequest.builder().value("Acme Holdings Limited").build(); + final EntityRequirementUpdateResponse expectedResponse = createEntityRequirementUpdateResponse(); + + when(apiClient.put(eq("accounts/entities/entity_id/requirements/requirement_id"), eq(authorization), eq(EntityRequirementUpdateResponse.class), eq(request))) + .thenReturn(expectedResponse); + + final EntityRequirementUpdateResponse actualResponse = accountsClient.resolveEntityRequirementSync("entity_id", "requirement_id", request); + + validateResponse(expectedResponse, actualResponse); + } + @Test void shouldThrowException_whenEntityIdIsNullGetEntityMembers() { try { @@ -776,6 +850,42 @@ void shouldThrowException_whenReserveRuleRequestIsNullCreateReserveRule() { verifyNoInteractions(apiClient); } + @Test + void shouldThrowException_whenEntityIdIsNullGetEntityRequirements() { + try { + accountsClient.getEntityRequirements(null); + fail(); + } catch (final CheckoutArgumentException checkoutArgumentException) { + assertEquals("entityId cannot be null", checkoutArgumentException.getMessage()); + } + + verifyNoInteractions(apiClient); + } + + @Test + void shouldThrowException_whenRequirementIdIsNullGetEntityRequirementDetails() { + try { + accountsClient.getEntityRequirementDetails("entity_id", null); + fail(); + } catch (final CheckoutArgumentException checkoutArgumentException) { + assertEquals("requirementId cannot be null", checkoutArgumentException.getMessage()); + } + + verifyNoInteractions(apiClient); + } + + @Test + void shouldThrowException_whenUpdateRequestIsNullResolveEntityRequirement() { + try { + accountsClient.resolveEntityRequirement("entity_id", "requirement_id", null); + fail(); + } catch (final CheckoutArgumentException checkoutArgumentException) { + assertEquals("updateRequest cannot be null", checkoutArgumentException.getMessage()); + } + + verifyNoInteractions(apiClient); + } + // Common methods private OnboardEntityRequest createOnboardEntityRequest() { return OnboardEntityRequest.builder().build(); @@ -882,6 +992,43 @@ private ReserveRuleCreateResponse createReserveRuleCreateResponse() { return mock(ReserveRuleCreateResponse.class); } + private EntityRequirementListResponse createEntityRequirementListResponse() { + return new EntityRequirementListResponse(java.util.Collections.singletonList( + new EntityRequirementListItem( + "requirement_id", + "entity_id", + "company", + EntityRequirementReason.PERIODIC_REVIEW, + EntityRequirementPriority.CRITICAL, + java.time.Instant.now().plusSeconds(3600), + "urn:object:company#entity_id#field:company.legal_name", + "company.legal_name", + "urn:field:company.legal_name", + java.util.Collections.emptyMap()))); + } + + private EntityRequirementDetailsResponse createEntityRequirementDetailsResponse() { + final EntityRequirementDetailsResponse response = new EntityRequirementDetailsResponse(); + response.setId("requirement_id"); + response.setResource("entity_id"); + response.setResourceType("company"); + response.setReason(EntityRequirementReason.PERIODIC_REVIEW); + response.setPriority(EntityRequirementPriority.CRITICAL); + response.setDeadline(java.time.Instant.now().plusSeconds(3600)); + response.setFieldPath("company.legal_name"); + response.setMessage("Provide the legal name for periodic review."); + response.setSchema(java.util.Collections.singletonMap("type", "string")); + return response; + } + + private EntityRequirementUpdateResponse createEntityRequirementUpdateResponse() { + final EntityRequirementUpdateResponse response = new EntityRequirementUpdateResponse(); + response.setId("requirement_id"); + response.setStatus(EntityRequirementUpdateStatus.PROCESSING); + response.setSubmittedAt(java.time.Instant.now()); + return response; + } + private void validateResponse(T expectedResponse, T actualResponse) { assertNotNull(actualResponse); assertEquals(expectedResponse, actualResponse); diff --git a/src/test/java/com/checkout/accounts/AccountsTestIT.java b/src/test/java/com/checkout/accounts/AccountsTestIT.java index 0348fdfd0..1641b6ddd 100644 --- a/src/test/java/com/checkout/accounts/AccountsTestIT.java +++ b/src/test/java/com/checkout/accounts/AccountsTestIT.java @@ -31,6 +31,7 @@ import java.net.URISyntaxException; import java.net.URL; import java.util.Collections; +import java.util.Objects; import static com.checkout.TestHelper.generateRandomEmail; import static java.util.Objects.requireNonNull; @@ -275,6 +276,44 @@ void shouldGetReserveRules() { validateReserveRulesResponse(response); } + @Disabled("Requires a sub-entity with pending requirements") + @Test + void shouldGetEntityRequirements() { + final String entityId = Objects.requireNonNull(System.getenv("CHECKOUT_DEFAULT_ENTITY_ID")); + + final EntityRequirementListResponse response = blocking(() -> checkoutApi.accountsClient().getEntityRequirements(entityId)); + validateEntityRequirementListResponse(response); + } + + @Disabled("Requires a sub-entity with a known requirement id") + @Test + void shouldGetEntityRequirementDetails() { + final String entityId = Objects.requireNonNull(System.getenv("CHECKOUT_DEFAULT_ENTITY_ID")); + final EntityRequirementListResponse listResponse = blocking(() -> checkoutApi.accountsClient().getEntityRequirements(entityId)); + assertNotNull(listResponse.getData()); + assertNotNull(listResponse.getData().get(0)); + + final String requirementId = listResponse.getData().get(0).getId(); + final EntityRequirementDetailsResponse detailsResponse = blocking(() -> checkoutApi.accountsClient().getEntityRequirementDetails(entityId, requirementId)); + validateEntityRequirementDetailsResponse(detailsResponse, requirementId); + } + + @Disabled("Requires a sub-entity with a resolvable requirement") + @Test + void shouldResolveEntityRequirement() { + final String entityId = Objects.requireNonNull(System.getenv("CHECKOUT_DEFAULT_ENTITY_ID")); + final EntityRequirementListResponse listResponse = blocking(() -> checkoutApi.accountsClient().getEntityRequirements(entityId)); + assertNotNull(listResponse.getData()); + assertNotNull(listResponse.getData().get(0)); + + final String requirementId = listResponse.getData().get(0).getId(); + final EntityRequirementUpdateRequest updateRequest = EntityRequirementUpdateRequest.builder() + .value("Acme Holdings Limited") + .build(); + final EntityRequirementUpdateResponse response = blocking(() -> checkoutApi.accountsClient().resolveEntityRequirement(entityId, requirementId, updateRequest)); + validateEntityRequirementUpdateResponse(response, requirementId); + } + // Synchronous methods @Test void shouldGetEntityMembersSync() { @@ -657,4 +696,22 @@ private static void validateReserveRulesResponse(final ReserveRulesResponse resp assertNotNull(response.getData()); } + private static void validateEntityRequirementListResponse(final EntityRequirementListResponse response) { + assertNotNull(response); + assertNotNull(response.getData()); + } + + private static void validateEntityRequirementDetailsResponse(final EntityRequirementDetailsResponse response, final String requirementId) { + assertNotNull(response); + assertEquals(requirementId, response.getId()); + assertNotNull(response.getResource()); + } + + private static void validateEntityRequirementUpdateResponse(final EntityRequirementUpdateResponse response, final String requirementId) { + assertNotNull(response); + assertEquals(requirementId, response.getId()); + assertEquals(EntityRequirementUpdateStatus.PROCESSING, response.getStatus()); + assertNotNull(response.getSubmittedAt()); + } + } \ No newline at end of file diff --git a/src/test/java/com/checkout/disputes/DisputesTestIT.java b/src/test/java/com/checkout/disputes/DisputesTestIT.java index 762c8a4bb..89ad133a4 100644 --- a/src/test/java/com/checkout/disputes/DisputesTestIT.java +++ b/src/test/java/com/checkout/disputes/DisputesTestIT.java @@ -16,6 +16,7 @@ import java.time.temporal.ChronoUnit; import org.apache.http.entity.ContentType; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import com.checkout.CheckoutApiException; @@ -130,6 +131,18 @@ void shouldTestFullDisputesWorkFlow() throws Exception { assertNotNull(compiledSubmittedEvidenceResponse.getFileId()); } + @Test + @Disabled("This test is disabled to avoid uploading files constantly, reactivate if needed for digging purposes") + void shouldUploadAndGetFileDetails() throws Exception { + final FileRequest fileRequest = createFileRequest(); + final IdResponse fileResponse = blocking(() -> checkoutApi.disputesClient().uploadFile(fileRequest)); + assertNotNull(fileResponse); + assertNotNull(fileResponse.getId()); + + final FileDetailsResponse fileDetailsResponse = blocking(() -> checkoutApi.disputesClient().getFileDetails(fileResponse.getId())); + validateFileDetailsResponse(fileRequest, fileDetailsResponse); + } + // Synchronous methods @Test void shouldQueryDisputesSync() { @@ -226,6 +239,18 @@ void shouldCreateAndUploadFileSync() throws Exception { assertNotNull(compiledSubmittedEvidenceResponse.getFileId()); } + @Test + @Disabled("This test is disabled to avoid uploading files constantly, reactivate if needed for digging purposes") + void shouldUploadAndGetFileDetailsSync() throws Exception { + final FileRequest fileRequest = createFileRequest(); + final IdResponse fileResponse = checkoutApi.disputesClient().uploadFileSync(fileRequest); + assertNotNull(fileResponse); + assertNotNull(fileResponse.getId()); + + final FileDetailsResponse fileDetailsResponse = checkoutApi.disputesClient().getFileDetailsSync(fileResponse.getId()); + validateFileDetailsResponse(fileRequest, fileDetailsResponse); + } + // Common methods private DisputesQueryFilter createDisputesQueryFilter() { return DisputesQueryFilter diff --git a/src/test/java/com/checkout/instruments/InstrumentsClientImplTest.java b/src/test/java/com/checkout/instruments/InstrumentsClientImplTest.java index e934c0c11..8ed713cec 100644 --- a/src/test/java/com/checkout/instruments/InstrumentsClientImplTest.java +++ b/src/test/java/com/checkout/instruments/InstrumentsClientImplTest.java @@ -122,6 +122,19 @@ void shouldDeleteInstrument() throws ExecutionException, InterruptedException { validateResponse(expectedResponse, actualResponse); } + @Test + void shouldRevokeInstrument() throws ExecutionException, InterruptedException { + final EmptyResponse expectedResponse = Mockito.mock(EmptyResponse.class); + + when(apiClient.patchAsync(eq(INSTRUMENTS + "/" + "123" + "/revoke"), any(SdkAuthorization.class), eq(EmptyResponse.class), isNull(), isNull())) + .thenReturn(CompletableFuture.completedFuture(expectedResponse)); + + final CompletableFuture future = instrumentsClient.revoke("123"); + final EmptyResponse actualResponse = future.get(); + + validateResponse(expectedResponse, actualResponse); + } + @Test void shouldGetBankAccountFieldFormatting() throws ExecutionException, InterruptedException { when(sdkCredentials.getAuthorization(SdkAuthorizationType.OAUTH)).thenReturn(authorization); @@ -188,6 +201,18 @@ void shouldDeleteInstrumentSync() { validateResponse(expectedResponse, actualResponse); } + @Test + void shouldRevokeInstrumentSync() { + final EmptyResponse expectedResponse = Mockito.mock(EmptyResponse.class); + + when(apiClient.patch(eq(INSTRUMENTS + "/" + "123" + "/revoke"), any(SdkAuthorization.class), eq(EmptyResponse.class), isNull(), isNull())) + .thenReturn(expectedResponse); + + final EmptyResponse actualResponse = instrumentsClient.revokeSync("123"); + + validateResponse(expectedResponse, actualResponse); + } + @Test void shouldGetBankAccountFieldFormattingSync() { when(sdkCredentials.getAuthorization(SdkAuthorizationType.OAUTH)).thenReturn(authorization); diff --git a/src/test/java/com/checkout/instruments/InstrumentsTestIT.java b/src/test/java/com/checkout/instruments/InstrumentsTestIT.java index d9e00c68f..948bf717e 100644 --- a/src/test/java/com/checkout/instruments/InstrumentsTestIT.java +++ b/src/test/java/com/checkout/instruments/InstrumentsTestIT.java @@ -62,6 +62,19 @@ void shouldDeleteInstrument() { assertNotFound(checkoutApi.instrumentsClient().get(createResponse.getId())); } + @Disabled("Sandbox instrument revoke currently returns 503 from VaultExternalApi in this environment") + @Test + void shouldRevokeInstrument() { + final String customerId = createCustomer(); + + final CreateInstrumentTokenRequest request = createTokenInstrumentRequest(customerId); + final CreateInstrumentTokenResponse createResponse = blocking(() -> checkoutApi.instrumentsClient().create(request)); + validateInstrumentCreation(createResponse); + + final EmptyResponse revokeResponse = blocking(() -> checkoutApi.instrumentsClient().revoke(createResponse.getId())); + assertNotNull(revokeResponse); + } + @Test @Disabled("Requires OAuth configuration") void shouldGetBankAccountFieldFormatting() { @@ -111,6 +124,19 @@ void shouldDeleteInstrumentSync() { assertNotFound(checkoutApi.instrumentsClient().get(createResponse.getId())); } + @Disabled("Sandbox instrument revoke currently returns 503 from VaultExternalApi in this environment") + @Test + void shouldRevokeInstrumentSync() { + final String customerId = createCustomer(); + + final CreateInstrumentTokenRequest request = createTokenInstrumentRequest(customerId); + final CreateInstrumentTokenResponse createResponse = checkoutApi.instrumentsClient().createSync(request); + validateInstrumentCreation(createResponse); + + final EmptyResponse revokeResponse = checkoutApi.instrumentsClient().revokeSync(createResponse.getId()); + assertNotNull(revokeResponse); + } + @Test @Disabled("Requires OAuth configuration") void shouldGetBankAccountFieldFormattingSync() { diff --git a/src/test/java/com/checkout/networkTokens/NetworkTokensClientImplTest.java b/src/test/java/com/checkout/networkTokens/NetworkTokensClientImplTest.java index 516b5e57a..12c0614a4 100644 --- a/src/test/java/com/checkout/networkTokens/NetworkTokensClientImplTest.java +++ b/src/test/java/com/checkout/networkTokens/NetworkTokensClientImplTest.java @@ -1,4 +1,4 @@ -package com.checkout.networkTokens; +package com.checkout.networktokens; import com.checkout.ApiClient; import com.checkout.CheckoutConfiguration; @@ -6,8 +6,6 @@ import com.checkout.SdkAuthorization; import com.checkout.SdkAuthorizationType; import com.checkout.SdkCredentials; -import com.checkout.networktokens.NetworkTokensClient; -import com.checkout.networktokens.NetworkTokensClientImpl; import com.checkout.networktokens.entities.DeletionReason; import com.checkout.networktokens.entities.IdNetworkTokenSource; import com.checkout.networktokens.entities.InitiatedBy; diff --git a/src/test/java/com/checkout/networkTokens/NetworkTokensTestIT.java b/src/test/java/com/checkout/networkTokens/NetworkTokensTestIT.java index 483497b7b..7a8569196 100644 --- a/src/test/java/com/checkout/networkTokens/NetworkTokensTestIT.java +++ b/src/test/java/com/checkout/networkTokens/NetworkTokensTestIT.java @@ -1,4 +1,4 @@ -package com.checkout.networkTokens; +package com.checkout.networktokens; import com.checkout.EmptyResponse; import com.checkout.PlatformType; diff --git a/src/test/java/com/checkout/onboardingsimulator/OnboardingSimulatorClientImplTest.java b/src/test/java/com/checkout/onboardingsimulator/OnboardingSimulatorClientImplTest.java new file mode 100644 index 000000000..02e4dc6fc --- /dev/null +++ b/src/test/java/com/checkout/onboardingsimulator/OnboardingSimulatorClientImplTest.java @@ -0,0 +1,306 @@ +package com.checkout.onboardingsimulator; + +import com.checkout.ApiClient; +import com.checkout.CheckoutArgumentException; +import com.checkout.CheckoutConfiguration; +import com.checkout.ItemsResponse; +import com.checkout.SdkAuthorization; +import com.checkout.SdkAuthorizationType; +import com.checkout.SdkCredentials; +import com.checkout.onboardingsimulator.entities.SimulatorAvailableRequirement; +import com.checkout.onboardingsimulator.entities.SimulatorEntityStatus; +import com.checkout.onboardingsimulator.entities.SimulatorScenario; +import com.checkout.onboardingsimulator.requests.SimulatorSetRequirementsDueRequest; +import com.checkout.onboardingsimulator.requests.SimulatorSetStatusRequest; +import com.checkout.onboardingsimulator.responses.SimulatorRunScenarioResponse; +import com.checkout.onboardingsimulator.responses.SimulatorSetRequirementsDueResponse; +import com.checkout.onboardingsimulator.responses.SimulatorSetStatusResponse; +import com.google.gson.reflect.TypeToken; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.lang.reflect.Type; +import java.util.Collections; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.fail; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class OnboardingSimulatorClientImplTest { + + private static final String ENTITY_ID = "ent_w4jelhppmfiufdnatam37wrfc4"; + private static final String SCENARIO_ID = "go_active"; + private static final Type AVAILABLE_REQUIREMENTS_TYPE = new TypeToken>() { } + .getType(); + private static final Type SCENARIOS_TYPE = new TypeToken>() { } + .getType(); + + @Mock + private ApiClient apiClient; + + @Mock + private CheckoutConfiguration configuration; + + @Mock + private SdkCredentials sdkCredentials; + + @Mock + private SdkAuthorization authorization; + + private OnboardingSimulatorClient client; + + @BeforeEach + void setUp() { + lenient().when(sdkCredentials.getAuthorization(SdkAuthorizationType.OAUTH)).thenReturn(authorization); + lenient().when(configuration.getSdkCredentials()).thenReturn(sdkCredentials); + client = new OnboardingSimulatorClientImpl(apiClient, configuration); + } + + @Test + void shouldSetRequirementsDue() throws ExecutionException, InterruptedException { + final SimulatorSetRequirementsDueRequest request = createSetRequirementsDueRequest(); + final SimulatorSetRequirementsDueResponse expectedResponse = createSetRequirementsDueResponse(); + + when(apiClient.postAsync(eq("simulate/entities/" + ENTITY_ID + "/requirements-due"), eq(authorization), + eq(SimulatorSetRequirementsDueResponse.class), eq(request), isNull())) + .thenReturn(CompletableFuture.completedFuture(expectedResponse)); + + final SimulatorSetRequirementsDueResponse actualResponse = client.setRequirementsDue(ENTITY_ID, request).get(); + + validateResponse(expectedResponse, actualResponse); + } + + @Test + void shouldRunScenario() throws ExecutionException, InterruptedException { + final SimulatorRunScenarioResponse expectedResponse = createRunScenarioResponse(); + + when(apiClient.postAsync(eq("simulate/entities/" + ENTITY_ID + "/scenarios/" + SCENARIO_ID), eq(authorization), + eq(SimulatorRunScenarioResponse.class), isNull(), isNull())) + .thenReturn(CompletableFuture.completedFuture(expectedResponse)); + + final SimulatorRunScenarioResponse actualResponse = client.runScenario(ENTITY_ID, SCENARIO_ID).get(); + + validateResponse(expectedResponse, actualResponse); + } + + @Test + void shouldSetEntityStatus() throws ExecutionException, InterruptedException { + final SimulatorSetStatusRequest request = createSetStatusRequest(); + final SimulatorSetStatusResponse expectedResponse = createSetStatusResponse(); + + when(apiClient.postAsync(eq("simulate/entities/" + ENTITY_ID + "/status"), eq(authorization), + eq(SimulatorSetStatusResponse.class), eq(request), isNull())) + .thenReturn(CompletableFuture.completedFuture(expectedResponse)); + + final SimulatorSetStatusResponse actualResponse = client.setEntityStatus(ENTITY_ID, request).get(); + + validateResponse(expectedResponse, actualResponse); + } + + @Test + void shouldListAvailableRequirements() throws ExecutionException, InterruptedException { + final ItemsResponse expectedResponse = createAvailableRequirementsResponse(); + + when(apiClient.getAsync(eq("simulate/requirements-due"), eq(authorization), eq(AVAILABLE_REQUIREMENTS_TYPE))) + .thenReturn(CompletableFuture.completedFuture(expectedResponse)); + + final ItemsResponse actualResponse = client.listAvailableRequirements().get(); + + validateResponse(expectedResponse, actualResponse); + } + + @Test + void shouldListScenarios() throws ExecutionException, InterruptedException { + final ItemsResponse expectedResponse = createScenariosResponse(); + + when(apiClient.getAsync(eq("simulate/scenarios"), eq(authorization), eq(SCENARIOS_TYPE))) + .thenReturn(CompletableFuture.completedFuture(expectedResponse)); + + final ItemsResponse actualResponse = client.listScenarios().get(); + + validateResponse(expectedResponse, actualResponse); + } + + @Test + void shouldSetRequirementsDueSync() { + final SimulatorSetRequirementsDueRequest request = createSetRequirementsDueRequest(); + final SimulatorSetRequirementsDueResponse expectedResponse = createSetRequirementsDueResponse(); + + when(apiClient.post(eq("simulate/entities/" + ENTITY_ID + "/requirements-due"), eq(authorization), + eq(SimulatorSetRequirementsDueResponse.class), eq(request), isNull())) + .thenReturn(expectedResponse); + + final SimulatorSetRequirementsDueResponse actualResponse = client.setRequirementsDueSync(ENTITY_ID, request); + + validateResponse(expectedResponse, actualResponse); + } + + @Test + void shouldRunScenarioSync() { + final SimulatorRunScenarioResponse expectedResponse = createRunScenarioResponse(); + + when(apiClient.post(eq("simulate/entities/" + ENTITY_ID + "/scenarios/" + SCENARIO_ID), eq(authorization), + eq(SimulatorRunScenarioResponse.class), isNull(), isNull())) + .thenReturn(expectedResponse); + + final SimulatorRunScenarioResponse actualResponse = client.runScenarioSync(ENTITY_ID, SCENARIO_ID); + + validateResponse(expectedResponse, actualResponse); + } + + @Test + void shouldSetEntityStatusSync() { + final SimulatorSetStatusRequest request = createSetStatusRequest(); + final SimulatorSetStatusResponse expectedResponse = createSetStatusResponse(); + + when(apiClient.post(eq("simulate/entities/" + ENTITY_ID + "/status"), eq(authorization), + eq(SimulatorSetStatusResponse.class), eq(request), isNull())) + .thenReturn(expectedResponse); + + final SimulatorSetStatusResponse actualResponse = client.setEntityStatusSync(ENTITY_ID, request); + + validateResponse(expectedResponse, actualResponse); + } + + @Test + void shouldListAvailableRequirementsSync() { + final ItemsResponse expectedResponse = createAvailableRequirementsResponse(); + + when(apiClient.get(eq("simulate/requirements-due"), eq(authorization), eq(AVAILABLE_REQUIREMENTS_TYPE))) + .thenReturn(expectedResponse); + + final ItemsResponse actualResponse = client.listAvailableRequirementsSync(); + + validateResponse(expectedResponse, actualResponse); + } + + @Test + void shouldListScenariosSync() { + final ItemsResponse expectedResponse = createScenariosResponse(); + + when(apiClient.get(eq("simulate/scenarios"), eq(authorization), eq(SCENARIOS_TYPE))) + .thenReturn(expectedResponse); + + final ItemsResponse actualResponse = client.listScenariosSync(); + + validateResponse(expectedResponse, actualResponse); + } + + @Test + void shouldThrowExceptionWhenEntityIdIsNullForSetRequirementsDue() { + try { + client.setRequirementsDue(null, createSetRequirementsDueRequest()); + fail(); + } catch (final CheckoutArgumentException exception) { + assertEquals("entityId cannot be null", exception.getMessage()); + } + + verifyNoInteractions(apiClient); + } + + @Test + void shouldThrowExceptionWhenRequestIsNullForSetRequirementsDue() { + try { + client.setRequirementsDue(ENTITY_ID, null); + fail(); + } catch (final CheckoutArgumentException exception) { + assertEquals("request cannot be null", exception.getMessage()); + } + + verifyNoInteractions(apiClient); + } + + @Test + void shouldThrowExceptionWhenScenarioIdIsNull() { + try { + client.runScenario(ENTITY_ID, null); + fail(); + } catch (final CheckoutArgumentException exception) { + assertEquals("scenarioId cannot be null", exception.getMessage()); + } + + verifyNoInteractions(apiClient); + } + + @Test + void shouldThrowExceptionWhenRequestIsNullForSetEntityStatus() { + try { + client.setEntityStatus(ENTITY_ID, null); + fail(); + } catch (final CheckoutArgumentException exception) { + assertEquals("request cannot be null", exception.getMessage()); + } + + verifyNoInteractions(apiClient); + } + + private SimulatorSetRequirementsDueRequest createSetRequirementsDueRequest() { + return SimulatorSetRequirementsDueRequest.builder() + .fields(Collections.singletonList("individual.identification.document")) + .build(); + } + + private SimulatorSetStatusRequest createSetStatusRequest() { + return SimulatorSetStatusRequest.builder() + .status(SimulatorEntityStatus.ACTIVE) + .build(); + } + + private SimulatorSetRequirementsDueResponse createSetRequirementsDueResponse() { + return new SimulatorSetRequirementsDueResponse( + ENTITY_ID, + "active", + "requirements_due", + Collections.singletonList("individual.identification.document")); + } + + private SimulatorRunScenarioResponse createRunScenarioResponse() { + return new SimulatorRunScenarioResponse( + ENTITY_ID, + SCENARIO_ID, + "Go Active", + "requirements_due", + "active", + Collections.emptyList()); + } + + private SimulatorSetStatusResponse createSetStatusResponse() { + return new SimulatorSetStatusResponse(ENTITY_ID, "pending", "active"); + } + + private ItemsResponse createAvailableRequirementsResponse() { + final ItemsResponse response = new ItemsResponse<>(); + response.setItems(Collections.singletonList(new SimulatorAvailableRequirement( + "individual.identification.document", + "string"))); + return response; + } + + private ItemsResponse createScenariosResponse() { + final ItemsResponse response = new ItemsResponse<>(); + response.setItems(Collections.singletonList(new SimulatorScenario( + SCENARIO_ID, + "Go Active", + "Moves the entity to active status", + "set_status", + "active", + Collections.emptyList()))); + return response; + } + + private void validateResponse(final T expectedResponse, final T actualResponse) { + assertNotNull(actualResponse); + assertEquals(expectedResponse, actualResponse); + } +} \ No newline at end of file diff --git a/src/test/java/com/checkout/onboardingsimulator/OnboardingSimulatorTestIT.java b/src/test/java/com/checkout/onboardingsimulator/OnboardingSimulatorTestIT.java new file mode 100644 index 000000000..5e5afbfaf --- /dev/null +++ b/src/test/java/com/checkout/onboardingsimulator/OnboardingSimulatorTestIT.java @@ -0,0 +1,96 @@ +package com.checkout.onboardingsimulator; + +import com.checkout.ItemsResponse; +import com.checkout.PlatformType; +import com.checkout.SandboxTestFixture; +import com.checkout.onboardingsimulator.entities.SimulatorAvailableRequirement; +import com.checkout.onboardingsimulator.entities.SimulatorEntityStatus; +import com.checkout.onboardingsimulator.entities.SimulatorScenario; +import com.checkout.onboardingsimulator.requests.SimulatorSetRequirementsDueRequest; +import com.checkout.onboardingsimulator.requests.SimulatorSetStatusRequest; +import com.checkout.onboardingsimulator.responses.SimulatorRunScenarioResponse; +import com.checkout.onboardingsimulator.responses.SimulatorSetRequirementsDueResponse; +import com.checkout.onboardingsimulator.responses.SimulatorSetStatusResponse; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import java.util.Collections; +import java.util.Objects; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +class OnboardingSimulatorTestIT extends SandboxTestFixture { + + OnboardingSimulatorTestIT() { + super(PlatformType.DEFAULT_OAUTH); + } + + @Disabled("Sandbox-only. Requires an existing sub-entity ID") + @Test + void shouldListAvailableRequirements() { + final ItemsResponse response = blocking(() -> checkoutApi.onboardingSimulatorClient().listAvailableRequirements()); + + assertNotNull(response); + assertNotNull(response.getItems()); + assertFalse(response.getItems().isEmpty()); + assertNotNull(response.getItems().get(0).getField()); + } + + @Disabled("Sandbox-only. Requires an existing sub-entity ID") + @Test + void shouldListScenarios() { + final ItemsResponse response = blocking(() -> checkoutApi.onboardingSimulatorClient().listScenarios()); + + assertNotNull(response); + assertNotNull(response.getItems()); + assertFalse(response.getItems().isEmpty()); + assertNotNull(response.getItems().get(0).getId()); + } + + @Disabled("Sandbox-only. Requires an existing sub-entity ID") + @Test + void shouldSetRequirementsDue() { + final String entityId = Objects.requireNonNull(System.getenv("CHECKOUT_DEFAULT_ENTITY_ID")); + final SimulatorSetRequirementsDueRequest request = SimulatorSetRequirementsDueRequest.builder() + .fields(Collections.singletonList("individual.identification.document")) + .build(); + + final SimulatorSetRequirementsDueResponse response = blocking(() -> checkoutApi.onboardingSimulatorClient().setRequirementsDue(entityId, request)); + + assertNotNull(response); + assertEquals(entityId, response.getEntityId()); + assertNotNull(response.getCurrentStatus()); + assertNotNull(response.getRequirementsDue()); + } + + @Disabled("Sandbox-only. Requires an existing sub-entity ID") + @Test + void shouldRunScenario() { + final String entityId = Objects.requireNonNull(System.getenv("CHECKOUT_DEFAULT_ENTITY_ID")); + final String scenarioId = "go_active"; + + final SimulatorRunScenarioResponse response = blocking(() -> checkoutApi.onboardingSimulatorClient().runScenario(entityId, scenarioId)); + + assertNotNull(response); + assertEquals(entityId, response.getEntityId()); + assertEquals(scenarioId, response.getScenarioId()); + assertNotNull(response.getCurrentStatus()); + } + + @Disabled("Sandbox-only. Requires an existing sub-entity ID") + @Test + void shouldSetEntityStatus() { + final String entityId = Objects.requireNonNull(System.getenv("CHECKOUT_DEFAULT_ENTITY_ID")); + final SimulatorSetStatusRequest request = SimulatorSetStatusRequest.builder() + .status(SimulatorEntityStatus.ACTIVE) + .build(); + + final SimulatorSetStatusResponse response = blocking(() -> checkoutApi.onboardingSimulatorClient().setEntityStatus(entityId, request)); + + assertNotNull(response); + assertEquals(entityId, response.getEntityId()); + assertNotNull(response.getCurrentStatus()); + } +} \ No newline at end of file diff --git a/src/test/java/com/checkout/tokens/TokensClientImplTest.java b/src/test/java/com/checkout/tokens/TokensClientImplTest.java index 78103db38..f40895938 100644 --- a/src/test/java/com/checkout/tokens/TokensClientImplTest.java +++ b/src/test/java/com/checkout/tokens/TokensClientImplTest.java @@ -127,6 +127,22 @@ void shouldRequestGooglePayToken() throws ExecutionException, InterruptedExcepti validateResponse(expectedResponse, actualResponse); } + @Test + void shouldGetTokenMetadata() throws ExecutionException, InterruptedException { + final String tokenId = "tok_4gzeau5o2uqubbk6fudbloo47a"; + final TokenMetadataResponse expectedResponse = mock(TokenMetadataResponse.class); + + when(sdkCredentials.getAuthorization(SdkAuthorizationType.SECRET_KEY_OR_OAUTH)).thenReturn(authorization); + when(configuration.getSdkCredentials()).thenReturn(sdkCredentials); + when(apiClient.getAsync(eq("tokens/" + tokenId + "/metadata"), eq(authorization), eq(TokenMetadataResponse.class))) + .thenReturn(CompletableFuture.completedFuture(expectedResponse)); + + final CompletableFuture future = client.getTokenMetadata(tokenId); + final TokenMetadataResponse actualResponse = future.get(); + + validateResponse(expectedResponse, actualResponse); + } + // Synchronous methods @Test void shouldRequestCardTokenSync() { @@ -170,6 +186,21 @@ void shouldRequestGooglePayTokenSync() { validateResponse(expectedResponse, actualResponse); } + @Test + void shouldGetTokenMetadataSync() { + final String tokenId = "tok_4gzeau5o2uqubbk6fudbloo47a"; + final TokenMetadataResponse expectedResponse = mock(TokenMetadataResponse.class); + + when(sdkCredentials.getAuthorization(SdkAuthorizationType.SECRET_KEY_OR_OAUTH)).thenReturn(authorization); + when(configuration.getSdkCredentials()).thenReturn(sdkCredentials); + when(apiClient.get(eq("tokens/" + tokenId + "/metadata"), eq(authorization), eq(TokenMetadataResponse.class))) + .thenReturn(expectedResponse); + + final TokenMetadataResponse actualResponse = client.getTokenMetadataSync(tokenId); + + validateResponse(expectedResponse, actualResponse); + } + // Common methods private void setUpAuthorizationMocks() { when(sdkCredentials.getAuthorization(SdkAuthorizationType.PUBLIC_KEY)).thenReturn(authorization); diff --git a/src/test/java/com/checkout/tokens/TokensTestIT.java b/src/test/java/com/checkout/tokens/TokensTestIT.java index e730c3db8..40784055e 100644 --- a/src/test/java/com/checkout/tokens/TokensTestIT.java +++ b/src/test/java/com/checkout/tokens/TokensTestIT.java @@ -5,6 +5,7 @@ import com.checkout.TestCardSource; import com.checkout.common.CardCategory; import com.checkout.common.CardType; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import java.time.Instant; @@ -31,6 +32,21 @@ void shouldRequestCardToken() { validateCardTokenResponse(response); } + @Disabled("Sandbox token metadata lookup currently returns 404 for generated tokens in this environment") + @Test + void shouldGetTokenMetadata() { + final CardTokenRequest request = CardTokenRequest.builder() + .number(TestCardSource.VISA.getNumber()) + .expiryMonth(TestCardSource.VISA.getExpiryMonth()) + .expiryYear(TestCardSource.VISA.getExpiryYear()) + .build(); + final CardTokenResponse tokenResponse = blocking(() -> checkoutApi.tokensClient().requestCardToken(request)); + + final TokenMetadataResponse metadataResponse = blocking(() -> checkoutApi.tokensClient().getTokenMetadata(tokenResponse.getToken())); + + validateTokenMetadataResponse(metadataResponse, tokenResponse.getToken()); + } + // Synchronous test methods @Test void shouldRequestCardTokenSync() { @@ -44,6 +60,21 @@ void shouldRequestCardTokenSync() { validateCardTokenResponse(response); } + @Disabled("Sandbox token metadata lookup currently returns 404 for generated tokens in this environment") + @Test + void shouldGetTokenMetadataSync() { + final CardTokenRequest request = CardTokenRequest.builder() + .number(TestCardSource.VISA.getNumber()) + .expiryMonth(TestCardSource.VISA.getExpiryMonth()) + .expiryYear(TestCardSource.VISA.getExpiryYear()) + .build(); + final CardTokenResponse tokenResponse = checkoutApi.tokensClient().requestCardTokenSync(request); + + final TokenMetadataResponse metadataResponse = checkoutApi.tokensClient().getTokenMetadataSync(tokenResponse.getToken()); + + validateTokenMetadataResponse(metadataResponse, tokenResponse.getToken()); + } + // Common validation methods private void validateCardTokenResponse(CardTokenResponse response) { assertNotNull(response); @@ -65,4 +96,18 @@ private void validateCardTokenResponse(CardTokenResponse response) { //assertEquals("Visa Traditional", response.getProductType()); } + private void validateTokenMetadataResponse(TokenMetadataResponse response, String tokenId) { + assertNotNull(response); + assertEquals(tokenId, response.getToken()); + assertEquals("card", response.getType()); + assertNotNull(response.getExpiresOn()); + assertEquals(6, response.getExpiryMonth().intValue()); + assertEquals(2025, response.getExpiryYear().intValue()); + assertEquals("VISA", response.getScheme()); + assertEquals("4242", response.getLast4()); + assertNotNull(response.getBin()); + assertNotNull(response.getCardType()); + assertNotNull(response.getCardCategory()); + } + } \ No newline at end of file