diff --git a/README.md b/README.md index a4255de..c7fe996 100644 --- a/README.md +++ b/README.md @@ -195,9 +195,10 @@ Contributions to PG Java SDK are welcome! Here's how you can contribute: 1. Fork the repository 2. Create a feature branch (`git checkout -b feature/amazing-feature`) -3. Commit your changes (`git commit -m 'Add some amazing feature'`) -4. Push to the branch (`git push origin feature/amazing-feature`) -5. Open a Pull Request +3. Make sure to set up the pre-commit hook by running the command `pre-commit install` +4. Commit your changes (`git commit -m 'Add some amazing feature'`) +5. Push to the branch (`git push origin feature/amazing-feature`) +6. Open a Pull Request Please ensure your code follows the project's coding standards and includes appropriate tests. diff --git a/pom.xml b/pom.xml index 2fcaa72..11ceac7 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.phonepe pg-sdk-java - 2.2.1 + 2.2.2 B2B JAVA SDK https://github.com/PhonePe/phonepe-pg-sdk-java Phonepe PG JAVA SDK diff --git a/src/main/java/com/phonepe/sdk/pg/common/constants/Headers.java b/src/main/java/com/phonepe/sdk/pg/common/constants/Headers.java index 89ee09d..d8c6e4b 100644 --- a/src/main/java/com/phonepe/sdk/pg/common/constants/Headers.java +++ b/src/main/java/com/phonepe/sdk/pg/common/constants/Headers.java @@ -51,4 +51,5 @@ public class Headers { public static final String OAUTH_AUTHORIZATION = "Authorization"; public static final String CONTENT_TYPE = "Content-Type"; public static final String ACCEPT = "Accept"; + public static final String X_DEVICE_OS = "x-device-os"; } diff --git a/src/main/java/com/phonepe/sdk/pg/common/models/request/PgPaymentRequest.java b/src/main/java/com/phonepe/sdk/pg/common/models/request/PgPaymentRequest.java index ce33a9b..f382093 100644 --- a/src/main/java/com/phonepe/sdk/pg/common/models/request/PgPaymentRequest.java +++ b/src/main/java/com/phonepe/sdk/pg/common/models/request/PgPaymentRequest.java @@ -15,6 +15,7 @@ */ package com.phonepe.sdk.pg.common.models.request; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.phonepe.sdk.pg.common.models.MetaInfo; @@ -59,6 +60,15 @@ public class PgPaymentRequest { private Long expireAfter; private Long expireAt; + // The "x-device-os" request header is currently required only for UPI COLLECT transactions. + // Once the COLLECT payment instrument is fully disabled, as per the guidelines, + // this header will no longer be necessary, even for iOS devices. + // To streamline the request structure and avoid unnecessary complexity, + // this header is now being incorporated directly into the request builder + // instead of being managed as a separate value. + @JsonIgnore + private String deviceOS; + private PgPaymentRequest( String merchantOrderId, Long amount, @@ -73,8 +83,7 @@ private PgPaymentRequest( } @Builder( - builderClassName = "UpiIntentPayRequestBuilder", - builderMethodName = "UpiIntentPayRequestBuilder") + builderClassName = "UpiIntentPayRequestBuilder", builderMethodName = "UpiIntentPayRequestBuilder") public PgPaymentRequest( String merchantOrderId, long amount, @@ -85,21 +94,21 @@ public PgPaymentRequest( String targetApp, Long expireAfter) { this(merchantOrderId, amount, metaInfo, constraints, expireAfter); - this.deviceContext = - DeviceContext.builder() - .deviceOS(deviceOS) - .merchantCallBackScheme(merchantCallBackScheme) - .build(); - this.paymentFlow = - PgPaymentFlow.builder() - .paymentMode( - IntentPaymentV2Instrument.builder().targetApp(targetApp).build()) - .build(); + this.deviceContext = DeviceContext.builder() + .deviceOS(deviceOS) + .merchantCallBackScheme(merchantCallBackScheme) + .build(); + this.paymentFlow = PgPaymentFlow.builder() + .paymentMode( + IntentPaymentV2Instrument.builder() + .targetApp(targetApp) + .build()) + .build(); } @Builder( - builderClassName = "UpiCollectPayViaVpaRequestBuilder", - builderMethodName = "UpiCollectPayViaVpaRequestBuilder") + builderClassName = "UpiCollectPayViaVpaRequestBuilder", builderMethodName = + "UpiCollectPayViaVpaRequestBuilder") public PgPaymentRequest( long amount, String merchantOrderId, @@ -107,22 +116,25 @@ public PgPaymentRequest( List constraints, String vpa, String message, - Long expireAfter) { + Long expireAfter, + String deviceOS) { this(merchantOrderId, amount, metaInfo, constraints, expireAfter); - this.paymentFlow = - PgPaymentFlow.builder() - .paymentMode( - CollectPaymentV2Instrument.builder() - .details( - VpaCollectPaymentDetails.builder().vpa(vpa).build()) - .message(message) - .build()) - .build(); + this.deviceOS = deviceOS; + this.paymentFlow = PgPaymentFlow.builder() + .paymentMode( + CollectPaymentV2Instrument.builder() + .details( + VpaCollectPaymentDetails.builder() + .vpa(vpa) + .build()) + .message(message) + .build()) + .build(); } @Builder( - builderClassName = "UpiCollectPayViaPhoneNumberRequestBuilder", - builderMethodName = "UpiCollectPayViaPhoneNumberRequestBuilder") + builderClassName = "UpiCollectPayViaPhoneNumberRequestBuilder", builderMethodName = + "UpiCollectPayViaPhoneNumberRequestBuilder") public PgPaymentRequest( long amount, MetaInfo metaInfo, @@ -130,19 +142,20 @@ public PgPaymentRequest( String phoneNumber, List constraints, String message, - Long expireAfter) { + Long expireAfter, + String deviceOS) { this(merchantOrderId, amount, metaInfo, constraints, expireAfter); - this.paymentFlow = - PgPaymentFlow.builder() - .paymentMode( - CollectPaymentV2Instrument.builder() - .details( - PhoneNumberCollectPaymentDetails.builder() - .phoneNumber(phoneNumber) - .build()) - .message(message) - .build()) - .build(); + this.deviceOS = deviceOS; + this.paymentFlow = PgPaymentFlow.builder() + .paymentMode( + CollectPaymentV2Instrument.builder() + .details( + PhoneNumberCollectPaymentDetails.builder() + .phoneNumber(phoneNumber) + .build()) + .message(message) + .build()) + .build(); } @Builder(builderClassName = "UpiQrRequestBuilder", builderMethodName = "UpiQrRequestBuilder") @@ -153,15 +166,14 @@ public PgPaymentRequest( List constraints, Long expireAfter) { this(merchantOrderId, amount, metaInfo, constraints, expireAfter); - this.paymentFlow = - PgPaymentFlow.builder() - .paymentMode(UpiQrPaymentV2Instrument.builder().build()) - .build(); + this.paymentFlow = PgPaymentFlow.builder() + .paymentMode(UpiQrPaymentV2Instrument.builder() + .build()) + .build(); } @Builder( - builderClassName = "NetBankingPayRequestBuilder", - builderMethodName = "NetBankingPayRequestBuilder") + builderClassName = "NetBankingPayRequestBuilder", builderMethodName = "NetBankingPayRequestBuilder") public PgPaymentRequest( long amount, MetaInfo metaInfo, @@ -172,20 +184,20 @@ public PgPaymentRequest( String redirectUrl, Long expireAfter) { this(merchantOrderId, amount, metaInfo, constraints, expireAfter); - this.paymentFlow = - PgPaymentFlow.builder() - .paymentMode( - NetBankingPaymentV2Instrument.builder() - .bankId(bankId) - .merchantUserId(merchantUserId) - .build()) - .merchantUrls(MerchantUrls.builder().redirectUrl(redirectUrl).build()) - .build(); + this.paymentFlow = PgPaymentFlow.builder() + .paymentMode( + NetBankingPaymentV2Instrument.builder() + .bankId(bankId) + .merchantUserId(merchantUserId) + .build()) + .merchantUrls(MerchantUrls.builder() + .redirectUrl(redirectUrl) + .build()) + .build(); } @Builder( - builderClassName = "TokenPayRequestBuilder", - builderMethodName = "TokenPayRequestBuilder") + builderClassName = "TokenPayRequestBuilder", builderMethodName = "TokenPayRequestBuilder") public PgPaymentRequest( String merchantOrderId, long amount, @@ -204,34 +216,34 @@ public PgPaymentRequest( List constraints, Long expireAfter) { this(merchantOrderId, amount, metaInfo, constraints, expireAfter); - this.paymentFlow = - PgPaymentFlow.builder() - .paymentMode( - TokenPaymentV2Instrument.builder() - .merchantUserId(merchantUserId) - .authMode(authMode) - .tokenDetails( - TokenDetails.builder() - .cardHolderName(cardHolderName) - .cryptogram(cryptogram) - .encryptedCvv(encryptedCvv) - .encryptedToken(encryptedToken) - .encryptionKeyId(encryptionKeyId) - .panSuffix(panSuffix) - .expiry( - Expiry.builder() - .month(expiryMonth) - .year(expiryYear) - .build()) - .build()) - .build()) - .merchantUrls(MerchantUrls.builder().redirectUrl(redirectUrl).build()) - .build(); + this.paymentFlow = PgPaymentFlow.builder() + .paymentMode( + TokenPaymentV2Instrument.builder() + .merchantUserId(merchantUserId) + .authMode(authMode) + .tokenDetails( + TokenDetails.builder() + .cardHolderName(cardHolderName) + .cryptogram(cryptogram) + .encryptedCvv(encryptedCvv) + .encryptedToken(encryptedToken) + .encryptionKeyId(encryptionKeyId) + .panSuffix(panSuffix) + .expiry( + Expiry.builder() + .month(expiryMonth) + .year(expiryYear) + .build()) + .build()) + .build()) + .merchantUrls(MerchantUrls.builder() + .redirectUrl(redirectUrl) + .build()) + .build(); } @Builder( - builderClassName = "CardPayRequestBuilder", - builderMethodName = "CardPayRequestBuilder") + builderClassName = "CardPayRequestBuilder", builderMethodName = "CardPayRequestBuilder") public PgPaymentRequest( String merchantOrderId, long amount, @@ -248,32 +260,33 @@ public PgPaymentRequest( String redirectUrl, Long expireAfter) { this(merchantOrderId, amount, metaInfo, constraints, expireAfter); - this.paymentFlow = - PgPaymentFlow.builder() - .paymentMode( - CardPaymentV2Instrument.builder() - .merchantUserId(merchantUserId) - .authMode(authMode) - .cardDetails( - NewCardDetails.builder() - .cardHolderName(cardHolderName) - .encryptedCvv(encryptedCvv) - .encryptionKeyId(encryptionKeyId) - .encryptedCardNumber(encryptedCardNumber) - .expiry( - Expiry.builder() - .month(expiryMonth) - .year(expiryYear) - .build()) - .build()) - .build()) - .merchantUrls(MerchantUrls.builder().redirectUrl(redirectUrl).build()) - .build(); + this.paymentFlow = PgPaymentFlow.builder() + .paymentMode( + CardPaymentV2Instrument.builder() + .merchantUserId(merchantUserId) + .authMode(authMode) + .cardDetails( + NewCardDetails.builder() + .cardHolderName(cardHolderName) + .encryptedCvv(encryptedCvv) + .encryptionKeyId(encryptionKeyId) + .encryptedCardNumber(encryptedCardNumber) + .expiry( + Expiry.builder() + .month(expiryMonth) + .year(expiryYear) + .build()) + .build()) + .build()) + .merchantUrls(MerchantUrls.builder() + .redirectUrl(redirectUrl) + .build()) + .build(); } @Builder( - builderClassName = "SubscriptionSetupUpiIntentBuilder", - builderMethodName = "SubscriptionSetupUpiIntentBuilder") + builderClassName = "SubscriptionSetupUpiIntentBuilder", builderMethodName = + "SubscriptionSetupUpiIntentBuilder") public PgPaymentRequest( String merchantOrderId, String merchantSubscriptionId, @@ -291,27 +304,27 @@ public PgPaymentRequest( List constraints) { this(merchantOrderId, amount, metaInfo, constraints, null); this.expireAt = orderExpireAt; - this.deviceContext = - DeviceContext.builder() - .deviceOS(deviceOS) - .merchantCallBackScheme(merchantCallbackScheme) - .build(); - this.paymentFlow = - SubscriptionSetupPaymentFlow.builder() - .merchantSubscriptionId(merchantSubscriptionId) - .amountType(amountType) - .authWorkflowType(authWorkflowType) - .expireAt(subscriptionExpireAt) - .frequency(frequency) - .maxAmount(maxAmount) - .paymentMode( - IntentPaymentV2Instrument.builder().targetApp(targetApp).build()) - .build(); + this.deviceContext = DeviceContext.builder() + .deviceOS(deviceOS) + .merchantCallBackScheme(merchantCallbackScheme) + .build(); + this.paymentFlow = SubscriptionSetupPaymentFlow.builder() + .merchantSubscriptionId(merchantSubscriptionId) + .amountType(amountType) + .authWorkflowType(authWorkflowType) + .expireAt(subscriptionExpireAt) + .frequency(frequency) + .maxAmount(maxAmount) + .paymentMode( + IntentPaymentV2Instrument.builder() + .targetApp(targetApp) + .build()) + .build(); } @Builder( - builderClassName = "SubscriptionSetupUpiCollectBuilder", - builderMethodName = "SubscriptionSetupUpiCollectBuilder") + builderClassName = "SubscriptionSetupUpiCollectBuilder", builderMethodName = + "SubscriptionSetupUpiCollectBuilder") public PgPaymentRequest( String merchantOrderId, String merchantSubscriptionId, @@ -328,26 +341,27 @@ public PgPaymentRequest( List constraints) { this(merchantOrderId, amount, metaInfo, constraints, null); this.expireAt = orderExpireAt; - this.paymentFlow = - SubscriptionSetupPaymentFlow.builder() - .merchantSubscriptionId(merchantSubscriptionId) - .amountType(amountType) - .authWorkflowType(authWorkflowType) - .expireAt(subscriptionExpireAt) - .frequency(frequency) - .maxAmount(maxAmount) - .paymentMode( - CollectPaymentV2Instrument.builder() - .details( - VpaCollectPaymentDetails.builder().vpa(vpa).build()) - .message(message) - .build()) - .build(); + this.paymentFlow = SubscriptionSetupPaymentFlow.builder() + .merchantSubscriptionId(merchantSubscriptionId) + .amountType(amountType) + .authWorkflowType(authWorkflowType) + .expireAt(subscriptionExpireAt) + .frequency(frequency) + .maxAmount(maxAmount) + .paymentMode( + CollectPaymentV2Instrument.builder() + .details( + VpaCollectPaymentDetails.builder() + .vpa(vpa) + .build()) + .message(message) + .build()) + .build(); } @Builder( - builderClassName = "SubscriptionNotifyRequestBuilder", - builderMethodName = "SubscriptionNotifyRequestBuilder") + builderClassName = "SubscriptionNotifyRequestBuilder", builderMethodName = + "SubscriptionNotifyRequestBuilder") public PgPaymentRequest( String merchantOrderId, Long amount, @@ -359,11 +373,10 @@ public PgPaymentRequest( List constraints) { this(merchantOrderId, amount, metaInfo, constraints, null); this.expireAt = expireAt; - this.paymentFlow = - SubscriptionRedemptionPaymentFlow.builder() - .redemptionRetryStrategy(redemptionRetryStrategy) - .autoDebit(autoDebit) - .merchantSubscriptionId(merchantSubscriptionId) - .build(); + this.paymentFlow = SubscriptionRedemptionPaymentFlow.builder() + .redemptionRetryStrategy(redemptionRetryStrategy) + .autoDebit(autoDebit) + .merchantSubscriptionId(merchantSubscriptionId) + .build(); } } diff --git a/src/main/java/com/phonepe/sdk/pg/payments/v2/CustomCheckoutClient.java b/src/main/java/com/phonepe/sdk/pg/payments/v2/CustomCheckoutClient.java index 1cec884..029401d 100644 --- a/src/main/java/com/phonepe/sdk/pg/payments/v2/CustomCheckoutClient.java +++ b/src/main/java/com/phonepe/sdk/pg/payments/v2/CustomCheckoutClient.java @@ -64,31 +64,29 @@ private CustomCheckoutClient( /** * Generates a CustomCheckout Client for interacting with the PhonePe APIs * - * @param clientId Unique client-id assigned to merchant by PhonePe - * @param clientSecret Secret provided by PhonePe + * @param clientId Unique client-id assigned to merchant by PhonePe + * @param clientSecret Secret provided by PhonePe * @param clientVersion The client version used for secure transactions - * @param env Set to `Env.SANDBOX` for the SANDBOX environment or `Env.PRODUCTION` for the - * production environment. + * @param env Set to `Env.SANDBOX` for the SANDBOX environment or `Env.PRODUCTION` for the production + * environment. * @return CustomCheckoutClient object for interacting with the PhonePe APIs */ public static CustomCheckoutClient getInstance( final String clientId, final String clientSecret, final Integer clientVersion, - final Env env) - throws PhonePeException { + final Env env) throws PhonePeException { return getInstance(clientId, clientSecret, clientVersion, env, true); } /** * Generates a CustomCheckout Client for interacting with the PhonePe APIs * - * @param clientId received at the time of onboarding - * @param clientSecret received at the time of onboarding - * @param clientVersion received at the time of onboarding - * @param env environment to be used by the merchant - * @param shouldPublishEvents When true, events are sent to PhonePe providing smoother - * experience + * @param clientId received at the time of onboarding + * @param clientSecret received at the time of onboarding + * @param clientVersion received at the time of onboarding + * @param env environment to be used by the merchant + * @param shouldPublishEvents When true, events are sent to PhonePe providing smoother experience * @return CustomCheckoutClient object for interacting with the PhonePe APIs */ public static CustomCheckoutClient getInstance( @@ -96,8 +94,7 @@ public static CustomCheckoutClient getInstance( final String clientSecret, final Integer clientVersion, final Env env, - boolean shouldPublishEvents) - throws PhonePeException { + boolean shouldPublishEvents) throws PhonePeException { final boolean shouldPublishInProd = shouldPublishEvents && env == Env.PRODUCTION; return new CustomCheckoutClient( clientId, clientSecret, clientVersion, env, shouldPublishInProd); @@ -113,14 +110,23 @@ public static CustomCheckoutClient getInstance( public PgPaymentResponse pay(PgPaymentRequest pgPaymentRequest) { String url = CustomCheckoutConstants.PAY_API; try { - PgPaymentResponse pgPaymentResponse = - requestViaAuthRefresh( - HttpMethodType.POST, - pgPaymentRequest, - url, - null, - new TypeReference() {}, - headers); + List requestHeaders = headers; + if (pgPaymentRequest.getDeviceOS() != null && !pgPaymentRequest.getDeviceOS() + .isBlank()) { + requestHeaders = new ArrayList<>(headers); + requestHeaders.add( + HttpHeaderPair.builder() + .key(Headers.X_DEVICE_OS) + .value(pgPaymentRequest.getDeviceOS()) + .build()); + } + PgPaymentResponse pgPaymentResponse = requestViaAuthRefresh( + HttpMethodType.POST, + pgPaymentRequest, + url, + null, + new TypeReference() {}, + requestHeaders); this.eventPublisher.send( BaseEvent.buildCustomCheckoutPayEvent( EventState.SUCCESS, pgPaymentRequest, url, EventType.PAY_SUCCESS)); @@ -152,24 +158,23 @@ public OrderStatusResponse getOrderStatus(String merchantOrderId) { * Gets status of an order * * @param merchantOrderId Order id generated by merchant - * @param details true -> order status has all attempt details under paymentDetails list false - * -> order status has only latest attempt details under paymentDetails list + * @param details true -> order status has all attempt details under paymentDetails list false -> order + * status has only latest attempt details under paymentDetails list * @return OrderStatusResponse Response with order and transaction details */ @SneakyThrows public OrderStatusResponse getOrderStatus(String merchantOrderId, boolean details) { String url = String.format(CustomCheckoutConstants.ORDER_STATUS_API, merchantOrderId); try { - OrderStatusResponse orderStatusResponse = - requestViaAuthRefresh( - HttpMethodType.GET, - null, - url, - Collections.singletonMap( - CustomCheckoutConstants.ORDER_DETAILS, - Boolean.toString(details)), - new TypeReference() {}, - headers); + OrderStatusResponse orderStatusResponse = requestViaAuthRefresh( + HttpMethodType.GET, + null, + url, + Collections.singletonMap( + CustomCheckoutConstants.ORDER_DETAILS, + Boolean.toString(details)), + new TypeReference() {}, + headers); this.eventPublisher.send( BaseEvent.buildOrderStatusEvent( EventState.SUCCESS, @@ -201,14 +206,13 @@ public OrderStatusResponse getOrderStatus(String merchantOrderId, boolean detail public RefundResponse refund(RefundRequest refundRequest) { String url = CustomCheckoutConstants.REFUND_API; try { - RefundResponse refundResponse = - requestViaAuthRefresh( - HttpMethodType.POST, - refundRequest, - url, - null, - new TypeReference() {}, - headers); + RefundResponse refundResponse = requestViaAuthRefresh( + HttpMethodType.POST, + refundRequest, + url, + null, + new TypeReference() {}, + headers); this.eventPublisher.send( BaseEvent.buildRefundEvent( EventState.SUCCESS, @@ -240,14 +244,13 @@ public RefundResponse refund(RefundRequest refundRequest) { public CreateSdkOrderResponse createSdkOrder(CreateSdkOrderRequest createSdkOrderRequest) { String url = CustomCheckoutConstants.CREATE_ORDER_API; try { - CreateSdkOrderResponse createSdkOrderResponse = - requestViaAuthRefresh( - HttpMethodType.POST, - createSdkOrderRequest, - url, - null, - new TypeReference() {}, - headers); + CreateSdkOrderResponse createSdkOrderResponse = requestViaAuthRefresh( + HttpMethodType.POST, + createSdkOrderRequest, + url, + null, + new TypeReference() {}, + headers); this.eventPublisher.send( BaseEvent.buildCreateSdkOrderEvent( EventState.SUCCESS, @@ -278,17 +281,15 @@ public CreateSdkOrderResponse createSdkOrder(CreateSdkOrderRequest createSdkOrde */ @SneakyThrows public OrderStatusResponse getTransactionStatus(String transactionId) { - final String url = - String.format(CustomCheckoutConstants.TRANSACTION_STATUS_API, transactionId); + final String url = String.format(CustomCheckoutConstants.TRANSACTION_STATUS_API, transactionId); try { - OrderStatusResponse orderStatusResponse = - requestViaAuthRefresh( - HttpMethodType.GET, - null, - url, - null, - new TypeReference() {}, - headers); + OrderStatusResponse orderStatusResponse = requestViaAuthRefresh( + HttpMethodType.GET, + null, + url, + null, + new TypeReference() {}, + headers); this.eventPublisher.send( BaseEvent.buildTransactionStatusEvent( EventState.SUCCESS, @@ -321,14 +322,13 @@ public OrderStatusResponse getTransactionStatus(String transactionId) { public RefundStatusResponse getRefundStatus(String refundId) { final String url = String.format(CustomCheckoutConstants.REFUND_STATUS_API, refundId); try { - RefundStatusResponse refundStatusResponse = - requestViaAuthRefresh( - HttpMethodType.GET, - null, - url, - null, - new TypeReference() {}, - headers); + RefundStatusResponse refundStatusResponse = requestViaAuthRefresh( + HttpMethodType.GET, + null, + url, + null, + new TypeReference() {}, + headers); this.eventPublisher.send( BaseEvent.buildRefundStatusEvent( EventState.SUCCESS, @@ -353,10 +353,10 @@ public RefundStatusResponse getRefundStatus(String refundId) { /** * Validate if the callback is valid * - * @param username username set by the merchant on the dashboard - * @param password password set by the merchant on the dashboard + * @param username username set by the merchant on the dashboard + * @param password password set by the merchant on the dashboard * @param authorization String data under `authorization` key of response headers - * @param responseBody Callback response body + * @param responseBody Callback response body * @return CallbackResponse Deserialized callback body * @throws PhonePeException when callback is not valid */ @@ -379,13 +379,21 @@ public CallbackResponse validateCallback( } } - /** Prepares the headers for CustomCheckout Client */ + /** + * Prepares the headers for CustomCheckout Client + */ private void prepareHeaders() { this.headers = new ArrayList<>(); headers.add( - HttpHeaderPair.builder().key(Headers.CONTENT_TYPE).value(APPLICATION_JSON).build()); + HttpHeaderPair.builder() + .key(Headers.CONTENT_TYPE) + .value(APPLICATION_JSON) + .build()); headers.add( - HttpHeaderPair.builder().key(Headers.SOURCE).value(Headers.INTEGRATION).build()); + HttpHeaderPair.builder() + .key(Headers.SOURCE) + .value(Headers.INTEGRATION) + .build()); headers.add( HttpHeaderPair.builder() .key(Headers.SOURCE_VERSION) diff --git a/src/main/java/com/phonepe/sdk/pg/payments/v2/models/request/PrefillUserLoginDetails.java b/src/main/java/com/phonepe/sdk/pg/payments/v2/models/request/PrefillUserLoginDetails.java new file mode 100644 index 0000000..835a88a --- /dev/null +++ b/src/main/java/com/phonepe/sdk/pg/payments/v2/models/request/PrefillUserLoginDetails.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2025 Original Author(s), PhonePe India Pvt. Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.phonepe.sdk.pg.payments.v2.models.request; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +@JsonInclude(Include.NON_NULL) +public class PrefillUserLoginDetails { + + private String phoneNumber; +} diff --git a/src/main/java/com/phonepe/sdk/pg/payments/v2/models/request/StandardCheckoutPayRequest.java b/src/main/java/com/phonepe/sdk/pg/payments/v2/models/request/StandardCheckoutPayRequest.java index 98714bc..b6e10f9 100644 --- a/src/main/java/com/phonepe/sdk/pg/payments/v2/models/request/StandardCheckoutPayRequest.java +++ b/src/main/java/com/phonepe/sdk/pg/payments/v2/models/request/StandardCheckoutPayRequest.java @@ -32,15 +32,16 @@ public class StandardCheckoutPayRequest { private PaymentFlow paymentFlow; private Long expireAfter; private Boolean disablePaymentRetry; + private PrefillUserLoginDetails prefillUserLoginDetails; /** * Builds Standard Checkout Pay Request * * @param merchantOrderId The unique order ID generated by the merchant - * @param amount Order amount in paisa - * @param redirectUrl URL where user will be redirected after success/failed payment - * @param metaInfo Merchant defined meta info to store additional information - * @param message Payment message shown in APP for collect requests + * @param amount Order amount in paisa + * @param redirectUrl URL where user will be redirected after success/failed payment + * @param metaInfo Merchant defined meta info to store additional information + * @param message Payment message shown in APP for collect requests */ @Builder public StandardCheckoutPayRequest( @@ -51,12 +52,15 @@ public StandardCheckoutPayRequest( String message, Long expireAfter, PaymentModeConfig paymentModeConfig, - Boolean disablePaymentRetry) { + Boolean disablePaymentRetry, + PrefillUserLoginDetails prefillUserLoginDetails) { this.merchantOrderId = merchantOrderId; this.amount = amount; this.metaInfo = metaInfo; this.expireAfter = expireAfter; - MerchantUrls merchantUrls = MerchantUrls.builder().redirectUrl(redirectUrl).build(); + MerchantUrls merchantUrls = MerchantUrls.builder() + .redirectUrl(redirectUrl) + .build(); this.setPaymentFlow( PgCheckoutPaymentFlow.builder() .message(message) @@ -64,5 +68,6 @@ public StandardCheckoutPayRequest( .paymentModeConfig(paymentModeConfig) .build()); this.disablePaymentRetry = disablePaymentRetry; + this.prefillUserLoginDetails = prefillUserLoginDetails; } } diff --git a/src/test/PayTest.java b/src/test/PayTest.java index bc8ce17..6bb311e 100644 --- a/src/test/PayTest.java +++ b/src/test/PayTest.java @@ -16,6 +16,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import com.google.common.collect.Maps; +import com.phonepe.sdk.pg.common.constants.Headers; import com.phonepe.sdk.pg.common.exception.PhonePeException; import com.phonepe.sdk.pg.common.http.PhonePeResponse; import com.phonepe.sdk.pg.common.models.request.PgPaymentRequest; @@ -29,11 +30,13 @@ import com.phonepe.sdk.pg.common.models.response.PgPaymentResponse; import com.phonepe.sdk.pg.payments.v2.customcheckout.CustomCheckoutConstants; import com.phonepe.sdk.pg.payments.v2.models.request.PaymentModeConfig; +import com.phonepe.sdk.pg.payments.v2.models.request.PrefillUserLoginDetails; import com.phonepe.sdk.pg.payments.v2.models.request.StandardCheckoutPayRequest; import com.phonepe.sdk.pg.payments.v2.models.response.StandardCheckoutPayResponse; import com.phonepe.sdk.pg.payments.v2.standardcheckout.StandardCheckoutConstants; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -163,6 +166,78 @@ void testCustomPay() { Assertions.assertEquals(pgPaymentResponse, actual); } + @SneakyThrows + @Test + void testCustomPayUpiCollectWithXDeviceOsHeader() { + final String url = CustomCheckoutConstants.PAY_API; + + PgPaymentRequest request = + PgPaymentRequest.UpiCollectPayViaVpaRequestBuilder() + .merchantOrderId("MerchantOrderId") + .amount(100) + .vpa("user@upi") + .deviceOS("ANDROID") + .build(); + PgPaymentResponse pgPaymentResponse = + PgPaymentResponse.builder() + .orderId("OMO2403071446458436434329") + .state("PENDING") + .expireAt(java.time.Instant.now().getEpochSecond()) + .redirectUrl("mercury.com") + .build(); + + Map headers = new HashMap<>(getHeaders()); + headers.put(Headers.X_DEVICE_OS, "ANDROID"); + + addStubForPostRequest( + url, headers, request, HttpStatus.SC_OK, Maps.newHashMap(), pgPaymentResponse); + + PgPaymentResponse actual = customCheckoutClient.pay(request); + Assertions.assertEquals(pgPaymentResponse, actual); + } + + @SneakyThrows + @Test + void testCustomPayUpiCollectWithoutXDeviceOsHeader() { + final String url = CustomCheckoutConstants.PAY_API; + + PgPaymentRequest request = + PgPaymentRequest.UpiCollectPayViaVpaRequestBuilder() + .merchantOrderId("MerchantOrderId") + .amount(100) + .vpa("user@upi") + .build(); + PgPaymentResponse pgPaymentResponse = + PgPaymentResponse.builder() + .orderId("OMO2403071446458436434329") + .state("PENDING") + .expireAt(java.time.Instant.now().getEpochSecond()) + .redirectUrl("mercury.com") + .build(); + + addStubForPostRequest( + url, getHeaders(), request, HttpStatus.SC_OK, Maps.newHashMap(), pgPaymentResponse); + + PgPaymentResponse actual = customCheckoutClient.pay(request); + Assertions.assertEquals(pgPaymentResponse, actual); + } + + @SneakyThrows + @Test + void testCustomPayUpiCollectXDeviceOsNotInRequestBody() { + PgPaymentRequest request = + PgPaymentRequest.UpiCollectPayViaVpaRequestBuilder() + .merchantOrderId("MerchantOrderId") + .amount(100) + .vpa("user@upi") + .deviceOS("ANDROID") + .build(); + + String json = objectMapper.writeValueAsString(request); + Assertions.assertFalse(json.contains("xDeviceOs"), + "xDeviceOs should not be serialized into the request body"); + } + @SneakyThrows @Test void testStandardCheckoutPayWithDisablePaymentRetryTrue() { @@ -263,4 +338,113 @@ void testStandardCheckoutPayWithDisablePaymentRetryNull() { Assertions.assertEquals(standardCheckoutResponse, actual); Assertions.assertNull(standardCheckoutPayRequest.getDisablePaymentRetry()); } + + @SneakyThrows + @Test + void testStandardCheckoutPayWithPrefillUserLoginDetails() { + final String url = StandardCheckoutConstants.PAY_API; + String redirectUrl = "https://redirectUrl.com"; + String prefilledMobileNumber = "+919090909009"; + + PrefillUserLoginDetails prefillUserLoginDetails = PrefillUserLoginDetails.builder() + .phoneNumber("+919090909009") + .build(); + StandardCheckoutPayRequest standardCheckoutPayRequest = + StandardCheckoutPayRequest.builder() + .merchantOrderId(merchantOrderId) + .amount(amount) + .prefillUserLoginDetails(prefillUserLoginDetails) + .redirectUrl(redirectUrl) + .build(); + StandardCheckoutPayResponse standardCheckoutResponse = + StandardCheckoutPayResponse.builder() + .orderId(String.valueOf(java.time.Instant.now().getEpochSecond())) + .state("PENDING") + .expireAt(java.time.Instant.now().getEpochSecond()) + .redirectUrl("https://google.com") + .build(); + Map headers = getHeaders(); + + addStubForPostRequest( + url, + headers, + standardCheckoutPayRequest, + HttpStatus.SC_OK, + Maps.newHashMap(), + standardCheckoutResponse); + StandardCheckoutPayResponse actual = standardCheckoutClient.pay(standardCheckoutPayRequest); + Assertions.assertEquals(standardCheckoutResponse, actual); + Assertions.assertNotNull(standardCheckoutPayRequest.getPrefillUserLoginDetails()); + Assertions.assertEquals(prefilledMobileNumber, standardCheckoutPayRequest.getPrefillUserLoginDetails().getPhoneNumber()); + } + + @SneakyThrows + @Test + void testStandardCheckoutPayWithEmptyPrefillUserLoginDetails() { + final String url = StandardCheckoutConstants.PAY_API; + String redirectUrl = "https://redirectUrl.com"; + + PrefillUserLoginDetails prefillUserLoginDetails = PrefillUserLoginDetails.builder() + .build(); + StandardCheckoutPayRequest standardCheckoutPayRequest = + StandardCheckoutPayRequest.builder() + .merchantOrderId(merchantOrderId) + .amount(amount) + .prefillUserLoginDetails(prefillUserLoginDetails) + .redirectUrl(redirectUrl) + .build(); + StandardCheckoutPayResponse standardCheckoutResponse = + StandardCheckoutPayResponse.builder() + .orderId(String.valueOf(java.time.Instant.now().getEpochSecond())) + .state("PENDING") + .expireAt(java.time.Instant.now().getEpochSecond()) + .redirectUrl("https://google.com") + .build(); + Map headers = getHeaders(); + + addStubForPostRequest( + url, + headers, + standardCheckoutPayRequest, + HttpStatus.SC_OK, + Maps.newHashMap(), + standardCheckoutResponse); + StandardCheckoutPayResponse actual = standardCheckoutClient.pay(standardCheckoutPayRequest); + Assertions.assertEquals(standardCheckoutResponse, actual); + Assertions.assertNotNull(standardCheckoutPayRequest.getPrefillUserLoginDetails()); + Assertions.assertNull(standardCheckoutPayRequest.getPrefillUserLoginDetails().getPhoneNumber()); + } + + @SneakyThrows + @Test + void testStandardCheckoutPayWithoutPrefillUserLoginDetails() { + final String url = StandardCheckoutConstants.PAY_API; + String redirectUrl = "https://redirectUrl.com"; + + StandardCheckoutPayRequest standardCheckoutPayRequest = + StandardCheckoutPayRequest.builder() + .merchantOrderId(merchantOrderId) + .amount(amount) + .redirectUrl(redirectUrl) + .build(); + StandardCheckoutPayResponse standardCheckoutResponse = + StandardCheckoutPayResponse.builder() + .orderId(String.valueOf(java.time.Instant.now().getEpochSecond())) + .state("PENDING") + .expireAt(java.time.Instant.now().getEpochSecond()) + .redirectUrl("https://google.com") + .build(); + Map headers = getHeaders(); + + addStubForPostRequest( + url, + headers, + standardCheckoutPayRequest, + HttpStatus.SC_OK, + Maps.newHashMap(), + standardCheckoutResponse); + StandardCheckoutPayResponse actual = standardCheckoutClient.pay(standardCheckoutPayRequest); + Assertions.assertEquals(standardCheckoutResponse, actual); + Assertions.assertNull(standardCheckoutPayRequest.getPrefillUserLoginDetails()); + } } diff --git a/src/test/customCheckoutTests/CardPayRequestBuilderTest.java b/src/test/customCheckoutTests/CardPayRequestBuilderTest.java new file mode 100644 index 0000000..0a7fa05 --- /dev/null +++ b/src/test/customCheckoutTests/CardPayRequestBuilderTest.java @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2025 Original Author(s), PhonePe India Pvt. Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package customCheckoutTests; + +import com.phonepe.sdk.pg.common.models.PgV2InstrumentType; +import com.phonepe.sdk.pg.common.models.PaymentFlowType; +import com.phonepe.sdk.pg.common.models.request.PgPaymentRequest; +import com.phonepe.sdk.pg.common.models.request.instruments.CardPaymentV2Instrument; +import com.phonepe.sdk.pg.payments.v2.models.request.PgPaymentFlow; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class CardPayRequestBuilderTest { + + private PgPaymentRequest buildFullCardRequest(String orderId) { + return PgPaymentRequest.CardPayRequestBuilder() + .merchantOrderId(orderId) + .amount(2000L) + .encryptionKeyId(99L) + .authMode("H2H") + .encryptedCardNumber("encCard9876") + .encryptedCvv("encCvv111") + .expiryMonth("06") + .expiryYear("2027") + .cardHolderName("Jane Smith") + .merchantUserId("USER_CARD_001") + .redirectUrl("https://merchant.example.com/card/redirect") + .expireAfter(900L) + .build(); + } + + @Test + void testBuildWithAllFields() { + PgPaymentRequest request = buildFullCardRequest("ORDER_CARD_001"); + + Assertions.assertNotNull(request); + Assertions.assertEquals("ORDER_CARD_001", request.getMerchantOrderId()); + Assertions.assertEquals(2000L, request.getAmount()); + } + + @Test + void testPaymentFlowIsSetToPg() { + PgPaymentRequest request = buildFullCardRequest("ORDER_CARD_002"); + + Assertions.assertNotNull(request.getPaymentFlow()); + Assertions.assertTrue(request.getPaymentFlow() instanceof PgPaymentFlow); + Assertions.assertEquals(PaymentFlowType.PG, request.getPaymentFlow().getType()); + } + + @Test + void testPaymentModeIsCard() { + PgPaymentRequest request = buildFullCardRequest("ORDER_CARD_003"); + + PgPaymentFlow flow = (PgPaymentFlow) request.getPaymentFlow(); + Assertions.assertTrue(flow.getPaymentMode() instanceof CardPaymentV2Instrument); + CardPaymentV2Instrument instrument = (CardPaymentV2Instrument) flow.getPaymentMode(); + Assertions.assertEquals(PgV2InstrumentType.CARD, instrument.getType()); + } + + @Test + void testCardDetailsFieldsAreCorrectlySet() { + PgPaymentRequest request = buildFullCardRequest("ORDER_CARD_004"); + + PgPaymentFlow flow = (PgPaymentFlow) request.getPaymentFlow(); + CardPaymentV2Instrument instrument = (CardPaymentV2Instrument) flow.getPaymentMode(); + Assertions.assertNotNull(instrument.getCardDetails()); + Assertions.assertEquals( + "encCard9876", instrument.getCardDetails().getEncryptedCardNumber()); + Assertions.assertEquals("encCvv111", instrument.getCardDetails().getEncryptedCvv()); + Assertions.assertEquals(99L, instrument.getCardDetails().getEncryptionKeyId()); + Assertions.assertEquals("Jane Smith", instrument.getCardDetails().getCardHolderName()); + } + + @Test + void testCardExpiryIsCorrectlySet() { + PgPaymentRequest request = buildFullCardRequest("ORDER_CARD_005"); + + PgPaymentFlow flow = (PgPaymentFlow) request.getPaymentFlow(); + CardPaymentV2Instrument instrument = (CardPaymentV2Instrument) flow.getPaymentMode(); + Assertions.assertNotNull(instrument.getCardDetails().getExpiry()); + Assertions.assertEquals("06", instrument.getCardDetails().getExpiry().getMonth()); + Assertions.assertEquals("2027", instrument.getCardDetails().getExpiry().getYear()); + } + + @Test + void testMerchantUrlsRedirectUrlIsCorrectlySet() { + PgPaymentRequest request = buildFullCardRequest("ORDER_CARD_006"); + + PgPaymentFlow flow = (PgPaymentFlow) request.getPaymentFlow(); + Assertions.assertNotNull(flow.getMerchantUrls()); + Assertions.assertEquals( + "https://merchant.example.com/card/redirect", + flow.getMerchantUrls().getRedirectUrl()); + } + + @Test + void testMerchantUserIdIsCorrectlySet() { + PgPaymentRequest request = buildFullCardRequest("ORDER_CARD_007"); + + PgPaymentFlow flow = (PgPaymentFlow) request.getPaymentFlow(); + CardPaymentV2Instrument instrument = (CardPaymentV2Instrument) flow.getPaymentMode(); + Assertions.assertEquals("USER_CARD_001", instrument.getMerchantUserId()); + } + + @Test + void testAuthModeIsCorrectlySet() { + PgPaymentRequest request = buildFullCardRequest("ORDER_CARD_008"); + + PgPaymentFlow flow = (PgPaymentFlow) request.getPaymentFlow(); + CardPaymentV2Instrument instrument = (CardPaymentV2Instrument) flow.getPaymentMode(); + Assertions.assertEquals("H2H", instrument.getAuthMode()); + } + + @Test + void testExpireAfterIsSetWhenProvided() { + PgPaymentRequest request = buildFullCardRequest("ORDER_CARD_009"); + + Assertions.assertEquals(900L, request.getExpireAfter()); + } +} diff --git a/src/test/customCheckoutTests/CustomCheckoutBaseSetup.java b/src/test/customCheckoutTests/CustomCheckoutBaseSetup.java new file mode 100644 index 0000000..0f091cf --- /dev/null +++ b/src/test/customCheckoutTests/CustomCheckoutBaseSetup.java @@ -0,0 +1,212 @@ +/* + * Copyright (c) 2025 Original Author(s), PhonePe India Pvt. Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package customCheckoutTests; + +import static javax.ws.rs.core.MediaType.APPLICATION_JSON; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.client.MappingBuilder; +import com.github.tomakehurst.wiremock.client.ResponseDefinitionBuilder; +import com.github.tomakehurst.wiremock.client.WireMock; +import com.github.tomakehurst.wiremock.core.WireMockConfiguration; +import com.google.common.collect.ImmutableMap; +import com.phonepe.sdk.pg.Env; +import com.phonepe.sdk.pg.common.constants.Headers; +import com.phonepe.sdk.pg.common.tokenhandler.OAuthResponse; +import com.phonepe.sdk.pg.common.tokenhandler.TokenConstants; +import com.phonepe.sdk.pg.payments.v2.CustomCheckoutClient; +import java.util.Map; +import okhttp3.FormBody; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; + +/** + * Base class for CustomCheckout tests. Sets up a WireMock server, stubs the OAuth token + * endpoint, and instantiates {@link CustomCheckoutClient} ready for use in tests. + */ +abstract class CustomCheckoutBaseSetup { + + protected static final int WIREMOCK_PORT = 30419; + + protected final ObjectMapper mapper = new ObjectMapper(); + + private static final WireMockConfiguration wireMockConfiguration = + new WireMockConfiguration().port(WIREMOCK_PORT); + protected static final WireMockServer wireMockServer = + new WireMockServer(wireMockConfiguration); + + protected CustomCheckoutClient customCheckoutClient; + + protected final String clientId = "CLIENTID"; + protected final String clientSecret = "CLIENTSECRET"; + protected final Integer clientVersion = 1; + protected final Env env = Env.TEST; + protected final String merchantOrderId = "Merchant_CC_001"; + protected final long amount = 100L; + + private final FormBody oauthFormBody = + new FormBody.Builder() + .add("client_id", clientId) + .add("client_secret", clientSecret) + .add("grant_type", "client_credentials") + .add("client_version", String.valueOf(clientVersion)) + .build(); + + @BeforeEach + void setUp() { + wireMockServer.start(); + stubOAuthToken(); + this.customCheckoutClient = + CustomCheckoutClient.getInstance(clientId, clientSecret, clientVersion, env); + } + + @AfterEach + void tearDown() { + Assertions.assertEquals(0, wireMockServer.findAllUnmatchedRequests().size()); + wireMockServer.stop(); + } + + // ── OAuth stub ──────────────────────────────────────────────────────── + + private void stubOAuthToken() { + OAuthResponse oAuthResponse = + OAuthResponse.builder() + .accessToken("accessToken") + .encryptedAccessToken("encryptedAccessToken") + .expiresAt(java.time.Instant.now().getEpochSecond() + 500) + .expiresIn(2432) + .issuedAt(java.time.Instant.now().getEpochSecond()) + .refreshToken("refreshToken") + .tokenType("O-Bearer") + .sessionExpiresAt(234543534) + .build(); + + String requestBody = formDataToString(oauthFormBody); + addStubForPostRequest( + TokenConstants.OAUTH_GET_TOKEN, + getAuthHeaders(), + requestBody, + 200, + ImmutableMap.of(), + oAuthResponse); + } + + // ── Header helpers ──────────────────────────────────────────────────── + + protected Map getHeaders() { + return ImmutableMap.builder() + .put(Headers.CONTENT_TYPE, APPLICATION_JSON) + .put(Headers.SOURCE, Headers.INTEGRATION) + .put(Headers.SOURCE_VERSION, Headers.API_VERSION) + .put(Headers.SOURCE_PLATFORM, Headers.SDK_TYPE) + .put(Headers.SOURCE_PLATFORM_VERSION, Headers.SDK_VERSION) + .put(Headers.OAUTH_AUTHORIZATION, "O-Bearer accessToken") + .build(); + } + + protected Map getAuthHeaders() { + return ImmutableMap.builder() + .put("Content-Type", "application/x-www-form-urlencoded") + .put("accept", "application/json") + .build(); + } + + // ── WireMock stub helpers ───────────────────────────────────────────── + + protected void addStubForGetRequest( + final String urlPath, final int status, final Object response) { + addStubForGetRequest( + urlPath, ImmutableMap.of(), ImmutableMap.of(), status, ImmutableMap.of(), response); + } + + protected void addStubForGetRequest( + final String urlPath, + final Map queryParams, + final Map requestHeaders, + final int status, + final Map responseHeaders, + final Object response) { + final MappingBuilder mappingBuilder = WireMock.get(WireMock.urlPathEqualTo(urlPath)); + requestHeaders.forEach( + (key, value) -> mappingBuilder.withHeader(key, WireMock.containing(value))); + queryParams.forEach( + (key, value) -> mappingBuilder.withQueryParam(key, WireMock.equalTo(value))); + + wireMockServer.stubFor( + mappingBuilder.willReturn(buildResponse(status, responseHeaders, response))); + } + + protected void addStubForPostRequest( + final String urlPath, final Object request, final int status, final Object response) { + addStubForPostRequest( + urlPath, ImmutableMap.of(), request, status, ImmutableMap.of(), response); + } + + protected void addStubForPostRequest( + final String urlPath, + final Map requestHeaders, + final Object request, + final int status, + final Map responseHeaders, + final Object response) { + try { + String body = + request instanceof String + ? (String) request + : mapper.writeValueAsString(request); + + final MappingBuilder mappingBuilder = + WireMock.post(WireMock.urlPathEqualTo(urlPath)) + .withRequestBody(WireMock.equalTo(body)); + + requestHeaders.forEach( + (key, value) -> mappingBuilder.withHeader(key, WireMock.containing(value))); + + wireMockServer.stubFor( + mappingBuilder.willReturn(buildResponse(status, responseHeaders, response))); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private ResponseDefinitionBuilder buildResponse( + int status, Map responseHeaders, Object response) { + try { + ResponseDefinitionBuilder builder = + WireMock.aResponse() + .withStatus(status) + .withBody( + response instanceof String + ? (String) response + : mapper.writeValueAsString(response)); + responseHeaders.forEach(builder::withHeader); + return builder; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private String formDataToString(FormBody formBody) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < formBody.size(); i++) { + if (i > 0) sb.append("&"); + sb.append(formBody.encodedName(i)).append("=").append(formBody.encodedValue(i)); + } + return sb.toString(); + } +} diff --git a/src/test/customCheckoutTests/CustomCheckoutClientTest.java b/src/test/customCheckoutTests/CustomCheckoutClientTest.java new file mode 100644 index 0000000..a2be690 --- /dev/null +++ b/src/test/customCheckoutTests/CustomCheckoutClientTest.java @@ -0,0 +1,454 @@ +/* + * Copyright (c) 2025 Original Author(s), PhonePe India Pvt. Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package customCheckoutTests; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Maps; +import com.phonepe.sdk.pg.common.exception.PhonePeException; +import com.phonepe.sdk.pg.common.http.PhonePeResponse; +import com.phonepe.sdk.pg.common.models.request.PgPaymentRequest; +import com.phonepe.sdk.pg.common.models.request.RefundRequest; +import com.phonepe.sdk.pg.common.models.response.OrderStatusResponse; +import com.phonepe.sdk.pg.common.models.response.PgPaymentResponse; +import com.phonepe.sdk.pg.common.models.response.RefundResponse; +import com.phonepe.sdk.pg.common.models.response.RefundStatusResponse; +import com.phonepe.sdk.pg.payments.v2.customcheckout.CustomCheckoutConstants; +import java.util.Collections; +import java.util.Map; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import wiremock.org.apache.http.HttpStatus; + +class CustomCheckoutClientTest extends CustomCheckoutBaseSetup { + + // ─────────────────────────────────────────────────────────────────────── + // pay() – UPI Collect via VPA + // ─────────────────────────────────────────────────────────────────────── + + @Test + void testPayViaVpaSuccess() { + final String url = CustomCheckoutConstants.PAY_API; + + PgPaymentRequest request = + PgPaymentRequest.UpiCollectPayViaVpaRequestBuilder() + .merchantOrderId("ORDER_CC_VPA_001") + .amount(1000L) + .vpa("customer@upi") + .message("Payment for order") + .build(); + + PgPaymentResponse response = + PgPaymentResponse.builder() + .orderId("OMO001") + .state("PENDING") + .expireAt(java.time.Instant.now().getEpochSecond() + 600) + .build(); + + addStubForPostRequest( + url, getHeaders(), request, HttpStatus.SC_OK, Maps.newHashMap(), response); + + PgPaymentResponse actual = customCheckoutClient.pay(request); + Assertions.assertEquals(response, actual); + } + + @Test + void testPayViaVpaBadRequest() { + final String url = CustomCheckoutConstants.PAY_API; + + PgPaymentRequest request = + PgPaymentRequest.UpiCollectPayViaVpaRequestBuilder() + .merchantOrderId("ORDER_CC_VPA_002") + .amount(500L) + .vpa("invalid-vpa") + .build(); + + PhonePeResponse errorResponse = + PhonePeResponse.>builder() + .code("BAD_REQUEST") + .message("Invalid VPA") + .data(Collections.singletonMap("field", (Object)"vpa")) + .build(); + + addStubForPostRequest( + url, + getHeaders(), + request, + HttpStatus.SC_BAD_REQUEST, + Maps.newHashMap(), + errorResponse); + + PhonePeException exception = + assertThrows(PhonePeException.class, () -> customCheckoutClient.pay(request)); + Assertions.assertEquals(400, exception.getHttpStatusCode()); + Assertions.assertEquals("BAD_REQUEST", exception.getCode()); + } + + // ─────────────────────────────────────────────────────────────────────── + // pay() – UPI Collect via Phone Number + // ─────────────────────────────────────────────────────────────────────── + + @Test + void testPayViaPhoneNumberSuccess() { + final String url = CustomCheckoutConstants.PAY_API; + + PgPaymentRequest request = + PgPaymentRequest.UpiCollectPayViaPhoneNumberRequestBuilder() + .merchantOrderId("ORDER_CC_PHONE_001") + .amount(2000L) + .phoneNumber("9876543210") + .message("Pay for subscription") + .build(); + + PgPaymentResponse response = + PgPaymentResponse.builder() + .orderId("OMO002") + .state("PENDING") + .expireAt(java.time.Instant.now().getEpochSecond() + 600) + .build(); + + addStubForPostRequest( + url, getHeaders(), request, HttpStatus.SC_OK, Maps.newHashMap(), response); + + PgPaymentResponse actual = customCheckoutClient.pay(request); + Assertions.assertEquals(response, actual); + } + + @Test + void testPayViaPhoneNumberBadRequest() { + final String url = CustomCheckoutConstants.PAY_API; + + PgPaymentRequest request = + PgPaymentRequest.UpiCollectPayViaPhoneNumberRequestBuilder() + .merchantOrderId("ORDER_CC_PHONE_002") + .amount(500L) + .phoneNumber("000") + .build(); + + PhonePeResponse errorResponse = + PhonePeResponse.>builder() + .code("INVALID_PHONE_NUMBER") + .message("Phone number is not valid") + .data(Collections.emptyMap()) + .build(); + + addStubForPostRequest( + url, + getHeaders(), + request, + HttpStatus.SC_BAD_REQUEST, + Maps.newHashMap(), + errorResponse); + + PhonePeException exception = + assertThrows(PhonePeException.class, () -> customCheckoutClient.pay(request)); + Assertions.assertEquals(400, exception.getHttpStatusCode()); + Assertions.assertEquals("INVALID_PHONE_NUMBER", exception.getCode()); + } + + @Test + void testPayServerError() { + final String url = CustomCheckoutConstants.PAY_API; + + PgPaymentRequest request = + PgPaymentRequest.UpiCollectPayViaVpaRequestBuilder() + .merchantOrderId("ORDER_CC_VPA_SERVER_ERR") + .amount(1500L) + .vpa("user@upi") + .build(); + + PhonePeResponse errorResponse = + PhonePeResponse.>builder() + .code("INTERNAL_SERVER_ERROR") + .message("Service Unavailable") + .data(Collections.emptyMap()) + .build(); + + addStubForPostRequest( + url, + getHeaders(), + request, + HttpStatus.SC_INTERNAL_SERVER_ERROR, + Maps.newHashMap(), + errorResponse); + + PhonePeException exception = + assertThrows(PhonePeException.class, () -> customCheckoutClient.pay(request)); + Assertions.assertEquals(500, exception.getHttpStatusCode()); + } + + // ─────────────────────────────────────────────────────────────────────── + // getOrderStatus() + // ─────────────────────────────────────────────────────────────────────── + + @Test + void testGetOrderStatusSuccess() { + String url = + String.format(CustomCheckoutConstants.ORDER_STATUS_API, merchantOrderId); + OrderStatusResponse orderStatusResponse = + OrderStatusResponse.builder() + .orderId("OMO003") + .merchantOrderId(merchantOrderId) + .state("COMPLETED") + .amount(1000L) + .build(); + + addStubForGetRequest( + url, + ImmutableMap.of(CustomCheckoutConstants.ORDER_DETAILS, "false"), + getHeaders(), + HttpStatus.SC_OK, + ImmutableMap.of(), + orderStatusResponse); + + OrderStatusResponse actual = customCheckoutClient.getOrderStatus(merchantOrderId); + Assertions.assertEquals(orderStatusResponse, actual); + } + + @Test + void testGetOrderStatusWithDetailsFlagSuccess() { + String url = + String.format(CustomCheckoutConstants.ORDER_STATUS_API, merchantOrderId); + OrderStatusResponse orderStatusResponse = + OrderStatusResponse.builder() + .orderId("OMO004") + .merchantOrderId(merchantOrderId) + .state("PENDING") + .amount(2000L) + .build(); + + addStubForGetRequest( + url, + ImmutableMap.of(CustomCheckoutConstants.ORDER_DETAILS, "true"), + getHeaders(), + HttpStatus.SC_OK, + ImmutableMap.of(), + orderStatusResponse); + + OrderStatusResponse actual = customCheckoutClient.getOrderStatus(merchantOrderId, true); + Assertions.assertEquals(orderStatusResponse, actual); + } + + @Test + void testGetOrderStatusNotFound() { + String unknownOrderId = "UNKNOWN_ORDER"; + String url = String.format(CustomCheckoutConstants.ORDER_STATUS_API, unknownOrderId); + + PhonePeResponse errorResponse = + PhonePeResponse.>builder() + .code("ORDER_NOT_FOUND") + .message("Order not found") + .data(Collections.emptyMap()) + .build(); + + addStubForGetRequest( + url, + ImmutableMap.of(CustomCheckoutConstants.ORDER_DETAILS, "false"), + getHeaders(), + HttpStatus.SC_NOT_FOUND, + ImmutableMap.of(), + errorResponse); + + PhonePeException exception = + assertThrows( + PhonePeException.class, + () -> customCheckoutClient.getOrderStatus(unknownOrderId)); + Assertions.assertEquals(404, exception.getHttpStatusCode()); + Assertions.assertEquals("ORDER_NOT_FOUND", exception.getCode()); + } + + // ─────────────────────────────────────────────────────────────────────── + // getTransactionStatus() + // ─────────────────────────────────────────────────────────────────────── + + @Test + void testGetTransactionStatusSuccess() { + String transactionId = "TXN_CC_001"; + String url = String.format(CustomCheckoutConstants.TRANSACTION_STATUS_API, transactionId); + + OrderStatusResponse transactionStatusResponse = + OrderStatusResponse.builder() + .orderId("OMO005") + .state("COMPLETED") + .amount(3000L) + .build(); + + addStubForGetRequest( + url, + ImmutableMap.of(), + getHeaders(), + HttpStatus.SC_OK, + ImmutableMap.of(), + transactionStatusResponse); + + OrderStatusResponse actual = customCheckoutClient.getTransactionStatus(transactionId); + Assertions.assertEquals(transactionStatusResponse, actual); + } + + @Test + void testGetTransactionStatusNotFound() { + String transactionId = "TXN_INVALID"; + String url = String.format(CustomCheckoutConstants.TRANSACTION_STATUS_API, transactionId); + + PhonePeResponse errorResponse = + PhonePeResponse.>builder() + .code("TRANSACTION_NOT_FOUND") + .message("Transaction does not exist") + .data(Collections.emptyMap()) + .build(); + + addStubForGetRequest( + url, + ImmutableMap.of(), + getHeaders(), + HttpStatus.SC_NOT_FOUND, + ImmutableMap.of(), + errorResponse); + + PhonePeException exception = + assertThrows( + PhonePeException.class, + () -> customCheckoutClient.getTransactionStatus(transactionId)); + Assertions.assertEquals(404, exception.getHttpStatusCode()); + Assertions.assertEquals("TRANSACTION_NOT_FOUND", exception.getCode()); + } + + // ─────────────────────────────────────────────────────────────────────── + // refund() + // ─────────────────────────────────────────────────────────────────────── + + @Test + void testRefundSuccess() { + final String url = CustomCheckoutConstants.REFUND_API; + + RefundRequest refundRequest = + RefundRequest.builder() + .merchantRefundId("REFUND_CC_001") + .originalMerchantOrderId(merchantOrderId) + .amount(500L) + .build(); + + RefundResponse refundResponse = + RefundResponse.builder() + .refundId("REFUND_CC_001") + .state("CREATED") + .amount(500L) + .build(); + + addStubForPostRequest( + url, + getHeaders(), + refundRequest, + HttpStatus.SC_OK, + Maps.newHashMap(), + refundResponse); + + RefundResponse actual = customCheckoutClient.refund(refundRequest); + Assertions.assertEquals(refundResponse, actual); + } + + @Test + void testRefundBadRequest() { + final String url = CustomCheckoutConstants.REFUND_API; + + RefundRequest refundRequest = + RefundRequest.builder() + .merchantRefundId("REFUND_CC_002") + .originalMerchantOrderId("NONEXISTENT_ORDER") + .amount(9999999L) + .build(); + + PhonePeResponse errorResponse = + PhonePeResponse.>builder() + .code("REFUND_AMOUNT_EXCEEDS") + .message("Refund amount exceeds original order amount") + .data(Collections.emptyMap()) + .build(); + + addStubForPostRequest( + url, + getHeaders(), + refundRequest, + HttpStatus.SC_BAD_REQUEST, + Maps.newHashMap(), + errorResponse); + + PhonePeException exception = + assertThrows( + PhonePeException.class, () -> customCheckoutClient.refund(refundRequest)); + Assertions.assertEquals(400, exception.getHttpStatusCode()); + Assertions.assertEquals("REFUND_AMOUNT_EXCEEDS", exception.getCode()); + } + + // ─────────────────────────────────────────────────────────────────────── + // getRefundStatus() + // ─────────────────────────────────────────────────────────────────────── + + @Test + void testGetRefundStatusSuccess() { + String refundId = "REFUND_STATUS_CC_001"; + String url = String.format(CustomCheckoutConstants.REFUND_STATUS_API, refundId); + + RefundStatusResponse refundStatusResponse = + RefundStatusResponse.builder() + .merchantRefundId(refundId) + .originalMerchantOrderId(merchantOrderId) + .amount(500L) + .state("COMPLETED") + .paymentDetails(Collections.emptyList()) + .build(); + + addStubForGetRequest( + url, + ImmutableMap.of(), + getHeaders(), + HttpStatus.SC_OK, + ImmutableMap.of(), + refundStatusResponse); + + RefundStatusResponse actual = customCheckoutClient.getRefundStatus(refundId); + Assertions.assertEquals(refundStatusResponse, actual); + } + + @Test + void testGetRefundStatusNotFound() { + String refundId = "REFUND_UNKNOWN"; + String url = String.format(CustomCheckoutConstants.REFUND_STATUS_API, refundId); + + PhonePeResponse errorResponse = + PhonePeResponse.>builder() + .code("REFUND_NOT_FOUND") + .message("Refund does not exist") + .data(Collections.emptyMap()) + .build(); + + addStubForGetRequest( + url, + ImmutableMap.of(), + getHeaders(), + HttpStatus.SC_NOT_FOUND, + ImmutableMap.of(), + errorResponse); + + PhonePeException exception = + assertThrows( + PhonePeException.class, + () -> customCheckoutClient.getRefundStatus(refundId)); + Assertions.assertEquals(404, exception.getHttpStatusCode()); + Assertions.assertEquals("REFUND_NOT_FOUND", exception.getCode()); + } +} diff --git a/src/test/customCheckoutTests/NetBankingPayRequestBuilderTest.java b/src/test/customCheckoutTests/NetBankingPayRequestBuilderTest.java new file mode 100644 index 0000000..9576f80 --- /dev/null +++ b/src/test/customCheckoutTests/NetBankingPayRequestBuilderTest.java @@ -0,0 +1,155 @@ +/* + * Copyright (c) 2025 Original Author(s), PhonePe India Pvt. Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package customCheckoutTests; + +import com.phonepe.sdk.pg.common.models.PgV2InstrumentType; +import com.phonepe.sdk.pg.common.models.PaymentFlowType; +import com.phonepe.sdk.pg.common.models.request.PgPaymentRequest; +import com.phonepe.sdk.pg.common.models.request.instruments.NetBankingPaymentV2Instrument; +import com.phonepe.sdk.pg.payments.v2.models.request.PgPaymentFlow; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class NetBankingPayRequestBuilderTest { + + @Test + void testBuildWithAllFields() { + PgPaymentRequest request = + PgPaymentRequest.NetBankingPayRequestBuilder() + .merchantOrderId("ORDER_NB_001") + .amount(1000L) + .bankId("HDFC") + .merchantUserId("USER_001") + .redirectUrl("https://merchant.example.com/redirect") + .expireAfter(600L) + .build(); + + Assertions.assertNotNull(request); + Assertions.assertEquals("ORDER_NB_001", request.getMerchantOrderId()); + Assertions.assertEquals(1000L, request.getAmount()); + } + + @Test + void testPaymentFlowIsSetToPg() { + PgPaymentRequest request = + PgPaymentRequest.NetBankingPayRequestBuilder() + .merchantOrderId("ORDER_NB_002") + .amount(200L) + .bankId("ICICI") + .redirectUrl("https://merchant.example.com/redirect") + .build(); + + Assertions.assertNotNull(request.getPaymentFlow()); + Assertions.assertTrue(request.getPaymentFlow() instanceof PgPaymentFlow); + Assertions.assertEquals(PaymentFlowType.PG, request.getPaymentFlow().getType()); + } + + @Test + void testPaymentModeIsNetBanking() { + PgPaymentRequest request = + PgPaymentRequest.NetBankingPayRequestBuilder() + .merchantOrderId("ORDER_NB_003") + .amount(300L) + .bankId("SBI") + .redirectUrl("https://merchant.example.com/redirect") + .build(); + + PgPaymentFlow flow = (PgPaymentFlow) request.getPaymentFlow(); + Assertions.assertTrue(flow.getPaymentMode() instanceof NetBankingPaymentV2Instrument); + NetBankingPaymentV2Instrument instrument = + (NetBankingPaymentV2Instrument) flow.getPaymentMode(); + Assertions.assertEquals(PgV2InstrumentType.NET_BANKING, instrument.getType()); + } + + @Test + void testBankIdIsCorrectlySet() { + String expectedBankId = "AXIS"; + PgPaymentRequest request = + PgPaymentRequest.NetBankingPayRequestBuilder() + .merchantOrderId("ORDER_NB_004") + .amount(400L) + .bankId(expectedBankId) + .redirectUrl("https://merchant.example.com/redirect") + .build(); + + PgPaymentFlow flow = (PgPaymentFlow) request.getPaymentFlow(); + NetBankingPaymentV2Instrument instrument = + (NetBankingPaymentV2Instrument) flow.getPaymentMode(); + Assertions.assertEquals(expectedBankId, instrument.getBankId()); + } + + @Test + void testMerchantUserIdIsCorrectlySet() { + String expectedUserId = "USER_NB_123"; + PgPaymentRequest request = + PgPaymentRequest.NetBankingPayRequestBuilder() + .merchantOrderId("ORDER_NB_005") + .amount(500L) + .bankId("HDFC") + .merchantUserId(expectedUserId) + .redirectUrl("https://merchant.example.com/redirect") + .build(); + + PgPaymentFlow flow = (PgPaymentFlow) request.getPaymentFlow(); + NetBankingPaymentV2Instrument instrument = + (NetBankingPaymentV2Instrument) flow.getPaymentMode(); + Assertions.assertEquals(expectedUserId, instrument.getMerchantUserId()); + } + + @Test + void testMerchantUrlsRedirectUrlIsCorrectlySet() { + String expectedRedirectUrl = "https://merchant.example.com/nb/callback"; + PgPaymentRequest request = + PgPaymentRequest.NetBankingPayRequestBuilder() + .merchantOrderId("ORDER_NB_006") + .amount(600L) + .bankId("KOTAK") + .redirectUrl(expectedRedirectUrl) + .build(); + + PgPaymentFlow flow = (PgPaymentFlow) request.getPaymentFlow(); + Assertions.assertNotNull(flow.getMerchantUrls()); + Assertions.assertEquals(expectedRedirectUrl, flow.getMerchantUrls().getRedirectUrl()); + } + + @Test + void testExpireAfterIsSetWhenProvided() { + long expireAfter = 900L; + PgPaymentRequest request = + PgPaymentRequest.NetBankingPayRequestBuilder() + .merchantOrderId("ORDER_NB_007") + .amount(700L) + .bankId("PNB") + .redirectUrl("https://merchant.example.com/redirect") + .expireAfter(expireAfter) + .build(); + + Assertions.assertEquals(expireAfter, request.getExpireAfter()); + } + + @Test + void testExpireAfterIsNullWhenNotProvided() { + PgPaymentRequest request = + PgPaymentRequest.NetBankingPayRequestBuilder() + .merchantOrderId("ORDER_NB_008") + .amount(800L) + .bankId("BOB") + .redirectUrl("https://merchant.example.com/redirect") + .build(); + + Assertions.assertNull(request.getExpireAfter()); + } +} diff --git a/src/test/customCheckoutTests/TokenPayRequestBuilderTest.java b/src/test/customCheckoutTests/TokenPayRequestBuilderTest.java new file mode 100644 index 0000000..cfb88b5 --- /dev/null +++ b/src/test/customCheckoutTests/TokenPayRequestBuilderTest.java @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2025 Original Author(s), PhonePe India Pvt. Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package customCheckoutTests; + +import com.phonepe.sdk.pg.common.models.PgV2InstrumentType; +import com.phonepe.sdk.pg.common.models.PaymentFlowType; +import com.phonepe.sdk.pg.common.models.request.PgPaymentRequest; +import com.phonepe.sdk.pg.common.models.request.instruments.TokenPaymentV2Instrument; +import com.phonepe.sdk.pg.payments.v2.models.request.PgPaymentFlow; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class TokenPayRequestBuilderTest { + + private PgPaymentRequest buildFullTokenRequest(String orderId) { + return PgPaymentRequest.TokenPayRequestBuilder() + .merchantOrderId(orderId) + .amount(1000L) + .encryptionKeyId(42L) + .authMode("H2H") + .encryptedToken("encTok123") + .encryptedCvv("encCvv456") + .cryptogram("crypto789") + .panSuffix("1234") + .expiryMonth("12") + .expiryYear("2028") + .redirectUrl("https://merchant.example.com/redirect") + .cardHolderName("John Doe") + .merchantUserId("USER_TOKEN_001") + .expireAfter(600L) + .build(); + } + + @Test + void testBuildWithAllFields() { + PgPaymentRequest request = buildFullTokenRequest("ORDER_TOKEN_001"); + + Assertions.assertNotNull(request); + Assertions.assertEquals("ORDER_TOKEN_001", request.getMerchantOrderId()); + Assertions.assertEquals(1000L, request.getAmount()); + } + + @Test + void testPaymentFlowIsSetToPg() { + PgPaymentRequest request = buildFullTokenRequest("ORDER_TOKEN_002"); + + Assertions.assertNotNull(request.getPaymentFlow()); + Assertions.assertTrue(request.getPaymentFlow() instanceof PgPaymentFlow); + Assertions.assertEquals(PaymentFlowType.PG, request.getPaymentFlow().getType()); + } + + @Test + void testPaymentModeIsToken() { + PgPaymentRequest request = buildFullTokenRequest("ORDER_TOKEN_003"); + + PgPaymentFlow flow = (PgPaymentFlow) request.getPaymentFlow(); + Assertions.assertTrue(flow.getPaymentMode() instanceof TokenPaymentV2Instrument); + TokenPaymentV2Instrument instrument = (TokenPaymentV2Instrument) flow.getPaymentMode(); + Assertions.assertEquals(PgV2InstrumentType.TOKEN, instrument.getType()); + } + + @Test + void testTokenDetailsFieldsAreCorrectlySet() { + PgPaymentRequest request = buildFullTokenRequest("ORDER_TOKEN_004"); + + PgPaymentFlow flow = (PgPaymentFlow) request.getPaymentFlow(); + TokenPaymentV2Instrument instrument = (TokenPaymentV2Instrument) flow.getPaymentMode(); + Assertions.assertNotNull(instrument.getTokenDetails()); + Assertions.assertEquals("encTok123", instrument.getTokenDetails().getEncryptedToken()); + Assertions.assertEquals("encCvv456", instrument.getTokenDetails().getEncryptedCvv()); + Assertions.assertEquals(42L, instrument.getTokenDetails().getEncryptionKeyId()); + Assertions.assertEquals("crypto789", instrument.getTokenDetails().getCryptogram()); + Assertions.assertEquals("1234", instrument.getTokenDetails().getPanSuffix()); + Assertions.assertEquals("John Doe", instrument.getTokenDetails().getCardHolderName()); + } + + @Test + void testExpiryMonthAndYearAreCorrectlySet() { + PgPaymentRequest request = buildFullTokenRequest("ORDER_TOKEN_005"); + + PgPaymentFlow flow = (PgPaymentFlow) request.getPaymentFlow(); + TokenPaymentV2Instrument instrument = (TokenPaymentV2Instrument) flow.getPaymentMode(); + Assertions.assertNotNull(instrument.getTokenDetails().getExpiry()); + Assertions.assertEquals("12", instrument.getTokenDetails().getExpiry().getMonth()); + Assertions.assertEquals("2028", instrument.getTokenDetails().getExpiry().getYear()); + } + + @Test + void testMerchantUrlsRedirectUrlIsCorrectlySet() { + PgPaymentRequest request = buildFullTokenRequest("ORDER_TOKEN_006"); + + PgPaymentFlow flow = (PgPaymentFlow) request.getPaymentFlow(); + Assertions.assertNotNull(flow.getMerchantUrls()); + Assertions.assertEquals( + "https://merchant.example.com/redirect", + flow.getMerchantUrls().getRedirectUrl()); + } + + @Test + void testMerchantUserIdIsCorrectlySet() { + PgPaymentRequest request = buildFullTokenRequest("ORDER_TOKEN_007"); + + PgPaymentFlow flow = (PgPaymentFlow) request.getPaymentFlow(); + TokenPaymentV2Instrument instrument = (TokenPaymentV2Instrument) flow.getPaymentMode(); + Assertions.assertEquals("USER_TOKEN_001", instrument.getMerchantUserId()); + } + + @Test + void testAuthModeIsCorrectlySet() { + PgPaymentRequest request = buildFullTokenRequest("ORDER_TOKEN_008"); + + PgPaymentFlow flow = (PgPaymentFlow) request.getPaymentFlow(); + TokenPaymentV2Instrument instrument = (TokenPaymentV2Instrument) flow.getPaymentMode(); + Assertions.assertEquals("H2H", instrument.getAuthMode()); + } + + @Test + void testExpireAfterIsSetWhenProvided() { + PgPaymentRequest request = buildFullTokenRequest("ORDER_TOKEN_009"); + + Assertions.assertEquals(600L, request.getExpireAfter()); + } +} diff --git a/src/test/customCheckoutTests/UpiCollectPayViaPhoneNumberRequestBuilderTest.java b/src/test/customCheckoutTests/UpiCollectPayViaPhoneNumberRequestBuilderTest.java new file mode 100644 index 0000000..461b398 --- /dev/null +++ b/src/test/customCheckoutTests/UpiCollectPayViaPhoneNumberRequestBuilderTest.java @@ -0,0 +1,219 @@ +/* + * Copyright (c) 2025 Original Author(s), PhonePe India Pvt. Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package customCheckoutTests; + +import com.phonepe.sdk.pg.common.models.PgV2InstrumentType; +import com.phonepe.sdk.pg.common.models.PaymentFlowType; +import com.phonepe.sdk.pg.common.models.request.PgPaymentRequest; +import com.phonepe.sdk.pg.common.models.request.instruments.CollectPaymentV2Instrument; +import com.phonepe.sdk.pg.common.models.request.instruments.PhoneNumberCollectPaymentDetails; +import com.phonepe.sdk.pg.payments.v2.models.request.PgPaymentFlow; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class UpiCollectPayViaPhoneNumberRequestBuilderTest { + + @Test + void testBuildWithRequiredFieldsOnly() { + PgPaymentRequest request = + PgPaymentRequest.UpiCollectPayViaPhoneNumberRequestBuilder() + .merchantOrderId("ORDER_PHONE_001") + .amount(100L) + .phoneNumber("9876543210") + .build(); + + Assertions.assertNotNull(request); + Assertions.assertEquals("ORDER_PHONE_001", request.getMerchantOrderId()); + Assertions.assertEquals(100L, request.getAmount()); + Assertions.assertNull(request.getMetaInfo()); + Assertions.assertNull(request.getExpireAfter()); + } + + @Test + void testPaymentFlowIsSetToPg() { + PgPaymentRequest request = + PgPaymentRequest.UpiCollectPayViaPhoneNumberRequestBuilder() + .merchantOrderId("ORDER_PHONE_002") + .amount(200L) + .phoneNumber("9000000001") + .build(); + + Assertions.assertNotNull(request.getPaymentFlow()); + Assertions.assertTrue(request.getPaymentFlow() instanceof PgPaymentFlow); + Assertions.assertEquals(PaymentFlowType.PG, request.getPaymentFlow().getType()); + } + + @Test + void testPaymentModeIsUpiCollect() { + PgPaymentRequest request = + PgPaymentRequest.UpiCollectPayViaPhoneNumberRequestBuilder() + .merchantOrderId("ORDER_PHONE_003") + .amount(300L) + .phoneNumber("9000000002") + .build(); + + PgPaymentFlow flow = (PgPaymentFlow) request.getPaymentFlow(); + Assertions.assertTrue(flow.getPaymentMode() instanceof CollectPaymentV2Instrument); + CollectPaymentV2Instrument instrument = (CollectPaymentV2Instrument) flow.getPaymentMode(); + Assertions.assertEquals(PgV2InstrumentType.UPI_COLLECT, instrument.getType()); + } + + @Test + void testPhoneNumberIsCorrectlySet() { + String expectedPhone = "9123456789"; + PgPaymentRequest request = + PgPaymentRequest.UpiCollectPayViaPhoneNumberRequestBuilder() + .merchantOrderId("ORDER_PHONE_004") + .amount(400L) + .phoneNumber(expectedPhone) + .build(); + + PgPaymentFlow flow = (PgPaymentFlow) request.getPaymentFlow(); + CollectPaymentV2Instrument instrument = (CollectPaymentV2Instrument) flow.getPaymentMode(); + Assertions.assertTrue( + instrument.getDetails() instanceof PhoneNumberCollectPaymentDetails); + PhoneNumberCollectPaymentDetails details = + (PhoneNumberCollectPaymentDetails) instrument.getDetails(); + Assertions.assertEquals(expectedPhone, details.getPhoneNumber()); + } + + @Test + void testMessageIsSetWhenProvided() { + String expectedMessage = "Collect for order #5678"; + PgPaymentRequest request = + PgPaymentRequest.UpiCollectPayViaPhoneNumberRequestBuilder() + .merchantOrderId("ORDER_PHONE_005") + .amount(500L) + .phoneNumber("9111111111") + .message(expectedMessage) + .build(); + + PgPaymentFlow flow = (PgPaymentFlow) request.getPaymentFlow(); + CollectPaymentV2Instrument instrument = (CollectPaymentV2Instrument) flow.getPaymentMode(); + Assertions.assertEquals(expectedMessage, instrument.getMessage()); + } + + @Test + void testMessageIsNullWhenNotProvided() { + PgPaymentRequest request = + PgPaymentRequest.UpiCollectPayViaPhoneNumberRequestBuilder() + .merchantOrderId("ORDER_PHONE_006") + .amount(600L) + .phoneNumber("9222222222") + .build(); + + PgPaymentFlow flow = (PgPaymentFlow) request.getPaymentFlow(); + CollectPaymentV2Instrument instrument = (CollectPaymentV2Instrument) flow.getPaymentMode(); + Assertions.assertNull(instrument.getMessage()); + } + + @Test + void testExpireAfterIsSetWhenProvided() { + long expireAfter = 900L; + PgPaymentRequest request = + PgPaymentRequest.UpiCollectPayViaPhoneNumberRequestBuilder() + .merchantOrderId("ORDER_PHONE_007") + .amount(700L) + .phoneNumber("9333333333") + .expireAfter(expireAfter) + .build(); + + Assertions.assertEquals(expireAfter, request.getExpireAfter()); + } + + @Test + void testNoMerchantUrlsForUpiCollectPhoneNumber() { + PgPaymentRequest request = + PgPaymentRequest.UpiCollectPayViaPhoneNumberRequestBuilder() + .merchantOrderId("ORDER_PHONE_008") + .amount(800L) + .phoneNumber("9444444444") + .build(); + + PgPaymentFlow flow = (PgPaymentFlow) request.getPaymentFlow(); + Assertions.assertNull(flow.getMerchantUrls()); + } + + @Test + void testConstraintsAreNullWhenNotProvided() { + PgPaymentRequest request = + PgPaymentRequest.UpiCollectPayViaPhoneNumberRequestBuilder() + .merchantOrderId("ORDER_PHONE_009") + .amount(900L) + .phoneNumber("9555555555") + .build(); + + Assertions.assertNull(request.getConstraints()); + } + + @Test + void testDetailsTypeIsPhoneNumber() { + PgPaymentRequest request = + PgPaymentRequest.UpiCollectPayViaPhoneNumberRequestBuilder() + .merchantOrderId("ORDER_PHONE_010") + .amount(1000L) + .phoneNumber("9666666666") + .build(); + + PgPaymentFlow flow = (PgPaymentFlow) request.getPaymentFlow(); + CollectPaymentV2Instrument instrument = (CollectPaymentV2Instrument) flow.getPaymentMode(); + PhoneNumberCollectPaymentDetails details = + (PhoneNumberCollectPaymentDetails) instrument.getDetails(); + Assertions.assertEquals( + com.phonepe.sdk.pg.common.models.request.instruments.CollectPaymentDetailsType + .PHONE_NUMBER, + details.getType()); + } + + @Test + void testXDeviceOsIsSetWhenProvided() { + PgPaymentRequest request = + PgPaymentRequest.UpiCollectPayViaPhoneNumberRequestBuilder() + .merchantOrderId("ORDER_PHONE_011") + .amount(1100L) + .phoneNumber("9777777777") + .deviceOS("ANDROID") + .build(); + + Assertions.assertEquals("ANDROID", request.getDeviceOS()); + } + + @Test + void testXDeviceOsIsNullWhenNotProvided() { + PgPaymentRequest request = + PgPaymentRequest.UpiCollectPayViaPhoneNumberRequestBuilder() + .merchantOrderId("ORDER_PHONE_012") + .amount(1200L) + .phoneNumber("9888888888") + .build(); + + Assertions.assertNull(request.getDeviceOS()); + } + + @Test + void testXDeviceOsNotInJsonSerialization() throws Exception { + PgPaymentRequest request = + PgPaymentRequest.UpiCollectPayViaPhoneNumberRequestBuilder() + .merchantOrderId("ORDER_PHONE_013") + .amount(1300L) + .phoneNumber("9999999999") + .deviceOS("IOS") + .build(); + + String json = new com.fasterxml.jackson.databind.ObjectMapper().writeValueAsString(request); + Assertions.assertFalse(json.contains("xDeviceOs")); + } +} diff --git a/src/test/customCheckoutTests/UpiCollectPayViaVpaRequestBuilderTest.java b/src/test/customCheckoutTests/UpiCollectPayViaVpaRequestBuilderTest.java new file mode 100644 index 0000000..535d227 --- /dev/null +++ b/src/test/customCheckoutTests/UpiCollectPayViaVpaRequestBuilderTest.java @@ -0,0 +1,198 @@ +/* + * Copyright (c) 2025 Original Author(s), PhonePe India Pvt. Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package customCheckoutTests; + +import com.phonepe.sdk.pg.common.models.PgV2InstrumentType; +import com.phonepe.sdk.pg.common.models.PaymentFlowType; +import com.phonepe.sdk.pg.common.models.request.PgPaymentRequest; +import com.phonepe.sdk.pg.common.models.request.instruments.CollectPaymentV2Instrument; +import com.phonepe.sdk.pg.common.models.request.instruments.VpaCollectPaymentDetails; +import com.phonepe.sdk.pg.payments.v2.models.request.PgPaymentFlow; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class UpiCollectPayViaVpaRequestBuilderTest { + + @Test + void testBuildWithRequiredFieldsOnly() { + PgPaymentRequest request = + PgPaymentRequest.UpiCollectPayViaVpaRequestBuilder() + .merchantOrderId("ORDER_VPA_001") + .amount(100L) + .vpa("test@upi") + .build(); + + Assertions.assertNotNull(request); + Assertions.assertEquals("ORDER_VPA_001", request.getMerchantOrderId()); + Assertions.assertEquals(100L, request.getAmount()); + Assertions.assertNull(request.getMetaInfo()); + Assertions.assertNull(request.getExpireAfter()); + } + + @Test + void testPaymentFlowIsSetToPg() { + PgPaymentRequest request = + PgPaymentRequest.UpiCollectPayViaVpaRequestBuilder() + .merchantOrderId("ORDER_VPA_002") + .amount(200L) + .vpa("merchant@upi") + .build(); + + Assertions.assertNotNull(request.getPaymentFlow()); + Assertions.assertTrue(request.getPaymentFlow() instanceof PgPaymentFlow); + Assertions.assertEquals(PaymentFlowType.PG, request.getPaymentFlow().getType()); + } + + @Test + void testPaymentModeIsUpiCollect() { + PgPaymentRequest request = + PgPaymentRequest.UpiCollectPayViaVpaRequestBuilder() + .merchantOrderId("ORDER_VPA_003") + .amount(300L) + .vpa("user@paytm") + .build(); + + PgPaymentFlow flow = (PgPaymentFlow) request.getPaymentFlow(); + Assertions.assertTrue(flow.getPaymentMode() instanceof CollectPaymentV2Instrument); + CollectPaymentV2Instrument instrument = (CollectPaymentV2Instrument) flow.getPaymentMode(); + Assertions.assertEquals(PgV2InstrumentType.UPI_COLLECT, instrument.getType()); + } + + @Test + void testVpaIsCorrectlySet() { + String expectedVpa = "john.doe@hdfc"; + PgPaymentRequest request = + PgPaymentRequest.UpiCollectPayViaVpaRequestBuilder() + .merchantOrderId("ORDER_VPA_004") + .amount(400L) + .vpa(expectedVpa) + .build(); + + PgPaymentFlow flow = (PgPaymentFlow) request.getPaymentFlow(); + CollectPaymentV2Instrument instrument = (CollectPaymentV2Instrument) flow.getPaymentMode(); + Assertions.assertTrue(instrument.getDetails() instanceof VpaCollectPaymentDetails); + VpaCollectPaymentDetails details = (VpaCollectPaymentDetails) instrument.getDetails(); + Assertions.assertEquals(expectedVpa, details.getVpa()); + } + + @Test + void testMessageIsSetWhenProvided() { + String expectedMessage = "Payment for invoice #1234"; + PgPaymentRequest request = + PgPaymentRequest.UpiCollectPayViaVpaRequestBuilder() + .merchantOrderId("ORDER_VPA_005") + .amount(500L) + .vpa("customer@sbi") + .message(expectedMessage) + .build(); + + PgPaymentFlow flow = (PgPaymentFlow) request.getPaymentFlow(); + CollectPaymentV2Instrument instrument = (CollectPaymentV2Instrument) flow.getPaymentMode(); + Assertions.assertEquals(expectedMessage, instrument.getMessage()); + } + + @Test + void testMessageIsNullWhenNotProvided() { + PgPaymentRequest request = + PgPaymentRequest.UpiCollectPayViaVpaRequestBuilder() + .merchantOrderId("ORDER_VPA_006") + .amount(600L) + .vpa("user@icici") + .build(); + + PgPaymentFlow flow = (PgPaymentFlow) request.getPaymentFlow(); + CollectPaymentV2Instrument instrument = (CollectPaymentV2Instrument) flow.getPaymentMode(); + Assertions.assertNull(instrument.getMessage()); + } + + @Test + void testExpireAfterIsSetWhenProvided() { + long expireAfter = 600L; + PgPaymentRequest request = + PgPaymentRequest.UpiCollectPayViaVpaRequestBuilder() + .merchantOrderId("ORDER_VPA_007") + .amount(700L) + .vpa("user@axis") + .expireAfter(expireAfter) + .build(); + + Assertions.assertEquals(expireAfter, request.getExpireAfter()); + } + + @Test + void testNoMerchantUrlsForUpiCollectVpa() { + PgPaymentRequest request = + PgPaymentRequest.UpiCollectPayViaVpaRequestBuilder() + .merchantOrderId("ORDER_VPA_008") + .amount(800L) + .vpa("user@ybl") + .build(); + + PgPaymentFlow flow = (PgPaymentFlow) request.getPaymentFlow(); + Assertions.assertNull(flow.getMerchantUrls()); + } + + @Test + void testConstraintsAreNullWhenNotProvided() { + PgPaymentRequest request = + PgPaymentRequest.UpiCollectPayViaVpaRequestBuilder() + .merchantOrderId("ORDER_VPA_009") + .amount(900L) + .vpa("user@oksbi") + .build(); + + Assertions.assertNull(request.getConstraints()); + } + + @Test + void testXDeviceOsIsSetWhenProvided() { + PgPaymentRequest request = + PgPaymentRequest.UpiCollectPayViaVpaRequestBuilder() + .merchantOrderId("ORDER_VPA_010") + .amount(1000L) + .vpa("user@upi") + .deviceOS("ANDROID") + .build(); + + Assertions.assertEquals("ANDROID", request.getDeviceOS()); + } + + @Test + void testXDeviceOsIsNullWhenNotProvided() { + PgPaymentRequest request = + PgPaymentRequest.UpiCollectPayViaVpaRequestBuilder() + .merchantOrderId("ORDER_VPA_011") + .amount(1100L) + .vpa("user@upi") + .build(); + + Assertions.assertNull(request.getDeviceOS()); + } + + @Test + void testXDeviceOsNotInJsonSerialization() throws Exception { + PgPaymentRequest request = + PgPaymentRequest.UpiCollectPayViaVpaRequestBuilder() + .merchantOrderId("ORDER_VPA_012") + .amount(1200L) + .vpa("user@upi") + .deviceOS("IOS") + .build(); + + String json = new com.fasterxml.jackson.databind.ObjectMapper().writeValueAsString(request); + Assertions.assertFalse(json.contains("deviceOs")); + } +} diff --git a/src/test/customCheckoutTests/UpiIntentPayRequestBuilderTest.java b/src/test/customCheckoutTests/UpiIntentPayRequestBuilderTest.java new file mode 100644 index 0000000..f355aee --- /dev/null +++ b/src/test/customCheckoutTests/UpiIntentPayRequestBuilderTest.java @@ -0,0 +1,159 @@ +/* + * Copyright (c) 2025 Original Author(s), PhonePe India Pvt. Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package customCheckoutTests; + +import com.phonepe.sdk.pg.common.models.PgV2InstrumentType; +import com.phonepe.sdk.pg.common.models.PaymentFlowType; +import com.phonepe.sdk.pg.common.models.request.DeviceContext; +import com.phonepe.sdk.pg.common.models.request.PgPaymentRequest; +import com.phonepe.sdk.pg.common.models.request.instruments.IntentPaymentV2Instrument; +import com.phonepe.sdk.pg.payments.v2.models.request.PgPaymentFlow; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class UpiIntentPayRequestBuilderTest { + + @Test + void testBuildWithRequiredFieldsOnly() { + PgPaymentRequest request = + PgPaymentRequest.UpiIntentPayRequestBuilder() + .merchantOrderId("ORDER_INTENT_001") + .amount(100L) + .targetApp("com.phonepe.app") + .build(); + + Assertions.assertNotNull(request); + Assertions.assertEquals("ORDER_INTENT_001", request.getMerchantOrderId()); + Assertions.assertEquals(100L, request.getAmount()); + Assertions.assertNull(request.getMetaInfo()); + Assertions.assertNull(request.getExpireAfter()); + } + + @Test + void testPaymentFlowIsSetToPg() { + PgPaymentRequest request = + PgPaymentRequest.UpiIntentPayRequestBuilder() + .merchantOrderId("ORDER_INTENT_002") + .amount(200L) + .targetApp("com.phonepe.app") + .build(); + + Assertions.assertNotNull(request.getPaymentFlow()); + Assertions.assertTrue(request.getPaymentFlow() instanceof PgPaymentFlow); + Assertions.assertEquals(PaymentFlowType.PG, request.getPaymentFlow().getType()); + } + + @Test + void testPaymentModeIsUpiIntent() { + PgPaymentRequest request = + PgPaymentRequest.UpiIntentPayRequestBuilder() + .merchantOrderId("ORDER_INTENT_003") + .amount(300L) + .targetApp("com.google.android.apps.nbu.paisa.user") + .build(); + + PgPaymentFlow flow = (PgPaymentFlow) request.getPaymentFlow(); + Assertions.assertTrue(flow.getPaymentMode() instanceof IntentPaymentV2Instrument); + IntentPaymentV2Instrument instrument = (IntentPaymentV2Instrument) flow.getPaymentMode(); + Assertions.assertEquals(PgV2InstrumentType.UPI_INTENT, instrument.getType()); + } + + @Test + void testTargetAppIsCorrectlySet() { + String expectedTargetApp = "net.one97.paytm"; + PgPaymentRequest request = + PgPaymentRequest.UpiIntentPayRequestBuilder() + .merchantOrderId("ORDER_INTENT_004") + .amount(400L) + .targetApp(expectedTargetApp) + .build(); + + PgPaymentFlow flow = (PgPaymentFlow) request.getPaymentFlow(); + IntentPaymentV2Instrument instrument = (IntentPaymentV2Instrument) flow.getPaymentMode(); + Assertions.assertEquals(expectedTargetApp, instrument.getTargetApp()); + } + + @Test + void testDeviceContextIsSetWhenDeviceOsAndCallBackSchemeProvided() { + PgPaymentRequest request = + PgPaymentRequest.UpiIntentPayRequestBuilder() + .merchantOrderId("ORDER_INTENT_005") + .amount(500L) + .targetApp("com.phonepe.app") + .deviceOS("ANDROID") + .merchantCallBackScheme("merchantapp://callback") + .build(); + + Assertions.assertNotNull(request.getDeviceContext()); + Assertions.assertEquals("ANDROID", request.getDeviceContext().getDeviceOS()); + Assertions.assertEquals( + "merchantapp://callback", + request.getDeviceContext().getMerchantCallBackScheme()); + } + + @Test + void testDeviceContextDeviceOsIsNullWhenNotProvided() { + PgPaymentRequest request = + PgPaymentRequest.UpiIntentPayRequestBuilder() + .merchantOrderId("ORDER_INTENT_006") + .amount(600L) + .targetApp("com.phonepe.app") + .build(); + + DeviceContext deviceContext = request.getDeviceContext(); + Assertions.assertNotNull(deviceContext); + Assertions.assertNull(deviceContext.getDeviceOS()); + } + + @Test + void testExpireAfterIsSetWhenProvided() { + long expireAfter = 600L; + PgPaymentRequest request = + PgPaymentRequest.UpiIntentPayRequestBuilder() + .merchantOrderId("ORDER_INTENT_007") + .amount(700L) + .targetApp("com.phonepe.app") + .expireAfter(expireAfter) + .build(); + + Assertions.assertEquals(expireAfter, request.getExpireAfter()); + } + + @Test + void testNoMerchantUrlsForUpiIntent() { + PgPaymentRequest request = + PgPaymentRequest.UpiIntentPayRequestBuilder() + .merchantOrderId("ORDER_INTENT_008") + .amount(800L) + .targetApp("com.phonepe.app") + .build(); + + PgPaymentFlow flow = (PgPaymentFlow) request.getPaymentFlow(); + Assertions.assertNull(flow.getMerchantUrls()); + } + + @Test + void testConstraintsAreNullWhenNotProvided() { + PgPaymentRequest request = + PgPaymentRequest.UpiIntentPayRequestBuilder() + .merchantOrderId("ORDER_INTENT_009") + .amount(900L) + .targetApp("com.phonepe.app") + .build(); + + Assertions.assertNull(request.getConstraints()); + } +} diff --git a/src/test/customCheckoutTests/UpiQrRequestBuilderTest.java b/src/test/customCheckoutTests/UpiQrRequestBuilderTest.java new file mode 100644 index 0000000..b7d79e9 --- /dev/null +++ b/src/test/customCheckoutTests/UpiQrRequestBuilderTest.java @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2025 Original Author(s), PhonePe India Pvt. Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package customCheckoutTests; + +import com.phonepe.sdk.pg.common.models.PgV2InstrumentType; +import com.phonepe.sdk.pg.common.models.PaymentFlowType; +import com.phonepe.sdk.pg.common.models.request.PgPaymentRequest; +import com.phonepe.sdk.pg.common.models.request.instruments.UpiQrPaymentV2Instrument; +import com.phonepe.sdk.pg.payments.v2.models.request.PgPaymentFlow; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class UpiQrRequestBuilderTest { + + @Test + void testBuildWithRequiredFieldsOnly() { + PgPaymentRequest request = + PgPaymentRequest.UpiQrRequestBuilder() + .merchantOrderId("ORDER_QR_001") + .amount(100L) + .build(); + + Assertions.assertNotNull(request); + Assertions.assertEquals("ORDER_QR_001", request.getMerchantOrderId()); + Assertions.assertEquals(100L, request.getAmount()); + Assertions.assertNull(request.getMetaInfo()); + Assertions.assertNull(request.getExpireAfter()); + } + + @Test + void testPaymentFlowIsSetToPg() { + PgPaymentRequest request = + PgPaymentRequest.UpiQrRequestBuilder() + .merchantOrderId("ORDER_QR_002") + .amount(200L) + .build(); + + Assertions.assertNotNull(request.getPaymentFlow()); + Assertions.assertTrue(request.getPaymentFlow() instanceof PgPaymentFlow); + Assertions.assertEquals(PaymentFlowType.PG, request.getPaymentFlow().getType()); + } + + @Test + void testPaymentModeIsUpiQr() { + PgPaymentRequest request = + PgPaymentRequest.UpiQrRequestBuilder() + .merchantOrderId("ORDER_QR_003") + .amount(300L) + .build(); + + PgPaymentFlow flow = (PgPaymentFlow) request.getPaymentFlow(); + Assertions.assertTrue(flow.getPaymentMode() instanceof UpiQrPaymentV2Instrument); + UpiQrPaymentV2Instrument instrument = (UpiQrPaymentV2Instrument) flow.getPaymentMode(); + Assertions.assertEquals(PgV2InstrumentType.UPI_QR, instrument.getType()); + } + + @Test + void testNoMerchantUrlsForUpiQr() { + PgPaymentRequest request = + PgPaymentRequest.UpiQrRequestBuilder() + .merchantOrderId("ORDER_QR_004") + .amount(400L) + .build(); + + PgPaymentFlow flow = (PgPaymentFlow) request.getPaymentFlow(); + Assertions.assertNull(flow.getMerchantUrls()); + } + + @Test + void testNoDeviceContextForUpiQr() { + PgPaymentRequest request = + PgPaymentRequest.UpiQrRequestBuilder() + .merchantOrderId("ORDER_QR_005") + .amount(500L) + .build(); + + Assertions.assertNull(request.getDeviceContext()); + } + + @Test + void testExpireAfterIsSetWhenProvided() { + long expireAfter = 300L; + PgPaymentRequest request = + PgPaymentRequest.UpiQrRequestBuilder() + .merchantOrderId("ORDER_QR_006") + .amount(600L) + .expireAfter(expireAfter) + .build(); + + Assertions.assertEquals(expireAfter, request.getExpireAfter()); + } + + @Test + void testConstraintsAreNullWhenNotProvided() { + PgPaymentRequest request = + PgPaymentRequest.UpiQrRequestBuilder() + .merchantOrderId("ORDER_QR_007") + .amount(700L) + .build(); + + Assertions.assertNull(request.getConstraints()); + } +} diff --git a/src/test/standardCheckoutTests/PrefillUserLoginDetailsTest.java b/src/test/standardCheckoutTests/PrefillUserLoginDetailsTest.java new file mode 100644 index 0000000..a51394a --- /dev/null +++ b/src/test/standardCheckoutTests/PrefillUserLoginDetailsTest.java @@ -0,0 +1,238 @@ +/* + * Copyright (c) 2025 Original Author(s), PhonePe India Pvt. Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package standardCheckoutTests; +import static org.junit.jupiter.api.Assertions.*; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.phonepe.sdk.pg.payments.v2.models.request.PrefillUserLoginDetails; +import com.phonepe.sdk.pg.payments.v2.models.request.StandardCheckoutPayRequest; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.DisplayName; + +/** + * Unit tests for PrefillUserLoginDetails feature. + * Tests cover: + * - PrefillUserLoginDetails class functionality + * - Integration with StandardCheckoutPayRequest + * - JSON serialization/deserialization + * - Edge cases and null handling + */ +class PrefillUserLoginDetailsTest { + + private final ObjectMapper objectMapper = new ObjectMapper(); + private final String redirectUrl = "https://merchant.com/callback"; + private final String merchantOrderId = "ORDER_" + System.currentTimeMillis(); + private final long amount = 10000L; // 100 INR + + // ========== PrefillUserLoginDetails Class Tests ========== + + @Test + @DisplayName("Should create PrefillUserLoginDetails with valid phone number using builder") + void testCreatePrefillUserLoginDetailsWithValidPhoneNumber() { + // Arrange & Act + PrefillUserLoginDetails prefill = PrefillUserLoginDetails.builder() + .phoneNumber("9876543210") + .build(); + + // Assert + assertNotNull(prefill); + assertEquals("9876543210", prefill.getPhoneNumber()); + } + + @Test + @DisplayName("Should create PrefillUserLoginDetails with null phone number") + void testCreatePrefillUserLoginDetailsWithNullPhoneNumber() { + // Arrange & Act + PrefillUserLoginDetails prefill = PrefillUserLoginDetails.builder() + .phoneNumber(null) + .build(); + + // Assert + assertNotNull(prefill); + assertNull(prefill.getPhoneNumber()); + } + + // ========== JSON Serialization Tests ========== + + @Test + @DisplayName("Should serialize PrefillUserLoginDetails to JSON with phone number") + void testSerializePrefillUserLoginDetailsWithPhoneNumber() throws Exception { + // Arrange + PrefillUserLoginDetails prefill = PrefillUserLoginDetails.builder() + .phoneNumber("9876543210") + .build(); + + // Act + String json = objectMapper.writeValueAsString(prefill); + + // Assert + assertNotNull(json); + assertTrue(json.contains("phoneNumber")); + assertTrue(json.contains("9876543210")); + assertEquals("{\"phoneNumber\":\"9876543210\"}", json); + } + + // ========== StandardCheckoutPayRequest Integration Tests ========== + + @Test + @DisplayName("Should create StandardCheckoutPayRequest with prefillUserLoginDetails") + void testStandardCheckoutPayRequestWithPrefillUserLoginDetails() { + // Arrange + PrefillUserLoginDetails prefill = PrefillUserLoginDetails.builder() + .phoneNumber("9876543210") + .build(); + + // Act + StandardCheckoutPayRequest request = StandardCheckoutPayRequest.builder() + .merchantOrderId(merchantOrderId) + .amount(amount) + .redirectUrl(redirectUrl) + .prefillUserLoginDetails(prefill) + .build(); + + // Assert + assertNotNull(request); + assertEquals(merchantOrderId, request.getMerchantOrderId()); + assertEquals(amount, request.getAmount()); + assertNotNull(request.getPrefillUserLoginDetails()); + assertEquals("9876543210", request.getPrefillUserLoginDetails().getPhoneNumber()); + } + + @Test + @DisplayName("Should create StandardCheckoutPayRequest without prefillUserLoginDetails") + void testStandardCheckoutPayRequestWithoutPrefillUserLoginDetails() { + // Arrange & Act + StandardCheckoutPayRequest request = StandardCheckoutPayRequest.builder() + .merchantOrderId(merchantOrderId) + .amount(amount) + .redirectUrl(redirectUrl) + .build(); + + // Assert + assertNotNull(request); + assertEquals(merchantOrderId, request.getMerchantOrderId()); + assertEquals(amount, request.getAmount()); + assertNull(request.getPrefillUserLoginDetails()); + } + + @Test + @DisplayName("Should create StandardCheckoutPayRequest with null prefillUserLoginDetails") + void testStandardCheckoutPayRequestWithNullPrefillUserLoginDetails() { + // Arrange & Act + StandardCheckoutPayRequest request = StandardCheckoutPayRequest.builder() + .merchantOrderId(merchantOrderId) + .amount(amount) + .redirectUrl(redirectUrl) + .prefillUserLoginDetails(null) + .build(); + + // Assert + assertNotNull(request); + assertNull(request.getPrefillUserLoginDetails()); + } + + @Test + @DisplayName("Should serialize StandardCheckoutPayRequest with prefillUserLoginDetails to JSON") + void testSerializeStandardCheckoutPayRequestWithPrefillUserLoginDetails() throws Exception { + // Arrange + PrefillUserLoginDetails prefill = PrefillUserLoginDetails.builder() + .phoneNumber("9876543210") + .build(); + + StandardCheckoutPayRequest request = StandardCheckoutPayRequest.builder() + .merchantOrderId(merchantOrderId) + .amount(amount) + .redirectUrl(redirectUrl) + .prefillUserLoginDetails(prefill) + .build(); + + // Act + String json = objectMapper.writeValueAsString(request); + + // Assert + assertNotNull(json); + assertTrue(json.contains("prefillUserLoginDetails")); + assertTrue(json.contains("phoneNumber")); + assertTrue(json.contains("9876543210")); + } + + // ========== Edge Cases and Validation Tests ========== + + @Test + @DisplayName("Should handle empty string phone number") + void testPrefillUserLoginDetailsWithEmptyStringPhoneNumber() { + // Arrange & Act + PrefillUserLoginDetails prefill = PrefillUserLoginDetails.builder() + .phoneNumber("") + .build(); + + // Assert + assertNotNull(prefill); + assertEquals("", prefill.getPhoneNumber()); + } + + // ========== Backward Compatibility Tests ========== + + @Test + @DisplayName("Should maintain backward compatibility - request without prefillUserLoginDetails works") + void testBackwardCompatibilityWithoutPrefillUserLoginDetails() { + // This test ensures existing code continues to work + // Act + StandardCheckoutPayRequest request = StandardCheckoutPayRequest.builder() + .merchantOrderId(merchantOrderId) + .amount(amount) + .redirectUrl(redirectUrl) + .message("Test payment") + .disablePaymentRetry(false) + .build(); + + // Assert + assertNotNull(request); + assertEquals(merchantOrderId, request.getMerchantOrderId()); + assertEquals(amount, request.getAmount()); + assertNull(request.getPrefillUserLoginDetails()); + assertNotNull(request.getPaymentFlow()); + } + + @Test + @DisplayName("Should work with all other StandardCheckoutPayRequest parameters") + void testPrefillUserLoginDetailsWithAllOtherParameters() { + // Arrange + PrefillUserLoginDetails prefill = PrefillUserLoginDetails.builder() + .phoneNumber("9876543210") + .build(); + + // Act + StandardCheckoutPayRequest request = StandardCheckoutPayRequest.builder() + .merchantOrderId(merchantOrderId) + .amount(amount) + .redirectUrl(redirectUrl) + .message("Test payment message") + .expireAfter(3600L) // 1 hour + .disablePaymentRetry(true) + .prefillUserLoginDetails(prefill) + .build(); + + // Assert + assertNotNull(request); + assertEquals(merchantOrderId, request.getMerchantOrderId()); + assertEquals(amount, request.getAmount()); + assertEquals(3600L, request.getExpireAfter()); + assertTrue(request.getDisablePaymentRetry()); + assertNotNull(request.getPrefillUserLoginDetails()); + assertEquals("9876543210", request.getPrefillUserLoginDetails().getPhoneNumber()); + } +}