diff --git a/src/main/java/com/checkout/common/ChallengeIndicator.java b/src/main/java/com/checkout/common/ChallengeIndicator.java index a9f531d6..73dc5f14 100644 --- a/src/main/java/com/checkout/common/ChallengeIndicator.java +++ b/src/main/java/com/checkout/common/ChallengeIndicator.java @@ -3,52 +3,99 @@ import com.google.gson.annotations.SerializedName; /** - * Indicates whether a challenge is requested for an authentication. + * Indicates the preference for whether or not a 3DS challenge should be performed. The customer's + * bank has the final say on whether or not the customer receives the challenge. *

- * The first four values are valid for all endpoints that accept a challenge indicator - * (e.g. {@code POST /payments} {@code three_ds.challenge_indicator}, {@code POST /sessions}, - * and their responses). + * This is the four-value indicator accepted by the {@code 3ds.challenge_indicator} field on + * {@code POST /payments}, {@code POST /hosted-payments}, {@code POST /payment-links} and + * {@code POST /payment-sessions}. *

- * The remaining values ({@link #LOW_VALUE}, {@link #TRUSTED_LISTING}, - * {@link #TRUSTED_LISTING_PROMPT}, {@link #TRANSACTION_RISK_ASSESSMENT}, {@link #DATA_SHARE}) - * represent requests for exemption and are only valid for {@code POST /sessions} - * (3DS Standalone Authentication). If an exemption cannot be applied, the value - * {@link #NO_CHALLENGE_REQUESTED} will be used instead. + * [Optional] + *

+ * Default: {@link #NO_PREFERENCE} + * + * @see com.checkout.sessions.SessionChallengeIndicator the wider nine-value enum accepted by + * {@code POST /sessions}, which additionally supports requests for exemption */ public enum ChallengeIndicator { + /** + * A challenge is requested for this payment. + */ @SerializedName("challenge_requested") CHALLENGE_REQUESTED, + + /** + * A challenge is requested for this payment because it is mandated by local regulation or + * scheme rules. + */ @SerializedName("challenge_requested_mandate") CHALLENGE_REQUESTED_MANDATE, + + /** + * A challenge is not requested for this payment. + */ @SerializedName("no_challenge_requested") NO_CHALLENGE_REQUESTED, + + /** + * No preference as to whether a challenge should be performed. This is the default. + */ @SerializedName("no_preference") NO_PREFERENCE, + /** - * Request a low-value exemption. Only valid for {@code POST /sessions}. + * Request a low-value exemption. + * + * @deprecated only valid for {@code POST /sessions}, which is now modelled by + * {@link com.checkout.sessions.SessionChallengeIndicator#LOW_VALUE}. This value is rejected by + * the {@code 3ds.challenge_indicator} fields that use this enum. */ + @Deprecated @SerializedName("low_value") LOW_VALUE, + /** - * Request a trusted listing exemption. Only valid for {@code POST /sessions}. + * Request a trusted listing exemption. + * + * @deprecated only valid for {@code POST /sessions}, which is now modelled by + * {@link com.checkout.sessions.SessionChallengeIndicator#TRUSTED_LISTING}. This value is + * rejected by the {@code 3ds.challenge_indicator} fields that use this enum. */ + @Deprecated @SerializedName("trusted_listing") TRUSTED_LISTING, + /** * Request a trusted listing prompt to add the merchant to the cardholder's trusted list. - * Only valid for {@code POST /sessions}. + * + * @deprecated only valid for {@code POST /sessions}, which is now modelled by + * {@link com.checkout.sessions.SessionChallengeIndicator#TRUSTED_LISTING_PROMPT}. This value is + * rejected by the {@code 3ds.challenge_indicator} fields that use this enum. */ + @Deprecated @SerializedName("trusted_listing_prompt") TRUSTED_LISTING_PROMPT, + /** - * Request a transaction risk analysis (TRA) exemption. Only valid for {@code POST /sessions}. + * Request a transaction risk analysis (TRA) exemption. + * + * @deprecated only valid for {@code POST /sessions}, which is now modelled by + * {@link com.checkout.sessions.SessionChallengeIndicator#TRANSACTION_RISK_ASSESSMENT}. This + * value is rejected by the {@code 3ds.challenge_indicator} fields that use this enum. */ + @Deprecated @SerializedName("transaction_risk_assessment") TRANSACTION_RISK_ASSESSMENT, + /** - * Indicates a data-share authentication request. Only valid for {@code POST /sessions}. + * Indicates a data-share authentication request. + * + * @deprecated only valid for {@code POST /sessions}, which is now modelled by + * {@link com.checkout.sessions.SessionChallengeIndicator#DATA_SHARE}. This value is rejected by + * the {@code 3ds.challenge_indicator} fields that use this enum. */ + @Deprecated @SerializedName("data_share") DATA_SHARE, diff --git a/src/main/java/com/checkout/sessions/CreateSessionAcceptedResponse.java b/src/main/java/com/checkout/sessions/CreateSessionAcceptedResponse.java index 35e52149..6aec2253 100644 --- a/src/main/java/com/checkout/sessions/CreateSessionAcceptedResponse.java +++ b/src/main/java/com/checkout/sessions/CreateSessionAcceptedResponse.java @@ -1,6 +1,5 @@ package com.checkout.sessions; -import com.checkout.common.ChallengeIndicator; import com.checkout.common.Currency; import com.checkout.common.Resource; import lombok.Data; @@ -15,48 +14,165 @@ @ToString(callSuper = true) public final class CreateSessionAcceptedResponse extends Resource { + /** + * Session unique identifier. + * [Required] + * ^(sid)_(\w{26})$ + * min 30 characters + * max 30 characters + */ private String id; + /** + * A base64 encoded value prefixed with {@code sek_} that gives access to client-side operations + * for a single authentication within the Sessions API. + * [Required] + * ^(sek)_(.{44})$ + * min 48 characters + * max 48 characters + */ private String sessionSecret; + /** + * The transaction identifier that needs to be provided when communicating directly with the + * Access Control Server (ACS). + * [Required] + * min 36 characters + * max 36 characters + */ private String transactionId; + /** + * Indicates the scheme this authentication is carried out against. + * [Required] + */ private SessionScheme scheme; + /** + * The amount in the minor currency. + * [Required] + * min 0 + * max 9007199254740991 + */ private Long amount; + /** + * The three-letter ISO currency code. + * [Required] + */ private Currency currency; + /** + * Indicates the type of payment this session is for. Please note the spelling of + * {@code installment} consists of two {@code l}s. + * [Required] + */ private AuthenticationType authenticationType; + /** + * Indicates the category of the authentication request. + * [Required] + */ private Category authenticationCategory; + /** + * The status of the session. + * [Required] + */ private SessionStatus status; + /** + * When the session is unavailable, this points to the reason why. + *

+ * Note: this field is not present in the {@code CreateSessionAcceptedResponse} schema of the + * Checkout.com API Reference, where it appears only on {@code GET /sessions/{id}}. It is + * retained for backwards compatibility pending confirmation from the API owners. + * [Optional] + */ private StatusReason statusReason; + /** + * Specifies which action to take in order to complete the session. + * The {@code redirect_cardholder} action is only applicable for hosted sessions. + * [Required] + */ private List nextActions; + /** + * The protocol version number of the specification used by the API for authentication. + * [Required] + * max 50 characters + */ private String protocolVersion; + /** + * Additional information about the cardholder's account. + * [Optional] + */ private CardholderAccountInfo accountInfo; + /** + * Additional information about the cardholder's purchase. + * [Optional] + */ private MerchantRiskInfo merchantRiskInfo; + /** + * A reference you can later use to identify this payment, such as an order number. + * [Optional] + * max 100 characters + */ private String reference; + /** + * Details related to the session source. This property should always be in the response, unless + * a {@code card} source was used and communication with Checkout.com's Vault was not possible. + * [Optional] + */ private CardInfo card; + /** + * Details of a recurring authentication. + * [Optional] + */ private Recurring recurring; + /** + * Details of an installment authentication. + * [Optional] + */ private Installment installment; + /** + * Details of a previous transaction. + * [Optional] + */ private InitialTransaction initialTransaction; + /** + * Authentication date and time. + * [Required] + * Format: date-time (RFC 3339) + */ private Instant authenticationDate; - private ChallengeIndicator challengeIndicator; - + /** + * Indicates the preference for whether or not a 3DS challenge should be performed. The + * customer's bank has the final say on whether or not the customer receives the challenge. + *

+ * Note: the API Reference specifies only the four base values for this response field, but the + * request accepts nine. This is typed as {@link SessionChallengeIndicator} so that an exemption + * value echoed back by the API still deserializes; see + * {@link SessionRequest#getChallengeIndicator()}. + * [Required] + * Default: {@link SessionChallengeIndicator#NO_PREFERENCE} + */ + private SessionChallengeIndicator challengeIndicator; + + /** + * The information about the optimization options selected. + * [Optional] + */ private Optimization optimization; } diff --git a/src/main/java/com/checkout/sessions/GetSessionResponse.java b/src/main/java/com/checkout/sessions/GetSessionResponse.java index 64019505..3d3364c4 100644 --- a/src/main/java/com/checkout/sessions/GetSessionResponse.java +++ b/src/main/java/com/checkout/sessions/GetSessionResponse.java @@ -1,6 +1,5 @@ package com.checkout.sessions; -import com.checkout.common.ChallengeIndicator; import com.checkout.common.Currency; import com.checkout.common.Resource; import com.checkout.common.ThreeDSFlowType; @@ -17,85 +16,298 @@ @ToString(callSuper = true) public class GetSessionResponse extends Resource { + /** + * Session unique identifier. + * [Required] + * ^(sid)_(\w{26})$ + * min 30 characters + * max 30 characters + */ private String id; + /** + * A base64 encoded value prefixed with {@code sek_} that gives access to client-side operations + * for a single authentication within the Sessions API. + * [Optional] + * ^(sek)_(.{44})$ + * min 48 characters + * max 48 characters + */ private String sessionSecret; + /** + * The transaction identifier that needs to be provided when communicating directly with the + * Access Control Server (ACS). + * [Required] + * min 36 characters + * max 36 characters + */ private String transactionId; + /** + * Indicates the scheme this authentication is carried out against. + * [Required] + */ private SessionScheme scheme; + /** + * The amount in the minor currency. + * [Required] + * min 0 + * max 9007199254740991 + */ private Long amount; + /** + * The three-letter ISO currency code. + * [Required] + */ private Currency currency; + /** + * Indicates whether this session has been completed. + * [Optional] + */ private Boolean completed; + /** + * Indicates whether this session involved a challenge. This will only be set after + * communication with the scheme is finished. + * [Optional] + */ private Boolean challenged; + /** + * Indicates the type of payment this session is for. Please note the spelling of + * {@code installment} consists of two {@code l}s. + * [Required] + */ private AuthenticationType authenticationType; + /** + * Indicates the category of the authentication request. + * [Required] + */ private Category authenticationCategory; + /** + * Public certificates specific to a Directory Server (DS) for encrypting device data and + * verifying ACS signed content. Required when the channel is {@code app}. + * [Optional] + */ private DsPublicKeys certificates; + /** + * Indicates the status of the session. + * [Required] + */ private SessionStatus status; + /** + * When the session is unavailable, this points to the reason why. For example, + * {@code ares_error} indicates there was an issue in the authentication response returned by + * the Directory Server, and {@code ares_status} indicates the status was set to the status in + * that authentication response. + * [Optional] + */ private StatusReason statusReason; + /** + * Whether the authentication was successful. This will only be set if the session is in a final + * state. + * [Optional] + */ private Boolean approved; + /** + * The protocol version number of the specification used by the API for authentication. + * [Required] + * max 50 characters + */ private String protocolVersion; + /** + * Additional information about the cardholder's account. + * [Optional] + */ @SerializedName("account_info") private CardholderAccountInfo cardholderAccountInfo; + /** + * Additional information about the cardholder's purchase. + * [Optional] + */ private MerchantRiskInfo merchantRiskInfo; + /** + * A reference you can later use to identify this payment, such as an order number. + * [Optional] + * max 100 characters + */ private String reference; + /** + * Identifies the type of transaction being authenticated. + * [Optional] + * Default: {@link TransactionType#GOODS_SERVICE} + * max 50 characters + */ private TransactionType transactionType; + /** + * Specifies which action to take in order to complete the session. + * The {@code redirect_cardholder} action is only applicable for hosted sessions. + * [Optional] + */ private List nextActions; + /** + * The directory server (DS) information. Can be empty if the session is pending or + * communication with the DS failed. + * [Optional] + */ private Ds ds; + /** + * The access control server (ACS) information. Can be empty if the session is still pending or + * if communication with the ACS failed. This will be available when the channel data and issuer + * fingerprint result have been provided. + * [Optional] + */ private Acs acs; + /** + * Only available as a result of a 3DS2 authentication. The response from the DS or ACS which + * indicates whether a transaction qualifies as an authenticated transaction or account + * verification. Only available if communication with the scheme was successful and the session + * is in a final state. + * [Optional] + */ private ResponseCode responseCode; + /** + * Only available as a result of a 3DS2 authentication. The response from the DS or ACS which + * provides information on why the {@link #responseCode} field has the specified value. Only + * available when {@link #responseCode} is not {@code Y}. + * [Optional] + */ private String responseStatusReason; + /** + * The 3DS1 payer authentication request message. + *

+ * Note: this field is not present in the {@code GetSessionResponse} schema of the Checkout.com + * API Reference. It is retained for backwards compatibility pending confirmation from the API + * owners. + * [Optional] + */ private String pareq; + /** + * Payment system-specific value provided as part of the ACS registration for each supported DS. + * This field is only included in responses when authenticating with a valid OAuth token, and not + * when authenticating with {@link #sessionSecret}. + * [Optional] + * min 28 characters + * max 28 characters + */ private String cryptogram; + /** + * Electronic Commerce Indicator. This field is only included in responses when authenticating + * with a valid OAuth token, and not when authenticating with {@link #sessionSecret}. + * [Optional] + * min 2 characters + * max 2 characters + */ private String eci; + /** + * The xid value to use for authorization. + * [Optional] + */ private String xid; + /** + * May provide cardholder information from the DS to be presented to the cardholder. + * [Optional] + */ private String cardholderInfo; + /** + * Details related to the session source. This property should always be in the response, unless + * a {@code card} source was used and communication with Checkout.com's Vault was not possible. + * [Optional] + */ private CardInfo card; + /** + * Details of a recurring authentication. + * [Optional] + */ private Recurring recurring; + /** + * Details of an installment authentication. + * [Optional] + */ private Installment installment; + /** + * Details of a previous transaction. + * [Optional] + */ private InitialTransaction initialTransaction; + /** + * Indicates the cardholder's IP address. Only available when the scheme selected is Cartes + * Bancaires. + * [Optional] + */ private String customerIp; + /** + * Authentication date and time. + * [Optional] + * Format: date-time (RFC 3339) + */ private Instant authenticationDate; + /** + * Details related to the exemption present in the 3DS flow. + * [Optional] + */ private ThreeDSExemption exemption; + /** + * Indicates whether the 3D Secure 2 authentication was challenged or frictionless. + * [Optional] + */ private ThreeDSFlowType flowType; - private ChallengeIndicator challengeIndicator; - + /** + * Indicates the preference for whether or not a 3DS challenge should be performed. The + * customer's bank has the final say on whether or not the customer receives the challenge. + *

+ * Note: the API Reference specifies only the four base values for this response field, but the + * request accepts nine. This is typed as {@link SessionChallengeIndicator} so that an exemption + * value echoed back by the API still deserializes; see + * {@link SessionRequest#getChallengeIndicator()}. + * [Required] + * Default: {@link SessionChallengeIndicator#NO_PREFERENCE} + */ + private SessionChallengeIndicator challengeIndicator; + + /** + * The information about the optimization options selected. + * [Optional] + */ private Optimization optimization; + /** + * Indicates scheme-specific information. + * [Optional] + */ private SchemeInfo schemeInfo; } diff --git a/src/main/java/com/checkout/sessions/SessionChallengeIndicator.java b/src/main/java/com/checkout/sessions/SessionChallengeIndicator.java new file mode 100644 index 00000000..fe51d55f --- /dev/null +++ b/src/main/java/com/checkout/sessions/SessionChallengeIndicator.java @@ -0,0 +1,89 @@ +package com.checkout.sessions; + +import com.google.gson.annotations.SerializedName; + +/** + * Indicates whether a challenge is requested for this session. + *

+ * Used by {@link SessionRequest#getChallengeIndicator()} for {@code POST /sessions} + * (3DS Standalone Authentication). This is the only field in the API that accepts the + * exemption values below; the {@code 3ds.challenge_indicator} field on payments, hosted + * payments, payment links and payment sessions accepts only the first four values and is + * modelled by {@link com.checkout.common.ChallengeIndicator}. + *

+ * The following are requests for exemption: + * {@link #LOW_VALUE}, {@link #TRUSTED_LISTING}, {@link #TRUSTED_LISTING_PROMPT} and + * {@link #TRANSACTION_RISK_ASSESSMENT}. If an exemption cannot be applied, then the value + * {@link #NO_CHALLENGE_REQUESTED} will be used instead. + *

+ * [Optional] + *

+ * Default: {@link #NO_PREFERENCE} + *

+ * max 50 characters + */ +public enum SessionChallengeIndicator { + + /** + * No preference as to whether a challenge should be performed. This is the default. + */ + @SerializedName("no_preference") + NO_PREFERENCE, + + /** + * A challenge is not requested for this session. + */ + @SerializedName("no_challenge_requested") + NO_CHALLENGE_REQUESTED, + + /** + * A challenge is requested for this session. + */ + @SerializedName("challenge_requested") + CHALLENGE_REQUESTED, + + /** + * A challenge is requested for this session because it is mandated by local regulation + * or scheme rules. + */ + @SerializedName("challenge_requested_mandate") + CHALLENGE_REQUESTED_MANDATE, + + /** + * Request a low-value exemption. If the exemption cannot be applied, the value + * {@link #NO_CHALLENGE_REQUESTED} will be used instead. + */ + @SerializedName("low_value") + LOW_VALUE, + + /** + * Request a trusted listing exemption, applied when the cardholder has already added the + * merchant to their list of trusted beneficiaries. If the exemption cannot be applied, the + * value {@link #NO_CHALLENGE_REQUESTED} will be used instead. + */ + @SerializedName("trusted_listing") + TRUSTED_LISTING, + + /** + * Request a trusted listing exemption and prompt the cardholder to add the merchant to their + * list of trusted beneficiaries. If the exemption cannot be applied, the value + * {@link #NO_CHALLENGE_REQUESTED} will be used instead. + */ + @SerializedName("trusted_listing_prompt") + TRUSTED_LISTING_PROMPT, + + /** + * Request a transaction risk analysis (TRA) exemption. If the exemption cannot be applied, + * the value {@link #NO_CHALLENGE_REQUESTED} will be used instead. + */ + @SerializedName("transaction_risk_assessment") + TRANSACTION_RISK_ASSESSMENT, + + /** + * Request a data-share authentication, where cardholder data is shared with the issuer to + * support their risk assessment without requesting a challenge. + */ + @SerializedName("data_share") + DATA_SHARE, + +} diff --git a/src/main/java/com/checkout/sessions/SessionRequest.java b/src/main/java/com/checkout/sessions/SessionRequest.java index d954ed66..b43fec3c 100644 --- a/src/main/java/com/checkout/sessions/SessionRequest.java +++ b/src/main/java/com/checkout/sessions/SessionRequest.java @@ -1,6 +1,5 @@ package com.checkout.sessions; -import com.checkout.common.ChallengeIndicator; import com.checkout.common.Currency; import com.checkout.sessions.channel.BrowserSession; import com.checkout.sessions.channel.ChannelData; @@ -19,54 +18,171 @@ @NoArgsConstructor public final class SessionRequest { + /** + * The source of the authentication. + * [Required] + */ @Builder.Default private SessionSource source = new SessionCardSource(); + /** + * The payment amount in the minor currency unit. + * For {@code recurring} and {@code installment} payment types, this value is required and must + * be greater than zero. + * Omitting this value will set {@link #authenticationCategory} to {@link Category#NON_PAYMENT}. + * [Optional] + * min 0 + * max 48 characters + */ private Long amount; + /** + * The three-letter ISO currency code. + * [Required] + * min 3 characters + * max 3 characters + */ private Currency currency; + /** + * The processing channel to be used for the session. Required if this was not set in the + * request for the OAuth token. + * [Optional] + * ^(pc)_(\w{26})$ + */ private String processingChannelId; + /** + * Information related to authentication for payfac payments. + * [Optional] + */ private SessionMarketplaceData marketplace; + /** + * Indicates the type of payment this session is for. Please note the spelling of + * {@code installment} consists of two {@code l}s. + * [Optional] + * Default: {@link AuthenticationType#REGULAR} + */ @Builder.Default private AuthenticationType authenticationType = AuthenticationType.REGULAR; + /** + * Indicates the category of the authentication request. + * [Optional] + * Default: {@link Category#PAYMENT} + */ @Builder.Default private Category authenticationCategory = Category.PAYMENT; + /** + * Additional information about the cardholder's account. + * [Optional] + */ @SerializedName("account_info") private CardholderAccountInfo cardholderAccountInfo; + /** + * Indicates whether a challenge is requested for this session. + * The exemption values are accepted only by {@code POST /sessions}; see + * {@link SessionChallengeIndicator}. + * [Optional] + * Default: {@link SessionChallengeIndicator#NO_PREFERENCE} + * max 50 characters + */ @Builder.Default - private ChallengeIndicator challengeIndicator = ChallengeIndicator.NO_PREFERENCE; + private SessionChallengeIndicator challengeIndicator = SessionChallengeIndicator.NO_PREFERENCE; + /** + * An optional dynamic billing descriptor. + * [Optional] + */ private SessionsBillingDescriptor billingDescriptor; + /** + * A reference you can later use to identify this payment, such as an order number. + * Do not pass sensitive information in this field, for example card details. + * [Optional] + * max 100 characters + */ private String reference; + /** + * Additional information about the cardholder's purchase. + * [Optional] + */ private MerchantRiskInfo merchantRiskInfo; + /** + * A reference to a previous transaction for this cardholder. + *

+ * Note: this field is not present in the {@code SessionRequest} schema of the Checkout.com API + * Reference. It is retained for backwards compatibility pending confirmation from the API + * owners. + * [Optional] + */ private String priorTransactionReference; + /** + * Identifies the type of transaction being authenticated. + * [Optional] + * Default: {@link TransactionType#GOODS_SERVICE} + * max 50 characters + */ @Builder.Default private TransactionType transactionType = TransactionType.GOODS_SERVICE; + /** + * The shipping address. Any special characters will be replaced. + * [Optional] + */ private SessionAddress shippingAddress; + /** + * Indicates whether the cardholder shipping address and billing address are the same. + * [Optional] + */ private Boolean shippingAddressMatchesBilling; + /** + * The redirect information needed for callbacks or redirects after the payment is completed. + * [Required] + */ private CompletionInfo completion; + /** + * The information gathered from the environment used to initiate the session. + * See {@link BrowserSession} for the browser channel. + * [Optional] + */ private ChannelData channelData; + /** + * Details of a recurring authentication. This property is needed only for a + * {@link AuthenticationType#RECURRING} authentication type. Value will be ignored in any other + * cases. + * [Optional] + */ private Recurring recurring; + /** + * Details of an installment authentication. This property is needed only for an + * {@link AuthenticationType#INSTALLMENT} authentication type. Value will be ignored in any + * other cases. + * [Optional] + */ private Installment installment; + /** + * Optionally opt into request optimization. + * [Optional] + */ private Optimization optimization; + /** + * Details of a previous transaction. + * [Optional] + */ private InitialTransaction initialTransaction; /** diff --git a/src/main/java/com/checkout/sessions/SessionScheme.java b/src/main/java/com/checkout/sessions/SessionScheme.java index 2c6469c0..5dc575f5 100644 --- a/src/main/java/com/checkout/sessions/SessionScheme.java +++ b/src/main/java/com/checkout/sessions/SessionScheme.java @@ -2,18 +2,64 @@ import com.google.gson.annotations.SerializedName; +/** + * Indicates the scheme this authentication is carried out against. + *

+ * Used by {@link GetSessionResponse#getScheme()}, + * {@link CreateSessionAcceptedResponse#getScheme()}, + * {@link GetSessionResponseAfterChannelDataSupplied#getScheme()}, + * {@link SchemeInfo#getName()} and + * {@link com.checkout.sessions.source.SessionSource#getScheme()}. + *

+ * [Required] on the session responses + */ public enum SessionScheme { + /** + * American Express. + */ @SerializedName("amex") AMEX, + + /** + * Cartes Bancaires. + */ @SerializedName("cartes_bancaires") CARTES_BANCAIRES, + + /** + * Diners Club. + */ @SerializedName("diners") DINERS, + + /** + * Discover. + */ + @SerializedName("discover") + DISCOVER, + + /** + * JCB. + */ @SerializedName("jcb") JCB, + + /** + * Mastercard. + */ @SerializedName("mastercard") MASTERCARD, + + /** + * Unified Payments Interface (UPI). + */ + @SerializedName("upi") + UPI, + + /** + * Visa. + */ @SerializedName("visa") VISA diff --git a/src/main/java/com/checkout/sessions/SessionsCardMetadataResponse.java b/src/main/java/com/checkout/sessions/SessionsCardMetadataResponse.java index fd2b62c7..204aa49d 100644 --- a/src/main/java/com/checkout/sessions/SessionsCardMetadataResponse.java +++ b/src/main/java/com/checkout/sessions/SessionsCardMetadataResponse.java @@ -3,19 +3,51 @@ import com.checkout.common.CardCategory; import com.checkout.common.CardType; import com.checkout.common.CountryCode; - +import lombok.Data; + +/** + * Additional details for this card. + *

+ * Returned as {@link CardInfo#getMetadata()} on the session responses. + */ +@Data public final class SessionsCardMetadataResponse { + /** + * The card type. + * [Optional] + */ private CardType cardType; + /** + * The card category. + * [Optional] + */ private CardCategory cardCategory; + /** + * The card issuer's name. + * [Optional] + */ private String issuerName; + /** + * The two letter alpha country code of the card issuer. + * [Optional] + * ^[A-Z]{2} + */ private CountryCode issuerCountry; + /** + * The issuer/card scheme product identifier. + * [Optional] + */ private String productId; + /** + * The issuer/card scheme product type. + * [Optional] + */ private String productType; } diff --git a/src/main/java/com/checkout/sessions/TransactionType.java b/src/main/java/com/checkout/sessions/TransactionType.java index aeca2127..1802ea19 100644 --- a/src/main/java/com/checkout/sessions/TransactionType.java +++ b/src/main/java/com/checkout/sessions/TransactionType.java @@ -2,17 +2,49 @@ import com.google.gson.annotations.SerializedName; +/** + * Identifies the type of transaction being authenticated. + *

+ * Used by {@link SessionRequest#getTransactionType()} and + * {@link GetSessionResponse#getTransactionType()}. + *

+ * [Optional] + *

+ * Default: {@link #GOODS_SERVICE} + *

+ * max 50 characters + */ public enum TransactionType { + /** + * A transaction that funds an account. + */ @SerializedName("account_funding") ACCOUNT_FUNDING, + + /** + * A transaction that accepts a check. + */ @SerializedName("check_acceptance") CHECK_ACCEPTANCE, + + /** + * A transaction for goods or a service. This is the default. + */ @SerializedName("goods_service") GOODS_SERVICE, + + /** + * A transaction that activates or loads a prepaid card. + */ @SerializedName("prepaid_activation_and_load") PREPAID_ACTIVATION_AND_LOAD, - @SerializedName("quashi_card_transaction") - QUASHI_CARD_TRANSACTION, + + /** + * A quasi-cash transaction, for example the purchase of casino chips, money orders or + * traveller's cheques. + */ + @SerializedName("quasi_card_transaction") + QUASI_CARD_TRANSACTION, } diff --git a/src/test/java/com/checkout/common/ChallengeIndicatorTest.java b/src/test/java/com/checkout/common/ChallengeIndicatorTest.java index 0d01df94..5af91bf6 100644 --- a/src/test/java/com/checkout/common/ChallengeIndicatorTest.java +++ b/src/test/java/com/checkout/common/ChallengeIndicatorTest.java @@ -11,10 +11,36 @@ import static org.junit.jupiter.api.Assertions.assertEquals; +/** + * The five exemption values are deprecated on this shared enum in favour of + * {@link com.checkout.sessions.SessionChallengeIndicator}, but they are deliberately kept and still + * covered here: removing them would break merchants that already reference them. + */ +@SuppressWarnings("deprecation") class ChallengeIndicatorTest { private final Serializer serializer = new GsonSerializer(); + private static Stream baseChallengeIndicators() { + return Stream.of( + Arguments.of(ChallengeIndicator.NO_PREFERENCE, "\"no_preference\""), + Arguments.of(ChallengeIndicator.NO_CHALLENGE_REQUESTED, "\"no_challenge_requested\""), + Arguments.of(ChallengeIndicator.CHALLENGE_REQUESTED, "\"challenge_requested\""), + Arguments.of(ChallengeIndicator.CHALLENGE_REQUESTED_MANDATE, "\"challenge_requested_mandate\"") + ); + } + + /** + * The four base values are the only ones accepted by the {@code 3ds.challenge_indicator} fields + * that use this enum. + */ + @ParameterizedTest + @MethodSource("baseChallengeIndicators") + void shouldRoundTripTheFourBaseValues(final ChallengeIndicator value, final String expectedJson) { + assertEquals(expectedJson, serializer.toJson(value)); + assertEquals(value, serializer.fromJson(expectedJson, ChallengeIndicator.class)); + } + private static Stream challengeIndicators() { return Stream.of( Arguments.of(ChallengeIndicator.NO_PREFERENCE, "\"no_preference\""), diff --git a/src/test/java/com/checkout/sessions/AbstractSessionsTestIT.java b/src/test/java/com/checkout/sessions/AbstractSessionsTestIT.java index 0580cc98..f0cd52c0 100644 --- a/src/test/java/com/checkout/sessions/AbstractSessionsTestIT.java +++ b/src/test/java/com/checkout/sessions/AbstractSessionsTestIT.java @@ -4,7 +4,6 @@ import com.checkout.PlatformType; import com.checkout.SandboxTestFixture; import com.checkout.TestHelper; -import com.checkout.common.ChallengeIndicator; import com.checkout.common.CountryCode; import com.checkout.common.Currency; import com.checkout.common.Phone; @@ -30,7 +29,7 @@ public AbstractSessionsTestIT() { protected SessionResponse createNonHostedSession(final ChannelData channelData, final Category authenticationCategory, - final ChallengeIndicator challengeIndicator, + final SessionChallengeIndicator challengeIndicator, final TransactionType transactionType) { final SessionRequest sessionRequest = createNonHostedSessionRequest(channelData, authenticationCategory, challengeIndicator, transactionType); @@ -45,7 +44,7 @@ protected SessionResponse createHostedSession() { // Common methods protected SessionRequest createNonHostedSessionRequest(final ChannelData channelData, final Category authenticationCategory, - final ChallengeIndicator challengeIndicator, + final SessionChallengeIndicator challengeIndicator, final TransactionType transactionType) { return SessionRequest.builder() .source(createSessionCardSource()) @@ -73,7 +72,7 @@ protected SessionRequest createHostedSessionRequest() { .processingChannelId(System.getenv("CHECKOUT_PROCESSING_CHANNEL_ID")) .authenticationType(AuthenticationType.REGULAR) .authenticationCategory(Category.PAYMENT) - .challengeIndicator(ChallengeIndicator.NO_PREFERENCE) + .challengeIndicator(SessionChallengeIndicator.NO_PREFERENCE) .reference("ORD-5023-4E89") .transactionType(TransactionType.GOODS_SERVICE) .shippingAddress(createShippingAddress()) diff --git a/src/test/java/com/checkout/sessions/CreateSessionAcceptedResponseSerializationTest.java b/src/test/java/com/checkout/sessions/CreateSessionAcceptedResponseSerializationTest.java new file mode 100644 index 00000000..811bce9a --- /dev/null +++ b/src/test/java/com/checkout/sessions/CreateSessionAcceptedResponseSerializationTest.java @@ -0,0 +1,163 @@ +package com.checkout.sessions; + +import com.checkout.GsonSerializer; +import com.checkout.Serializer; +import com.checkout.common.Currency; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.time.Instant; +import java.util.Arrays; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Full-property deserialization coverage for {@link CreateSessionAcceptedResponse}, the 202 schema + * returned by {@code POST /sessions}. + *

+ * Every one of the class's 22 properties is populated and asserted, plus the {@code _links} it + * inherits from {@link com.checkout.common.Resource}. + */ +class CreateSessionAcceptedResponseSerializationTest { + + private final Serializer serializer = new GsonSerializer(); + + private CreateSessionAcceptedResponse response; + + private static String fullPayload() { + return "{" + + "\"id\":\"sid_y3oqhf46pyzuxjbcn2giaqnb44\"," + + "\"session_secret\":\"sek_Dal7UyiH8rIFXA4PfgiIk2jUyQkVDeEWgVBEL4TsRTE=\"," + + "\"transaction_id\":\"9aea641d-0549-4222-9ca9-d90b43a4f38c\"," + + "\"scheme\":\"mastercard\"," + + "\"amount\":6540," + + "\"currency\":\"USD\"," + + "\"authentication_type\":\"regular\"," + + "\"authentication_category\":\"payment\"," + + "\"status\":\"pending\"," + + "\"status_reason\":\"ares_status\"," + + "\"next_actions\":[\"collect_channel_data\"]," + + "\"protocol_version\":\"2.2.0\"," + + "\"account_info\":{\"purchase_count\":10,\"add_card_attempts\":5,\"transactions_today\":3}," + + "\"merchant_risk_info\":{\"delivery_email\":\"bruce@wayne-enterprises.com\"," + + "\"is_preorder\":false,\"is_reorder\":false}," + + "\"reference\":\"ORD-5023-4E89\"," + + "\"card\":{\"instrument_id\":\"src_w4jelhppmfiufdnatndh3wtsfq\",\"fingerprint\":\"fp-1\"}," + + "\"recurring\":{\"days_between_payments\":30,\"expiry\":\"99991231\"}," + + "\"installment\":{\"number_of_payments\":3,\"days_between_payments\":30,\"expiry\":\"99991231\"}," + + "\"initial_transaction\":{\"acs_transaction_id\":\"acs-txn-id\"," + + "\"authentication_method\":\"frictionless\"," + + "\"authentication_timestamp\":\"2026-08-03T10:11:12Z\"," + + "\"authentication_data\":\"auth-data\"," + + "\"initial_session_id\":\"sid_y3oqhf46pyzuxjbcn2giaqnb44\"}," + + "\"authentication_date\":\"2026-08-03T10:11:12Z\"," + + "\"challenge_indicator\":\"transaction_risk_assessment\"," + + "\"optimization\":{\"optimized\":true,\"framework\":\"acceptance_rates\"," + + "\"optimized_properties\":[{\"field\":\"amount\",\"original_value\":\"1\",\"optimized_value\":\"2\"}]}," + + "\"_links\":{\"self\":{\"href\":\"https://api.checkout.com/sessions/sid_y3oqhf46pyzuxjbcn2giaqnb44\"}}" + + "}"; + } + + @BeforeEach + void setUp() { + response = serializer.fromJson(fullPayload(), CreateSessionAcceptedResponse.class); + assertNotNull(response); + } + + @Test + void shouldDeserializeIdentifiersAndAmounts() { + assertEquals("sid_y3oqhf46pyzuxjbcn2giaqnb44", response.getId()); + assertEquals("sek_Dal7UyiH8rIFXA4PfgiIk2jUyQkVDeEWgVBEL4TsRTE=", response.getSessionSecret()); + assertEquals("9aea641d-0549-4222-9ca9-d90b43a4f38c", response.getTransactionId()); + assertEquals(6540L, response.getAmount()); + assertEquals(Currency.USD, response.getCurrency()); + assertEquals("2.2.0", response.getProtocolVersion()); + assertEquals("ORD-5023-4E89", response.getReference()); + } + + @Test + void shouldDeserializeEveryEnumTypedProperty() { + assertEquals(SessionScheme.MASTERCARD, response.getScheme()); + assertEquals(AuthenticationType.REGULAR, response.getAuthenticationType()); + assertEquals(Category.PAYMENT, response.getAuthenticationCategory()); + assertEquals(SessionStatus.PENDING, response.getStatus()); + assertEquals(StatusReason.ARES_STATUS, response.getStatusReason()); + assertEquals(SessionChallengeIndicator.TRANSACTION_RISK_ASSESSMENT, response.getChallengeIndicator()); + assertEquals(Arrays.asList(NextAction.COLLECT_CHANNEL_DATA), response.getNextActions()); + } + + @Test + void shouldDeserializeAuthenticationDateAsInstant() { + assertEquals(Instant.parse("2026-08-03T10:11:12Z"), response.getAuthenticationDate()); + } + + /** + * This class names the field {@code accountInfo} and relies on the global + * {@code LOWER_CASE_WITH_UNDERSCORES} policy, whereas {@link GetSessionResponse} names it + * {@code cardholderAccountInfo} with an explicit {@code @SerializedName}. Both must map the same + * {@code account_info} wire key. + */ + @Test + void shouldDeserializeAccountInfoUnderTheAccountInfoKey() { + assertNotNull(response.getAccountInfo()); + assertEquals(10L, response.getAccountInfo().getPurchaseCount()); + assertEquals(5L, response.getAccountInfo().getAddCardAttempts()); + assertEquals(3L, response.getAccountInfo().getTransactionsToday()); + } + + @Test + void shouldDeserializeNestedObjects() { + assertNotNull(response.getMerchantRiskInfo()); + assertEquals("bruce@wayne-enterprises.com", response.getMerchantRiskInfo().getDeliveryEmail()); + + assertNotNull(response.getCard()); + assertEquals("src_w4jelhppmfiufdnatndh3wtsfq", response.getCard().getInstrumentId()); + assertEquals("fp-1", response.getCard().getFingerprint()); + + assertNotNull(response.getRecurring()); + assertEquals(30L, response.getRecurring().getDaysBetweenPayments()); + + assertNotNull(response.getInstallment()); + assertEquals(3L, response.getInstallment().getNumberOfPayments()); + + assertNotNull(response.getInitialTransaction()); + assertEquals("acs-txn-id", response.getInitialTransaction().getAcsTransactionId()); + + assertNotNull(response.getOptimization()); + assertEquals(Boolean.TRUE, response.getOptimization().getOptimized()); + assertEquals("acceptance_rates", response.getOptimization().getFramework()); + assertNotNull(response.getOptimization().getOptimizedProperties()); + assertEquals(1, response.getOptimization().getOptimizedProperties().size()); + } + + @Test + void shouldDeserializeInheritedLinks() { + assertNotNull(response.getSelfLink()); + assertEquals("https://api.checkout.com/sessions/sid_y3oqhf46pyzuxjbcn2giaqnb44", + response.getSelfLink().getHref()); + } + + /** + * Guards against a property being silently dropped: every field declared on the class must be + * non-null after deserializing a payload that populates all of them. + */ + @Test + void shouldLeaveNoDeclaredPropertyNull() { + final java.lang.reflect.Field[] fields = CreateSessionAcceptedResponse.class.getDeclaredFields(); + for (final java.lang.reflect.Field field : fields) { + if (field.isSynthetic()) { + continue; + } + field.setAccessible(true); + try { + assertNotNull(field.get(response), "property was not deserialized: " + field.getName()); + } catch (final IllegalAccessException e) { + throw new AssertionError(e); + } + } + assertTrue(fields.length >= 22, "expected at least 22 declared properties, found " + fields.length); + } + +} diff --git a/src/test/java/com/checkout/sessions/GetSessionResponseSerializationTest.java b/src/test/java/com/checkout/sessions/GetSessionResponseSerializationTest.java new file mode 100644 index 00000000..5118c79f --- /dev/null +++ b/src/test/java/com/checkout/sessions/GetSessionResponseSerializationTest.java @@ -0,0 +1,254 @@ +package com.checkout.sessions; + +import com.checkout.GsonSerializer; +import com.checkout.Serializer; +import com.checkout.common.CardCategory; +import com.checkout.common.CardType; +import com.checkout.common.CountryCode; +import com.checkout.common.Currency; +import com.checkout.common.Exemption; +import com.checkout.common.ThreeDSFlowType; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.time.Instant; +import java.util.Arrays; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Full-property deserialization coverage for {@link GetSessionResponse}, the schema returned by + * {@code GET /sessions/{id}} and {@code PUT /sessions/{id}/collect-data}. + *

+ * Every one of the class's 40 properties is populated and asserted, plus the {@code _links} it + * inherits from {@link com.checkout.common.Resource}. Nested objects are covered transitively. + */ +class GetSessionResponseSerializationTest { + + private final Serializer serializer = new GsonSerializer(); + + private GetSessionResponse response; + + private static String fullPayload() { + return "{" + + "\"id\":\"sid_y3oqhf46pyzuxjbcn2giaqnb44\"," + + "\"session_secret\":\"sek_Dal7UyiH8rIFXA4PfgiIk2jUyQkVDeEWgVBEL4TsRTE=\"," + + "\"transaction_id\":\"9aea641d-0549-4222-9ca9-d90b43a4f38c\"," + + "\"scheme\":\"visa\"," + + "\"amount\":6540," + + "\"currency\":\"USD\"," + + "\"completed\":true," + + "\"challenged\":true," + + "\"authentication_type\":\"regular\"," + + "\"authentication_category\":\"payment\"," + + "\"certificates\":{\"ds_public\":\"ds-public-key\",\"ca_public\":\"ca-public-key\"}," + + "\"status\":\"challenged\"," + + "\"status_reason\":\"ares_status\"," + + "\"approved\":true," + + "\"protocol_version\":\"2.2.0\"," + + "\"account_info\":{\"purchase_count\":10,\"add_card_attempts\":5,\"transactions_today\":3}," + + "\"merchant_risk_info\":{\"delivery_email\":\"bruce@wayne-enterprises.com\"," + + "\"is_preorder\":false,\"is_reorder\":false}," + + "\"reference\":\"ORD-5023-4E89\"," + + "\"transaction_type\":\"goods_service\"," + + "\"next_actions\":[\"collect_channel_data\",\"challenge_cardholder\"]," + + "\"ds\":{\"ds_id\":\"ds-id\",\"reference_number\":\"ds-ref\",\"transaction_id\":\"ds-txn\"}," + + "\"acs\":{\"reference_number\":\"acs-ref\",\"transaction_id\":\"acs-txn\"," + + "\"operator_id\":\"acs-operator\",\"url\":\"https://acs.example.com/challenge\"," + + "\"signed_content\":\"signed-content\",\"challenge_mandated\":true," + + "\"authentication_type\":\"static\",\"challenge_cancel_reason\":\"cardholder_cancel\"," + + "\"interface\":\"html\",\"ui_template\":\"single_select\"}," + + "\"response_code\":\"Y\"," + + "\"response_status_reason\":\"01\"," + + "\"pareq\":\"pareq-value\"," + + "\"cryptogram\":\"MTIzNDU2Nzg5MDA5ODc2NTQzMjE=\"," + + "\"eci\":\"05\"," + + "\"xid\":\"XSUErNftqkiTdlkpSk8p32GWOFA\"," + + "\"cardholder_info\":\"Card declined. Please contact your issuing bank.\"," + + "\"card\":{\"instrument_id\":\"src_w4jelhppmfiufdnatndh3wtsfq\",\"fingerprint\":\"fp-1\"," + + "\"metadata\":{\"card_type\":\"CREDIT\",\"card_category\":\"CONSUMER\"," + + "\"issuer_name\":\"Checkout\",\"issuer_country\":\"GB\"," + + "\"product_id\":\"MDS\",\"product_type\":\"Debit MasterCard Card\"}}," + + "\"recurring\":{\"days_between_payments\":30,\"expiry\":\"99991231\"}," + + "\"installment\":{\"number_of_payments\":3,\"days_between_payments\":30,\"expiry\":\"99991231\"}," + + "\"initial_transaction\":{\"acs_transaction_id\":\"acs-txn-id\"," + + "\"authentication_method\":\"frictionless\"," + + "\"authentication_timestamp\":\"2026-08-03T10:11:12Z\"," + + "\"authentication_data\":\"auth-data\"," + + "\"initial_session_id\":\"sid_y3oqhf46pyzuxjbcn2giaqnb44\"}," + + "\"customer_ip\":\"192.168.1.1\"," + + "\"authentication_date\":\"2026-08-03T10:11:12Z\"," + + "\"exemption\":{\"requested\":\"none\",\"applied\":\"low_value\",\"code\":\"cb-code\"}," + + "\"flow_type\":\"challenged\"," + + "\"challenge_indicator\":\"trusted_listing\"," + + "\"optimization\":{\"optimized\":true,\"framework\":\"acceptance_rates\"," + + "\"optimized_properties\":[{\"field\":\"amount\",\"original_value\":\"1\",\"optimized_value\":\"2\"}]}," + + "\"scheme_info\":{\"name\":\"visa\",\"score\":\"0.5\",\"avalgo\":\"1\"}," + + "\"_links\":{\"self\":{\"href\":\"https://api.checkout.com/sessions/sid_y3oqhf46pyzuxjbcn2giaqnb44\"}}" + + "}"; + } + + @BeforeEach + void setUp() { + response = serializer.fromJson(fullPayload(), GetSessionResponse.class); + assertNotNull(response); + } + + @Test + void shouldDeserializeIdentifiersAndAmounts() { + assertEquals("sid_y3oqhf46pyzuxjbcn2giaqnb44", response.getId()); + assertEquals("sek_Dal7UyiH8rIFXA4PfgiIk2jUyQkVDeEWgVBEL4TsRTE=", response.getSessionSecret()); + assertEquals("9aea641d-0549-4222-9ca9-d90b43a4f38c", response.getTransactionId()); + assertEquals(6540L, response.getAmount()); + assertEquals(Currency.USD, response.getCurrency()); + assertEquals("2.2.0", response.getProtocolVersion()); + assertEquals("ORD-5023-4E89", response.getReference()); + } + + @Test + void shouldDeserializeEveryEnumTypedProperty() { + assertEquals(SessionScheme.VISA, response.getScheme()); + assertEquals(AuthenticationType.REGULAR, response.getAuthenticationType()); + assertEquals(Category.PAYMENT, response.getAuthenticationCategory()); + assertEquals(SessionStatus.CHALLENGED, response.getStatus()); + assertEquals(StatusReason.ARES_STATUS, response.getStatusReason()); + assertEquals(TransactionType.GOODS_SERVICE, response.getTransactionType()); + assertEquals(ResponseCode.Y, response.getResponseCode()); + assertEquals(ThreeDSFlowType.CHALLENGED, response.getFlowType()); + assertEquals(SessionChallengeIndicator.TRUSTED_LISTING, response.getChallengeIndicator()); + assertEquals(Arrays.asList(NextAction.COLLECT_CHANNEL_DATA, NextAction.CHALLENGE_CARDHOLDER), + response.getNextActions()); + } + + @Test + void shouldDeserializeBooleanFlags() { + assertEquals(Boolean.TRUE, response.getCompleted()); + assertEquals(Boolean.TRUE, response.getChallenged()); + assertEquals(Boolean.TRUE, response.getApproved()); + } + + @Test + void shouldDeserializeAuthenticationResultFields() { + assertEquals("01", response.getResponseStatusReason()); + assertEquals("pareq-value", response.getPareq()); + assertEquals("MTIzNDU2Nzg5MDA5ODc2NTQzMjE=", response.getCryptogram()); + assertEquals("05", response.getEci()); + assertEquals("XSUErNftqkiTdlkpSk8p32GWOFA", response.getXid()); + assertEquals("Card declined. Please contact your issuing bank.", response.getCardholderInfo()); + assertEquals("192.168.1.1", response.getCustomerIp()); + } + + @Test + void shouldDeserializeAuthenticationDateAsInstant() { + assertEquals(Instant.parse("2026-08-03T10:11:12Z"), response.getAuthenticationDate()); + } + + @Test + void shouldDeserializeAccountInfoUnderTheAccountInfoKey() { + assertNotNull(response.getCardholderAccountInfo()); + assertEquals(10L, response.getCardholderAccountInfo().getPurchaseCount()); + assertEquals(5L, response.getCardholderAccountInfo().getAddCardAttempts()); + assertEquals(3L, response.getCardholderAccountInfo().getTransactionsToday()); + } + + @Test + void shouldDeserializeNestedObjects() { + assertNotNull(response.getCertificates()); + assertEquals("ds-public-key", response.getCertificates().getDsPublic()); + assertEquals("ca-public-key", response.getCertificates().getCaPublic()); + + assertNotNull(response.getMerchantRiskInfo()); + assertEquals("bruce@wayne-enterprises.com", response.getMerchantRiskInfo().getDeliveryEmail()); + + assertNotNull(response.getDs()); + assertEquals("ds-id", response.getDs().getDsId()); + + assertNotNull(response.getCard()); + assertEquals("src_w4jelhppmfiufdnatndh3wtsfq", response.getCard().getInstrumentId()); + assertEquals("fp-1", response.getCard().getFingerprint()); + + assertNotNull(response.getRecurring()); + assertEquals(30L, response.getRecurring().getDaysBetweenPayments()); + + assertNotNull(response.getInstallment()); + assertEquals(3L, response.getInstallment().getNumberOfPayments()); + + assertNotNull(response.getInitialTransaction()); + assertEquals("acs-txn-id", response.getInitialTransaction().getAcsTransactionId()); + + assertNotNull(response.getSchemeInfo()); + assertEquals(SessionScheme.VISA, response.getSchemeInfo().getName()); + + assertNotNull(response.getOptimization()); + assertEquals(Boolean.TRUE, response.getOptimization().getOptimized()); + assertNotNull(response.getOptimization().getOptimizedProperties()); + assertEquals(1, response.getOptimization().getOptimizedProperties().size()); + } + + @Test + void shouldDeserializeAcsWithReservedInterfaceKey() { + assertNotNull(response.getAcs()); + assertEquals("acs-ref", response.getAcs().getReferenceNumber()); + assertEquals("https://acs.example.com/challenge", response.getAcs().getUrl()); + assertEquals(Boolean.TRUE, response.getAcs().getChallengeMandated()); + assertEquals(ChallengeCancelReason.CARDHOLDER_CANCEL, response.getAcs().getChallengeCancelReason()); + assertEquals(SessionInterface.HTML, response.getAcs().getSessionInterface()); + assertEquals(UIElements.SINGLE_SELECT, response.getAcs().getUiTemplate()); + } + + /** + * Covers all six properties of {@link SessionsCardMetadataResponse}. The spec sends the card + * type and category in upper case, which the shared enums accept via {@code alternate} values. + */ + @Test + void shouldDeserializeEveryCardMetadataProperty() { + final SessionsCardMetadataResponse metadata = response.getCard().getMetadata(); + + assertNotNull(metadata); + assertEquals(CardType.CREDIT, metadata.getCardType()); + assertEquals(CardCategory.CONSUMER, metadata.getCardCategory()); + assertEquals("Checkout", metadata.getIssuerName()); + assertEquals(CountryCode.GB, metadata.getIssuerCountry()); + assertEquals("MDS", metadata.getProductId()); + assertEquals("Debit MasterCard Card", metadata.getProductType()); + } + + @Test + void shouldDeserializeExemption() { + assertNotNull(response.getExemption()); + assertEquals("none", response.getExemption().getRequested()); + assertEquals(Exemption.LOW_VALUE, response.getExemption().getApplied()); + assertEquals("cb-code", response.getExemption().getCode()); + } + + @Test + void shouldDeserializeInheritedLinks() { + assertNotNull(response.getSelfLink()); + assertEquals("https://api.checkout.com/sessions/sid_y3oqhf46pyzuxjbcn2giaqnb44", + response.getSelfLink().getHref()); + } + + /** + * Guards against a property being silently dropped: every field declared on the class must be + * non-null after deserializing a payload that populates all of them. + */ + @Test + void shouldLeaveNoDeclaredPropertyNull() { + final java.lang.reflect.Field[] fields = GetSessionResponse.class.getDeclaredFields(); + for (final java.lang.reflect.Field field : fields) { + if (field.isSynthetic()) { + continue; + } + field.setAccessible(true); + try { + assertNotNull(field.get(response), "property was not deserialized: " + field.getName()); + } catch (final IllegalAccessException e) { + throw new AssertionError(e); + } + } + assertTrue(fields.length >= 40, "expected at least 40 declared properties, found " + fields.length); + } + +} diff --git a/src/test/java/com/checkout/sessions/RequestAndGetSessionsTestIT.java b/src/test/java/com/checkout/sessions/RequestAndGetSessionsTestIT.java index a3ac5285..b2a8eb72 100644 --- a/src/test/java/com/checkout/sessions/RequestAndGetSessionsTestIT.java +++ b/src/test/java/com/checkout/sessions/RequestAndGetSessionsTestIT.java @@ -1,6 +1,5 @@ package com.checkout.sessions; -import com.checkout.common.ChallengeIndicator; import com.checkout.sessions.channel.ChannelData; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.params.ParameterizedTest; @@ -18,32 +17,32 @@ class RequestAndGetSessionsTestIT extends AbstractSessionsTestIT { private static Stream sessionsTypes_browserSession() { return Stream.of( - Arguments.of(Category.PAYMENT, ChallengeIndicator.NO_PREFERENCE, TransactionType.GOODS_SERVICE), - Arguments.of(Category.NON_PAYMENT, ChallengeIndicator.CHALLENGE_REQUESTED, TransactionType.CHECK_ACCEPTANCE), - Arguments.of(Category.NON_PAYMENT, ChallengeIndicator.CHALLENGE_REQUESTED_MANDATE, TransactionType.ACCOUNT_FUNDING) + Arguments.of(Category.PAYMENT, SessionChallengeIndicator.NO_PREFERENCE, TransactionType.GOODS_SERVICE), + Arguments.of(Category.NON_PAYMENT, SessionChallengeIndicator.CHALLENGE_REQUESTED, TransactionType.CHECK_ACCEPTANCE), + Arguments.of(Category.NON_PAYMENT, SessionChallengeIndicator.CHALLENGE_REQUESTED_MANDATE, TransactionType.ACCOUNT_FUNDING) ); } private static Stream sessionsTypes_appSession() { return Stream.of( - Arguments.of(Category.PAYMENT, ChallengeIndicator.NO_PREFERENCE, TransactionType.GOODS_SERVICE), - Arguments.of(Category.NON_PAYMENT, ChallengeIndicator.CHALLENGE_REQUESTED, TransactionType.CHECK_ACCEPTANCE), - Arguments.of(Category.NON_PAYMENT, ChallengeIndicator.CHALLENGE_REQUESTED_MANDATE, TransactionType.ACCOUNT_FUNDING) + Arguments.of(Category.PAYMENT, SessionChallengeIndicator.NO_PREFERENCE, TransactionType.GOODS_SERVICE), + Arguments.of(Category.NON_PAYMENT, SessionChallengeIndicator.CHALLENGE_REQUESTED, TransactionType.CHECK_ACCEPTANCE), + Arguments.of(Category.NON_PAYMENT, SessionChallengeIndicator.CHALLENGE_REQUESTED_MANDATE, TransactionType.ACCOUNT_FUNDING) ); } private static Stream sessionsTypes_merchantInitiatedSession() { return Stream.of( - Arguments.of(Category.PAYMENT, ChallengeIndicator.NO_PREFERENCE, TransactionType.GOODS_SERVICE), - Arguments.of(Category.NON_PAYMENT, ChallengeIndicator.CHALLENGE_REQUESTED, TransactionType.CHECK_ACCEPTANCE), - Arguments.of(Category.NON_PAYMENT, ChallengeIndicator.CHALLENGE_REQUESTED_MANDATE, TransactionType.ACCOUNT_FUNDING) + Arguments.of(Category.PAYMENT, SessionChallengeIndicator.NO_PREFERENCE, TransactionType.GOODS_SERVICE), + Arguments.of(Category.NON_PAYMENT, SessionChallengeIndicator.CHALLENGE_REQUESTED, TransactionType.CHECK_ACCEPTANCE), + Arguments.of(Category.NON_PAYMENT, SessionChallengeIndicator.CHALLENGE_REQUESTED_MANDATE, TransactionType.ACCOUNT_FUNDING) ); } @ParameterizedTest @MethodSource("sessionsTypes_browserSession") void shouldRequestAndGetCardSession_browserSession(final Category category, - final ChallengeIndicator challengeIndicator, + final SessionChallengeIndicator challengeIndicator, final TransactionType transactionType) { final ChannelData browserSession = browserSession(); @@ -64,7 +63,7 @@ void shouldRequestAndGetCardSession_browserSession(final Category category, @ParameterizedTest @MethodSource("sessionsTypes_appSession") void shouldRequestAndGetCardSession_appSession(final Category category, - final ChallengeIndicator challengeIndicator, + final SessionChallengeIndicator challengeIndicator, final TransactionType transactionType) { final ChannelData appSession = appSession(); @@ -83,7 +82,7 @@ void shouldRequestAndGetCardSession_appSession(final Category category, @ParameterizedTest @MethodSource("sessionsTypes_merchantInitiatedSession") void shouldRequestAndGetCardSession_merchantInitiatedSession(final Category category, - final ChallengeIndicator challengeIndicator, + final SessionChallengeIndicator challengeIndicator, final TransactionType transactionType) { final ChannelData merchantInitiatedSession = merchantInitiatedSession(); @@ -102,7 +101,7 @@ void shouldRequestAndGetCardSession_merchantInitiatedSession(final Category cate @ParameterizedTest @MethodSource("sessionsTypes_browserSession") void shouldRequestAndGetCardSessionSync_browserSession(final Category category, - final ChallengeIndicator challengeIndicator, + final SessionChallengeIndicator challengeIndicator, final TransactionType transactionType) { final ChannelData browserSession = browserSession(); @@ -123,7 +122,7 @@ void shouldRequestAndGetCardSessionSync_browserSession(final Category category, @ParameterizedTest @MethodSource("sessionsTypes_appSession") void shouldRequestAndGetCardSessionSync_appSession(final Category category, - final ChallengeIndicator challengeIndicator, + final SessionChallengeIndicator challengeIndicator, final TransactionType transactionType) { final ChannelData appSession = appSession(); @@ -142,7 +141,7 @@ void shouldRequestAndGetCardSessionSync_appSession(final Category category, @ParameterizedTest @MethodSource("sessionsTypes_merchantInitiatedSession") void shouldRequestAndGetCardSessionSync_merchantInitiatedSession(final Category category, - final ChallengeIndicator challengeIndicator, + final SessionChallengeIndicator challengeIndicator, final TransactionType transactionType) { final ChannelData merchantInitiatedSession = merchantInitiatedSession(); diff --git a/src/test/java/com/checkout/sessions/SessionChallengeIndicatorTest.java b/src/test/java/com/checkout/sessions/SessionChallengeIndicatorTest.java new file mode 100644 index 00000000..8f97a97e --- /dev/null +++ b/src/test/java/com/checkout/sessions/SessionChallengeIndicatorTest.java @@ -0,0 +1,119 @@ +package com.checkout.sessions; + +import com.checkout.GsonSerializer; +import com.checkout.Serializer; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class SessionChallengeIndicatorTest { + + private final Serializer serializer = new GsonSerializer(); + + private static Stream sessionChallengeIndicators() { + return Stream.of( + Arguments.of(SessionChallengeIndicator.NO_PREFERENCE, "\"no_preference\""), + Arguments.of(SessionChallengeIndicator.NO_CHALLENGE_REQUESTED, "\"no_challenge_requested\""), + Arguments.of(SessionChallengeIndicator.CHALLENGE_REQUESTED, "\"challenge_requested\""), + Arguments.of(SessionChallengeIndicator.CHALLENGE_REQUESTED_MANDATE, "\"challenge_requested_mandate\""), + Arguments.of(SessionChallengeIndicator.LOW_VALUE, "\"low_value\""), + Arguments.of(SessionChallengeIndicator.TRUSTED_LISTING, "\"trusted_listing\""), + Arguments.of(SessionChallengeIndicator.TRUSTED_LISTING_PROMPT, "\"trusted_listing_prompt\""), + Arguments.of(SessionChallengeIndicator.TRANSACTION_RISK_ASSESSMENT, "\"transaction_risk_assessment\""), + Arguments.of(SessionChallengeIndicator.DATA_SHARE, "\"data_share\"") + ); + } + + @ParameterizedTest + @MethodSource("sessionChallengeIndicators") + void shouldSerializeSessionChallengeIndicatorToSnakeCase(final SessionChallengeIndicator value, + final String expectedJson) { + assertEquals(expectedJson, serializer.toJson(value)); + } + + @ParameterizedTest + @MethodSource("sessionChallengeIndicators") + void shouldDeserializeSessionChallengeIndicatorFromSnakeCase(final SessionChallengeIndicator expected, + final String json) { + assertEquals(expected, serializer.fromJson(json, SessionChallengeIndicator.class)); + } + + @Test + void shouldRoundTripAllValues() { + for (final SessionChallengeIndicator value : SessionChallengeIndicator.values()) { + final String json = serializer.toJson(value); + assertEquals(value, serializer.fromJson(json, SessionChallengeIndicator.class)); + } + } + + @Test + void shouldExposeTheNineValuesDefinedBySessionRequest() { + assertEquals(9, SessionChallengeIndicator.values().length); + } + + /** + * {@link SessionRequest} is serialize-only: its {@code source} is an abstract + * {@link com.checkout.sessions.source.SessionSource} with no registered Gson adapter, so the + * request cannot be deserialized. Assert on the emitted JSON instead. + */ + @ParameterizedTest + @MethodSource("sessionChallengeIndicators") + void shouldSerializeEveryValueOnSessionRequest(final SessionChallengeIndicator value, + final String expectedJson) { + final SessionRequest request = SessionRequest.builder() + .challengeIndicator(value) + .build(); + + final String json = serializer.toJson(request); + + assertNotNull(json); + assertTrue(json.contains("\"challenge_indicator\":" + expectedJson), + "expected challenge_indicator " + expectedJson + " in " + json); + } + + @Test + void shouldDefaultSessionRequestChallengeIndicatorToNoPreference() { + final SessionRequest request = SessionRequest.builder().build(); + + assertEquals(SessionChallengeIndicator.NO_PREFERENCE, request.getChallengeIndicator()); + assertTrue(serializer.toJson(request).contains("\"challenge_indicator\":\"no_preference\"")); + } + + /** + * The API Reference specifies only the four base values on the session responses, but the + * request accepts all nine. This guards against a deserialization failure if the API echoes an + * exemption value back. + */ + @ParameterizedTest + @MethodSource("sessionChallengeIndicators") + void shouldDeserializeEveryValueOnGetSessionResponse(final SessionChallengeIndicator expected, + final String json) { + final String responseJson = "{\"challenge_indicator\":" + json + "}"; + + final GetSessionResponse response = serializer.fromJson(responseJson, GetSessionResponse.class); + + assertNotNull(response); + assertEquals(expected, response.getChallengeIndicator()); + } + + @ParameterizedTest + @MethodSource("sessionChallengeIndicators") + void shouldDeserializeEveryValueOnCreateSessionAcceptedResponse(final SessionChallengeIndicator expected, + final String json) { + final String responseJson = "{\"challenge_indicator\":" + json + "}"; + + final CreateSessionAcceptedResponse response = + serializer.fromJson(responseJson, CreateSessionAcceptedResponse.class); + + assertNotNull(response); + assertEquals(expected, response.getChallengeIndicator()); + } + +} diff --git a/src/test/java/com/checkout/sessions/SessionRequestSerializationTest.java b/src/test/java/com/checkout/sessions/SessionRequestSerializationTest.java new file mode 100644 index 00000000..94b5074e --- /dev/null +++ b/src/test/java/com/checkout/sessions/SessionRequestSerializationTest.java @@ -0,0 +1,194 @@ +package com.checkout.sessions; + +import com.checkout.GsonSerializer; +import com.checkout.Serializer; +import com.checkout.common.CountryCode; +import com.checkout.common.Currency; +import com.checkout.common.Phone; +import com.checkout.sessions.channel.BrowserSession; +import com.checkout.sessions.channel.ThreeDsMethodCompletion; +import com.checkout.sessions.completion.NonHostedCompletionInfo; +import com.checkout.sessions.source.SessionCardSource; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Serialization coverage for {@link SessionRequest}. The class is serialize-only: {@code source}, + * {@code completion} and {@code channel_data} are abstract types with no registered Gson adapter, so + * a deserialization roundtrip is not possible. Every property is therefore asserted on the emitted + * JSON. + */ +class SessionRequestSerializationTest { + + private final Serializer serializer = new GsonSerializer(); + + private static SessionRequest fullyPopulated() { + return SessionRequest.builder() + .source(SessionCardSource.builder() + .email("bruce@wayne-enterprises.com") + .expiryMonth(1) + .expiryYear(2030) + .number("4485040371536584") + .name("Bruce Wayne") + .billingAddress(SessionAddress.builderSessionAddress() + .addressLine1("Checkout.com") + .city("London") + .country(CountryCode.GB) + .zip("W1T 4TJ") + .build()) + .homePhone(Phone.builder().number("0204567895").countryCode("234").build()) + .build()) + .amount(6540L) + .currency(Currency.USD) + .processingChannelId("pc_q4dbxom5jbgudnjzjpz7iw4d0u") + .marketplace(SessionMarketplaceData.builder() + .subEntityId("ent_ocw5i74vowfg2edpy66izhts2u") + .build()) + .authenticationType(AuthenticationType.REGULAR) + .authenticationCategory(Category.PAYMENT) + .cardholderAccountInfo(CardholderAccountInfo.builder() + .purchaseCount(10L) + .addCardAttempts(5L) + .transactionsToday(3L) + .build()) + .challengeIndicator(SessionChallengeIndicator.TRUSTED_LISTING_PROMPT) + .billingDescriptor(SessionsBillingDescriptor.builder().name("SUPERHEROES.COM").build()) + .reference("ORD-5023-4E89") + .merchantRiskInfo(MerchantRiskInfo.builder() + .deliveryEmail("bruce@wayne-enterprises.com") + .isPreorder(false) + .isReorder(false) + .build()) + .priorTransactionReference("prior-txn-ref") + .transactionType(TransactionType.GOODS_SERVICE) + .shippingAddress(SessionAddress.builderSessionAddress() + .addressLine1("Checkout.com") + .addressLine2("ABC building") + .city("London") + .country(CountryCode.GB) + .state("ENG") + .zip("W1T 4TJ") + .build()) + .shippingAddressMatchesBilling(Boolean.TRUE) + .completion(NonHostedCompletionInfo.builder() + .callbackUrl("https://merchant.com/callback") + .build()) + .channelData(BrowserSession.builder() + .acceptHeader("Accept: *.*, q=0.1") + .javaEnabled(true) + .javascriptEnabled(true) + .language("FR-fr") + .colorDepth("16") + .screenWidth("1920") + .screenHeight("1080") + .timezone("60") + .userAgent("Mozilla/5.0") + .threeDsMethodCompletion(ThreeDsMethodCompletion.Y) + .ipAddress("1.12.123.255") + .build()) + .recurring(Recurring.builder().daysBetweenPayments(30L).expiry("99991231").build()) + .installment(Installment.builder() + .numberOfPayments(3L) + .daysBetweenPayments(30L) + .expiry("99991231") + .build()) + .optimization(Optimization.builder().optimized(true).framework("acceptance_rates").build()) + .initialTransaction(InitialTransaction.builder() + .acsTransactionId("acs-txn-id") + .authenticationMethod("frictionless") + .authenticationTimestamp("2026-08-03T10:11:12Z") + .authenticationData("auth-data") + .initialSessionId("sid_y3oqhf46pyzuxjbcn2giaqnb44") + .build()) + .deviceInformation(DeviceInformation.builder() + .deviceId("device-id") + .deviceSessionId("device-session-id") + .build()) + .build(); + } + + @Test + void shouldSerializeWithRequiredFieldsOnly() { + final SessionRequest request = SessionRequest.builder() + .source(SessionCardSource.builder().number("4485040371536584").build()) + .currency(Currency.USD) + .completion(NonHostedCompletionInfo.builder() + .callbackUrl("https://merchant.com/callback") + .build()) + .build(); + + assertDoesNotThrow(() -> serializer.toJson(request)); + } + + @Test + void shouldSerializeEveryProperty() { + final String json = serializer.toJson(fullyPopulated()); + + assertNotNull(json); + + final String[] expectedKeys = { + "source", + "amount", + "currency", + "processing_channel_id", + "marketplace", + "authentication_type", + "authentication_category", + "account_info", + "challenge_indicator", + "billing_descriptor", + "reference", + "merchant_risk_info", + "prior_transaction_reference", + "transaction_type", + "shipping_address", + "shipping_address_matches_billing", + "completion", + "channel_data", + "recurring", + "installment", + "optimization", + "initial_transaction", + "device_information" + }; + + for (final String key : expectedKeys) { + assertTrue(json.contains("\"" + key + "\""), "missing property " + key + " in " + json); + } + } + + @Test + void shouldSerializeScalarValuesAndEnumsAsSnakeCase() { + final String json = serializer.toJson(fullyPopulated()); + + assertTrue(json.contains("\"amount\":6540"), json); + assertTrue(json.contains("\"currency\":\"USD\""), json); + assertTrue(json.contains("\"processing_channel_id\":\"pc_q4dbxom5jbgudnjzjpz7iw4d0u\""), json); + assertTrue(json.contains("\"authentication_type\":\"regular\""), json); + assertTrue(json.contains("\"authentication_category\":\"payment\""), json); + assertTrue(json.contains("\"challenge_indicator\":\"trusted_listing_prompt\""), json); + assertTrue(json.contains("\"reference\":\"ORD-5023-4E89\""), json); + assertTrue(json.contains("\"prior_transaction_reference\":\"prior-txn-ref\""), json); + assertTrue(json.contains("\"transaction_type\":\"goods_service\""), json); + assertTrue(json.contains("\"shipping_address_matches_billing\":true"), json); + } + + @Test + void shouldSerializeNestedObjectContents() { + final String json = serializer.toJson(fullyPopulated()); + + assertTrue(json.contains("\"sub_entity_id\":\"ent_ocw5i74vowfg2edpy66izhts2u\""), json); + assertTrue(json.contains("\"purchase_count\":10"), json); + assertTrue(json.contains("\"name\":\"SUPERHEROES.COM\""), json); + assertTrue(json.contains("\"delivery_email\":\"bruce@wayne-enterprises.com\""), json); + assertTrue(json.contains("\"callback_url\":\"https://merchant.com/callback\""), json); + assertTrue(json.contains("\"number_of_payments\":3"), json); + assertTrue(json.contains("\"framework\":\"acceptance_rates\""), json); + assertTrue(json.contains("\"acs_transaction_id\":\"acs-txn-id\""), json); + assertTrue(json.contains("\"device_session_id\":\"device-session-id\""), json); + } + +} diff --git a/src/test/java/com/checkout/sessions/SessionSchemeTest.java b/src/test/java/com/checkout/sessions/SessionSchemeTest.java new file mode 100644 index 00000000..cd897ee0 --- /dev/null +++ b/src/test/java/com/checkout/sessions/SessionSchemeTest.java @@ -0,0 +1,81 @@ +package com.checkout.sessions; + +import com.checkout.GsonSerializer; +import com.checkout.Serializer; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +class SessionSchemeTest { + + private final Serializer serializer = new GsonSerializer(); + + private static Stream sessionSchemes() { + return Stream.of( + Arguments.of(SessionScheme.VISA, "\"visa\""), + Arguments.of(SessionScheme.MASTERCARD, "\"mastercard\""), + Arguments.of(SessionScheme.JCB, "\"jcb\""), + Arguments.of(SessionScheme.AMEX, "\"amex\""), + Arguments.of(SessionScheme.DINERS, "\"diners\""), + Arguments.of(SessionScheme.CARTES_BANCAIRES, "\"cartes_bancaires\""), + Arguments.of(SessionScheme.DISCOVER, "\"discover\""), + Arguments.of(SessionScheme.UPI, "\"upi\"") + ); + } + + @ParameterizedTest + @MethodSource("sessionSchemes") + void shouldSerializeSessionSchemeToSnakeCase(final SessionScheme value, final String expectedJson) { + assertEquals(expectedJson, serializer.toJson(value)); + } + + @ParameterizedTest + @MethodSource("sessionSchemes") + void shouldDeserializeSessionSchemeFromSnakeCase(final SessionScheme expected, final String json) { + assertEquals(expected, serializer.fromJson(json, SessionScheme.class)); + } + + @Test + void shouldExposeTheEightSchemesDefinedBySpec() { + assertEquals(8, SessionScheme.values().length); + } + + @Test + void shouldRoundTripAllValues() { + for (final SessionScheme value : SessionScheme.values()) { + assertEquals(value, serializer.fromJson(serializer.toJson(value), SessionScheme.class)); + } + } + + /** + * Gson resolves an unrecognised enum value to null rather than throwing, so a missing scheme + * would silently drop the required {@code scheme} field on a session response. + */ + @ParameterizedTest + @MethodSource("sessionSchemes") + void shouldDeserializeEverySchemeOnGetSessionResponse(final SessionScheme expected, final String json) { + final GetSessionResponse response = + serializer.fromJson("{\"scheme\":" + json + "}", GetSessionResponse.class); + + assertNotNull(response); + assertEquals(expected, response.getScheme()); + } + + @ParameterizedTest + @MethodSource("sessionSchemes") + void shouldDeserializeEverySchemeOnCreateSessionAcceptedResponse(final SessionScheme expected, + final String json) { + final CreateSessionAcceptedResponse response = + serializer.fromJson("{\"scheme\":" + json + "}", CreateSessionAcceptedResponse.class); + + assertNotNull(response); + assertEquals(expected, response.getScheme()); + } + +} diff --git a/src/test/java/com/checkout/sessions/TransactionTypeTest.java b/src/test/java/com/checkout/sessions/TransactionTypeTest.java new file mode 100644 index 00000000..6c1249cb --- /dev/null +++ b/src/test/java/com/checkout/sessions/TransactionTypeTest.java @@ -0,0 +1,84 @@ +package com.checkout.sessions; + +import com.checkout.GsonSerializer; +import com.checkout.Serializer; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class TransactionTypeTest { + + private final Serializer serializer = new GsonSerializer(); + + private static Stream transactionTypes() { + return Stream.of( + Arguments.of(TransactionType.GOODS_SERVICE, "\"goods_service\""), + Arguments.of(TransactionType.CHECK_ACCEPTANCE, "\"check_acceptance\""), + Arguments.of(TransactionType.ACCOUNT_FUNDING, "\"account_funding\""), + Arguments.of(TransactionType.QUASI_CARD_TRANSACTION, "\"quasi_card_transaction\""), + Arguments.of(TransactionType.PREPAID_ACTIVATION_AND_LOAD, "\"prepaid_activation_and_load\"") + ); + } + + @ParameterizedTest + @MethodSource("transactionTypes") + void shouldSerializeTransactionTypeToSnakeCase(final TransactionType value, final String expectedJson) { + assertEquals(expectedJson, serializer.toJson(value)); + } + + @ParameterizedTest + @MethodSource("transactionTypes") + void shouldDeserializeTransactionTypeFromSnakeCase(final TransactionType expected, final String json) { + assertEquals(expected, serializer.fromJson(json, TransactionType.class)); + } + + @Test + void shouldExposeTheFiveTypesDefinedBySpec() { + assertEquals(5, TransactionType.values().length); + } + + @Test + void shouldRoundTripAllValues() { + for (final TransactionType value : TransactionType.values()) { + assertEquals(value, serializer.fromJson(serializer.toJson(value), TransactionType.class)); + } + } + + /** + * The constant name retains a historical misspelling, but the API value is + * {@code quasi_card_transaction}. Guards both directions of the wire contract. + */ + @Test + void shouldUseTheSpecSpellingForQuasiCardTransaction() { + assertEquals("\"quasi_card_transaction\"", serializer.toJson(TransactionType.QUASI_CARD_TRANSACTION)); + assertEquals(TransactionType.QUASI_CARD_TRANSACTION, + serializer.fromJson("\"quasi_card_transaction\"", TransactionType.class)); + } + + @Test + void shouldSerializeQuasiCardTransactionOnSessionRequest() { + final SessionRequest request = SessionRequest.builder() + .transactionType(TransactionType.QUASI_CARD_TRANSACTION) + .build(); + + assertTrue(serializer.toJson(request).contains("\"transaction_type\":\"quasi_card_transaction\"")); + } + + @ParameterizedTest + @MethodSource("transactionTypes") + void shouldDeserializeEveryTypeOnGetSessionResponse(final TransactionType expected, final String json) { + final GetSessionResponse response = + serializer.fromJson("{\"transaction_type\":" + json + "}", GetSessionResponse.class); + + assertNotNull(response); + assertEquals(expected, response.getTransactionType()); + } + +}