Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions backend/common_enums/src/enums.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1028,6 +1028,7 @@ pub enum PaymentMethodType {
Twint,
UpiCollect,
UpiIntent,
UpiQr,
Vipps,
VietQr,
Venmo,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,25 @@ pub const API_STATUS_ENDPOINT: &str = "pg/v1/status";

// ===== IRCTC MERCHANT-BASED ENDPOINTS =====
pub const API_IRCTC_PAY_ENDPOINT: &str = "pg/v1/irctc-pay";
pub const API_IRCTC_STATUS_ENDPOINT: &str = "pg/v1/irctc-status";
pub const API_IRCTC_STATUS_ENDPOINT: &str = "pg/v1/irctc-pay/status";
pub const IRCTC_IDENTIFIER: &str = "IRCTC";

// ===== UPI INSTRUMENT TYPES =====
pub const UPI_INTENT: &str = "UPI_INTENT";
pub const UPI_COLLECT: &str = "UPI_COLLECT";
pub const UPI_QR: &str = "UPI_QR";
pub const UPI: &str = "UPI";

// ===== ACCOUNT TYPES =====
pub const ACCOUNT_TYPE_CREDIT: &str = "CREDIT";
pub const ACCOUNT_TYPE_SAVINGS: &str = "SAVINGS";

// ===== CARD NETWORKS =====
pub const CARD_NETWORK_RUPAY: &str = "RUPAY";

// ===== RESPONSE CODES =====
pub const RESPONSE_CODE_CREDIT_ACCOUNT_NOT_ALLOWED: &str = "CREDIT_ACCOUNT_NOT_ALLOWED_FOR_SENDER";
pub const RESPONSE_CODE_PAY0071: &str = "PAY0071";

// ===== DEFAULT VALUES =====
pub const DEFAULT_KEY_INDEX: &str = "1";
Expand Down
154 changes: 139 additions & 15 deletions backend/connector-integration/src/connectors/phonepe/transformers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use domain_types::{
PaymentFlowData, PaymentsAuthorizeData, PaymentsResponseData, PaymentsSyncData, ResponseId,
},
errors,
payment_method_data::{PaymentMethodData, PaymentMethodDataTypes, UpiData},
payment_method_data::{PaymentMethodData, PaymentMethodDataTypes, UpiData, UpiSource},
router_data::ConnectorAuthType,
router_data_v2::RouterDataV2,
router_response_types::RedirectForm,
Expand Down Expand Up @@ -154,6 +154,30 @@ pub struct PhonepeInstrumentResponse {
intent_url: Option<String>,
#[serde(rename = "qrData", skip_serializing_if = "Option::is_none")]
qr_data: Option<String>,
// Fields for UPI CC/CL detection
#[serde(rename = "accountType", skip_serializing_if = "Option::is_none")]
account_type: Option<String>,
#[serde(rename = "cardNetwork", skip_serializing_if = "Option::is_none")]
card_network: Option<String>,
#[serde(rename = "upiCreditLine", skip_serializing_if = "Option::is_none")]
upi_credit_line: Option<bool>,
}

#[derive(Debug, Deserialize, Serialize)]
pub struct PhonepePaymentInstrumentSync {
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
pub instrument_type: Option<String>,
#[serde(rename = "cardNetwork", skip_serializing_if = "Option::is_none")]
pub card_network: Option<String>,
#[serde(rename = "accountType", skip_serializing_if = "Option::is_none")]
pub account_type: Option<String>,
#[serde(rename = "upiCreditLine", skip_serializing_if = "Option::is_none")]
pub upi_credit_line: Option<bool>,
#[serde(
rename = "maskedAccountNumber",
skip_serializing_if = "Option::is_none"
)]
pub masked_account_number: Option<String>,
}

#[derive(Debug, Deserialize, Serialize)]
Expand All @@ -174,13 +198,7 @@ pub struct PhonepeSyncResponseData {
#[serde(rename = "responseCode", skip_serializing_if = "Option::is_none")]
response_code: Option<String>,
#[serde(rename = "paymentInstrument", skip_serializing_if = "Option::is_none")]
payment_instrument: Option<serde_json::Value>,
#[serde(rename = "cardNetwork", skip_serializing_if = "Option::is_none")]
card_network: Option<String>,
#[serde(rename = "accountType", skip_serializing_if = "Option::is_none")]
account_type: Option<String>,
#[serde(rename = "upiCreditLine", skip_serializing_if = "Option::is_none")]
upi_credit_line: Option<bool>,
pub payment_instrument: Option<PhonepePaymentInstrumentSync>,
}

// ===== REQUEST BUILDING =====
Expand Down Expand Up @@ -572,6 +590,9 @@ impl<T: PaymentMethodDataTypes + std::fmt::Debug + Sync + Send + 'static + Seria
incremental_authorization_allowed: None,
status_code: item.http_code,
}),
resource_common_data: PaymentFlowData {
..item.router_data.resource_common_data
},
..item.router_data
})
}
Expand Down Expand Up @@ -719,9 +740,7 @@ impl<T: PaymentMethodDataTypes + std::fmt::Debug + Sync + Send + 'static + Seria
let router_data = &wrapper.router_data;
let auth = PhonepeAuthType::try_from(&router_data.connector_auth_type)?;

let merchant_transaction_id = &router_data
.resource_common_data
.connector_request_reference_id;
let merchant_transaction_id = router_data.resource_common_data.get_reference_id()?;

// Generate checksum for status API - use IRCTC endpoint if merchant is IRCTC
let api_endpoint = if is_irctc_merchant(auth.merchant_id.peek()) {
Expand Down Expand Up @@ -764,9 +783,7 @@ impl<T: PaymentMethodDataTypes + std::fmt::Debug + Sync + Send + 'static + Seria
let router_data = item.router_data;
let auth = PhonepeAuthType::try_from(&router_data.connector_auth_type)?;

let merchant_transaction_id = &router_data
.resource_common_data
.connector_request_reference_id;
let merchant_transaction_id = router_data.resource_common_data.get_reference_id()?;

// Generate checksum for status API - use IRCTC endpoint if merchant is IRCTC
let api_endpoint = if is_irctc_merchant(auth.merchant_id.peek()) {
Expand Down Expand Up @@ -805,6 +822,24 @@ impl TryFrom<ResponseRouterData<PhonepeSyncResponse, Self>>
if let (Some(merchant_transaction_id), Some(transaction_id)) =
(&data.merchant_transaction_id, &data.transaction_id)
{
// Only extract UPI mode and BIN for UPI payment methods
let (upi_mode, bin) = match &item.router_data.request.payment_method_type {
Some(
common_enums::PaymentMethodType::UpiCollect
| common_enums::PaymentMethodType::UpiIntent
| common_enums::PaymentMethodType::UpiQr,
) => {
let upi_mode = extract_upi_mode_from_sync_data(data);
let bin = data.payment_instrument.as_ref().and_then(|payment_inst| {
extract_bin_from_masked_account_number(
payment_inst.masked_account_number.as_deref(),
)
});
(upi_mode, bin)
}
_ => (None, None),
};

// Map PhonePe response codes to payment statuses based on documentation
let status = match response.code.as_str() {
"PAYMENT_SUCCESS" => common_enums::AttemptStatus::Charged,
Expand All @@ -824,14 +859,15 @@ impl TryFrom<ResponseRouterData<PhonepeSyncResponse, Self>>
resource_id: ResponseId::ConnectorTransactionId(transaction_id.clone()),
redirection_data: None,
mandate_reference: None,
connector_metadata: get_wait_screen_metadata(),
connector_metadata: get_sync_metadata(bin),
network_txn_id: None,
connector_response_reference_id: Some(merchant_transaction_id.clone()),
incremental_authorization_allowed: None,
status_code: item.http_code,
}),
resource_common_data: PaymentFlowData {
status,
connector_response: get_connector_response_with_upi_mode(upi_mode),
..item.router_data.resource_common_data
},
..item.router_data
Expand Down Expand Up @@ -935,3 +971,91 @@ pub fn get_wait_screen_metadata() -> Option<serde_json::Value> {
})
.ok()
}

/// Creates metadata for sync response with BIN from masked account number
fn get_sync_metadata(bin: Option<String>) -> Option<serde_json::Value> {
bin.and_then(|b| {
serde_json::to_value(serde_json::json!({
"card_bin_number": b
}))
.map_err(|e| {
tracing::error!("Failed to serialize sync metadata: {}", e);
e
})
.ok()
})
}

fn determine_upi_mode(
payment_instrument: &PhonepePaymentInstrumentSync,
response_code: Option<&String>,
) -> Option<UpiSource> {
match (
payment_instrument.upi_credit_line,
payment_instrument.account_type.as_deref(),
payment_instrument.card_network.as_deref(),
response_code.map(|s| s.as_str()),
) {
(Some(true), _, _, _) => Some(UpiSource::UpiCl),
(_, Some(constants::ACCOUNT_TYPE_CREDIT), Some(constants::CARD_NETWORK_RUPAY), _) => {
Some(UpiSource::UpiCc)
}
(_, Some(constants::ACCOUNT_TYPE_SAVINGS), _, _) => Some(UpiSource::UpiAccount),
(_, _, _, Some(constants::RESPONSE_CODE_CREDIT_ACCOUNT_NOT_ALLOWED)) => {
Some(UpiSource::UpiCc)
}
(_, _, _, Some(constants::RESPONSE_CODE_PAY0071)) => Some(UpiSource::UpiCl),
_ => None,
}
}

/// Extracts UPI mode from sync response payment instrument
fn extract_upi_mode_from_sync_data(sync_data: &PhonepeSyncResponseData) -> Option<UpiSource> {
// Try to determine from payment_instrument
sync_data
.payment_instrument
.as_ref()
.and_then(|payment_instrument| {
determine_upi_mode(payment_instrument, sync_data.response_code.as_ref())
})
// Fallback: determine from response_code alone
.or_else(|| {
sync_data
.response_code
.as_deref()
.and_then(|code| match code {
constants::RESPONSE_CODE_CREDIT_ACCOUNT_NOT_ALLOWED => Some(UpiSource::UpiCc),
constants::RESPONSE_CODE_PAY0071 => Some(UpiSource::UpiCl),
_ => None,
})
})
}

/// Creates ConnectorResponseData with UPI mode
fn get_connector_response_with_upi_mode(
upi_mode: Option<UpiSource>,
) -> Option<domain_types::router_data::ConnectorResponseData> {
upi_mode.map(|mode| {
let upi_mode_string = match mode {
UpiSource::UpiCc => "UPI_CC".to_string(),
UpiSource::UpiCl => "UPI_CL".to_string(),
UpiSource::UpiAccount => "UPI_ACCOUNT".to_string(),
UpiSource::UpiCcCl => "UPI_CC_CL".to_string(),
};
domain_types::router_data::ConnectorResponseData::with_additional_payment_method_data(
domain_types::router_data::AdditionalPaymentMethodConnectorResponse::Upi {
upi_mode: Some(upi_mode_string),
},
)
})
}

/// Extracts the BIN (first 6 numeric characters) from a masked account number.
fn extract_bin_from_masked_account_number(masked_account_number: Option<&str>) -> Option<String> {
masked_account_number.and_then(|account_number| {
account_number
.get(..6)
.filter(|bin| bin.chars().all(|c| c.is_ascii_digit()))
.map(str::to_string)
})
}
Original file line number Diff line number Diff line change
Expand Up @@ -835,6 +835,7 @@ impl TryFrom<common_enums::PaymentMethodType> for StripePaymentMethodType {
| common_enums::PaymentMethodType::Pix
| common_enums::PaymentMethodType::UpiCollect
| common_enums::PaymentMethodType::UpiIntent
| common_enums::PaymentMethodType::UpiQr
| common_enums::PaymentMethodType::Cashapp
| common_enums::PaymentMethodType::Bluecode
| common_enums::PaymentMethodType::Oxxo => Err(ConnectorError::NotImplemented(
Expand Down
4 changes: 4 additions & 0 deletions backend/domain_types/src/router_data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,10 @@ pub enum AdditionalPaymentMethodConnectorResponse {
/// Domestic(Co-Branded) Card network returned by the processor
domestic_network: Option<String>,
},
Upi {
/// UPI mode detected from the connector response (e.g., "UPICC", "UPICL", "UPI_ACCOUNT")
upi_mode: Option<String>,
},
}

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
Expand Down
35 changes: 25 additions & 10 deletions backend/domain_types/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3398,17 +3398,32 @@ impl ForeignTryFrom<ConnectorResponseData> for grpc_api_types::payments::Connect
card_network,
domestic_network,
} => grpc_api_types::payments::AdditionalPaymentMethodConnectorResponse {
card: Some(grpc_api_types::payments::CardConnectorResponse {
authentication_data: authentication_data
.as_ref()
.and_then(|data| serde_json::to_vec(data).ok()),
payment_checks: payment_checks
.as_ref()
.and_then(|checks| serde_json::to_vec(checks).ok()),
card_network: card_network.clone(),
domestic_network: domestic_network.clone(),
}),
payment_method_data: Some(
grpc_api_types::payments::additional_payment_method_connector_response::PaymentMethodData::Card(
grpc_api_types::payments::CardConnectorResponse {
authentication_data: authentication_data
.as_ref()
.and_then(|data| serde_json::to_vec(data).ok()),
payment_checks: payment_checks
.as_ref()
.and_then(|checks| serde_json::to_vec(checks).ok()),
card_network: card_network.clone(),
domestic_network: domestic_network.clone(),
}
)
),
},
AdditionalPaymentMethodConnectorResponse::Upi { upi_mode } => {
grpc_api_types::payments::AdditionalPaymentMethodConnectorResponse {
payment_method_data: Some(
grpc_api_types::payments::additional_payment_method_connector_response::PaymentMethodData::Upi(
grpc_api_types::payments::UpiConnectorResponse {
upi_mode: upi_mode.clone(),
}
)
),
}
}
}
},
),
Expand Down
10 changes: 9 additions & 1 deletion backend/grpc-api-types/proto/payment.proto
Original file line number Diff line number Diff line change
Expand Up @@ -1195,9 +1195,17 @@ message CardConnectorResponse {
optional string domestic_network = 4; // Domestic (co-branded) card network
}

// Additional payment method data for UPI payments
message UpiConnectorResponse {
optional string upi_mode = 1; // UPI mode detected from connector (e.g., "UPICC", "UPICL", "UPI_ACCOUNT")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since this is UPI_SOURCE, use enum here

}

// Additional payment method connector response
message AdditionalPaymentMethodConnectorResponse {
CardConnectorResponse card = 1; // Card-specific response data
oneof payment_method_data {
CardConnectorResponse card = 1; // Card-specific response data
UpiConnectorResponse upi = 2; // UPI-specific response data
}
}

// Connector response data containing various information from the connector
Expand Down