mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-11-02 04:04:43 +08:00
feat(payment_methods): add support for tokenising bank details and fetching masked details while listing (#2585)
Co-authored-by: shashank_attarde <shashank.attarde@juspay.in> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This commit is contained in:
@ -811,10 +811,20 @@ pub struct CustomerPaymentMethod {
|
|||||||
#[schema(value_type = Option<Bank>)]
|
#[schema(value_type = Option<Bank>)]
|
||||||
pub bank_transfer: Option<payouts::Bank>,
|
pub bank_transfer: Option<payouts::Bank>,
|
||||||
|
|
||||||
|
/// Masked bank details from PM auth services
|
||||||
|
#[schema(example = json!({"mask": "0000"}))]
|
||||||
|
pub bank: Option<MaskedBankDetails>,
|
||||||
|
|
||||||
/// Whether this payment method requires CVV to be collected
|
/// Whether this payment method requires CVV to be collected
|
||||||
#[schema(example = true)]
|
#[schema(example = true)]
|
||||||
pub requires_cvv: bool,
|
pub requires_cvv: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)]
|
||||||
|
pub struct MaskedBankDetails {
|
||||||
|
pub mask: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
pub struct PaymentMethodId {
|
pub struct PaymentMethodId {
|
||||||
pub payment_method_id: String,
|
pub payment_method_id: String,
|
||||||
|
|||||||
@ -7,9 +7,10 @@ use api_models::{
|
|||||||
admin::{self, PaymentMethodsEnabled},
|
admin::{self, PaymentMethodsEnabled},
|
||||||
enums::{self as api_enums},
|
enums::{self as api_enums},
|
||||||
payment_methods::{
|
payment_methods::{
|
||||||
CardDetailsPaymentMethod, CardNetworkTypes, PaymentExperienceTypes, PaymentMethodsData,
|
BankAccountConnectorDetails, CardDetailsPaymentMethod, CardNetworkTypes, MaskedBankDetails,
|
||||||
RequestPaymentMethodTypes, RequiredFieldInfo, ResponsePaymentMethodIntermediate,
|
PaymentExperienceTypes, PaymentMethodsData, RequestPaymentMethodTypes, RequiredFieldInfo,
|
||||||
ResponsePaymentMethodTypes, ResponsePaymentMethodsEnabled,
|
ResponsePaymentMethodIntermediate, ResponsePaymentMethodTypes,
|
||||||
|
ResponsePaymentMethodsEnabled,
|
||||||
},
|
},
|
||||||
payments::BankCodeResponse,
|
payments::BankCodeResponse,
|
||||||
surcharge_decision_configs as api_surcharge_decision_configs,
|
surcharge_decision_configs as api_surcharge_decision_configs,
|
||||||
@ -2210,6 +2211,22 @@ pub async fn list_customer_payment_method(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enums::PaymentMethod::BankDebit => {
|
||||||
|
// Retrieve the pm_auth connector details so that it can be tokenized
|
||||||
|
let bank_account_connector_details = get_bank_account_connector_details(&pm, key)
|
||||||
|
.await
|
||||||
|
.unwrap_or_else(|err| {
|
||||||
|
logger::error!(error=?err);
|
||||||
|
None
|
||||||
|
});
|
||||||
|
if let Some(connector_details) = bank_account_connector_details {
|
||||||
|
let token_data = PaymentTokenData::AuthBankDebit(connector_details);
|
||||||
|
(None, None, token_data)
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_ => (
|
_ => (
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
@ -2217,6 +2234,18 @@ pub async fn list_customer_payment_method(
|
|||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Retrieve the masked bank details to be sent as a response
|
||||||
|
let bank_details = if pm.payment_method == enums::PaymentMethod::BankDebit {
|
||||||
|
get_masked_bank_details(&pm, key)
|
||||||
|
.await
|
||||||
|
.unwrap_or_else(|err| {
|
||||||
|
logger::error!(error=?err);
|
||||||
|
None
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
//Need validation for enabled payment method ,querying MCA
|
//Need validation for enabled payment method ,querying MCA
|
||||||
let pma = api::CustomerPaymentMethod {
|
let pma = api::CustomerPaymentMethod {
|
||||||
payment_token: parent_payment_method_token.to_owned(),
|
payment_token: parent_payment_method_token.to_owned(),
|
||||||
@ -2232,6 +2261,7 @@ pub async fn list_customer_payment_method(
|
|||||||
payment_experience: Some(vec![api_models::enums::PaymentExperience::RedirectToUrl]),
|
payment_experience: Some(vec![api_models::enums::PaymentExperience::RedirectToUrl]),
|
||||||
created: Some(pm.created_at),
|
created: Some(pm.created_at),
|
||||||
bank_transfer: pmd,
|
bank_transfer: pmd,
|
||||||
|
bank: bank_details,
|
||||||
requires_cvv,
|
requires_cvv,
|
||||||
};
|
};
|
||||||
customer_pms.push(pma.to_owned());
|
customer_pms.push(pma.to_owned());
|
||||||
@ -2356,6 +2386,84 @@ pub async fn get_lookup_key_from_locker(
|
|||||||
Ok(resp)
|
Ok(resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn get_masked_bank_details(
|
||||||
|
pm: &payment_method::PaymentMethod,
|
||||||
|
key: &[u8],
|
||||||
|
) -> errors::RouterResult<Option<MaskedBankDetails>> {
|
||||||
|
let payment_method_data =
|
||||||
|
decrypt::<serde_json::Value, masking::WithType>(pm.payment_method_data.clone(), key)
|
||||||
|
.await
|
||||||
|
.change_context(errors::StorageError::DecryptionError)
|
||||||
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||||
|
.attach_printable("unable to decrypt bank details")?
|
||||||
|
.map(|x| x.into_inner().expose())
|
||||||
|
.map(
|
||||||
|
|v| -> Result<PaymentMethodsData, error_stack::Report<errors::ApiErrorResponse>> {
|
||||||
|
v.parse_value::<PaymentMethodsData>("PaymentMethodsData")
|
||||||
|
.change_context(errors::StorageError::DeserializationFailed)
|
||||||
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||||
|
.attach_printable("Failed to deserialize Payment Method Auth config")
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.transpose()?;
|
||||||
|
|
||||||
|
match payment_method_data {
|
||||||
|
Some(pmd) => match pmd {
|
||||||
|
PaymentMethodsData::Card(_) => Ok(None),
|
||||||
|
PaymentMethodsData::BankDetails(bank_details) => Ok(Some(MaskedBankDetails {
|
||||||
|
mask: bank_details.mask,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
None => Err(errors::ApiErrorResponse::InternalServerError.into())
|
||||||
|
.attach_printable("Unable to fetch payment method data"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_bank_account_connector_details(
|
||||||
|
pm: &payment_method::PaymentMethod,
|
||||||
|
key: &[u8],
|
||||||
|
) -> errors::RouterResult<Option<BankAccountConnectorDetails>> {
|
||||||
|
let payment_method_data =
|
||||||
|
decrypt::<serde_json::Value, masking::WithType>(pm.payment_method_data.clone(), key)
|
||||||
|
.await
|
||||||
|
.change_context(errors::StorageError::DecryptionError)
|
||||||
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||||
|
.attach_printable("unable to decrypt bank details")?
|
||||||
|
.map(|x| x.into_inner().expose())
|
||||||
|
.map(
|
||||||
|
|v| -> Result<PaymentMethodsData, error_stack::Report<errors::ApiErrorResponse>> {
|
||||||
|
v.parse_value::<PaymentMethodsData>("PaymentMethodsData")
|
||||||
|
.change_context(errors::StorageError::DeserializationFailed)
|
||||||
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||||
|
.attach_printable("Failed to deserialize Payment Method Auth config")
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.transpose()?;
|
||||||
|
|
||||||
|
match payment_method_data {
|
||||||
|
Some(pmd) => match pmd {
|
||||||
|
PaymentMethodsData::Card(_) => Err(errors::ApiErrorResponse::UnprocessableEntity {
|
||||||
|
message: "Card is not a valid entity".to_string(),
|
||||||
|
})
|
||||||
|
.into_report(),
|
||||||
|
PaymentMethodsData::BankDetails(bank_details) => {
|
||||||
|
let connector_details = bank_details
|
||||||
|
.connector_details
|
||||||
|
.first()
|
||||||
|
.ok_or(errors::ApiErrorResponse::InternalServerError)?;
|
||||||
|
Ok(Some(BankAccountConnectorDetails {
|
||||||
|
connector: connector_details.connector.clone(),
|
||||||
|
account_id: connector_details.account_id.clone(),
|
||||||
|
mca_id: connector_details.mca_id.clone(),
|
||||||
|
access_token: connector_details.access_token.clone(),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => Err(errors::ApiErrorResponse::InternalServerError.into())
|
||||||
|
.attach_printable("Unable to fetch payment method data"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "payouts")]
|
#[cfg(feature = "payouts")]
|
||||||
pub async fn get_lookup_key_for_payout_method(
|
pub async fn get_lookup_key_for_payout_method(
|
||||||
state: &routes::AppState,
|
state: &routes::AppState,
|
||||||
|
|||||||
@ -315,6 +315,7 @@ Never share your secret api keys. Keep them guarded and secure.
|
|||||||
api_models::payments::PaymentAttemptResponse,
|
api_models::payments::PaymentAttemptResponse,
|
||||||
api_models::payments::CaptureResponse,
|
api_models::payments::CaptureResponse,
|
||||||
api_models::payment_methods::RequiredFieldInfo,
|
api_models::payment_methods::RequiredFieldInfo,
|
||||||
|
api_models::payment_methods::MaskedBankDetails,
|
||||||
api_models::refunds::RefundListRequest,
|
api_models::refunds::RefundListRequest,
|
||||||
api_models::refunds::RefundListResponse,
|
api_models::refunds::RefundListResponse,
|
||||||
api_models::payments::TimeRange,
|
api_models::payments::TimeRange,
|
||||||
|
|||||||
@ -4828,6 +4828,14 @@
|
|||||||
],
|
],
|
||||||
"nullable": true
|
"nullable": true
|
||||||
},
|
},
|
||||||
|
"bank": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/MaskedBankDetails"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
"requires_cvv": {
|
"requires_cvv": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"description": "Whether this payment method requires CVV to be collected",
|
"description": "Whether this payment method requires CVV to be collected",
|
||||||
@ -6434,6 +6442,17 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"MaskedBankDetails": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"mask"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"mask": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"MbWayRedirection": {
|
"MbWayRedirection": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
|
|||||||
Reference in New Issue
Block a user