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:
Chethan Rao
2023-11-22 17:11:02 +05:30
committed by GitHub
parent 4e15d7792e
commit 998948953a
4 changed files with 141 additions and 3 deletions

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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": [