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.