Skip to content

Commit

Permalink
feat(payment_methods): add support for tokenising bank details and fe…
Browse files Browse the repository at this point in the history
…tching 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>
  • Loading branch information
3 people authored Nov 22, 2023
1 parent 4e15d77 commit 9989489
Show file tree
Hide file tree
Showing 4 changed files with 141 additions and 3 deletions.
10 changes: 10 additions & 0 deletions crates/api_models/src/payment_methods.rs
Original file line number Diff line number Diff line change
Expand Up @@ -811,10 +811,20 @@ pub struct CustomerPaymentMethod {
#[schema(value_type = Option<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
#[schema(example = true)]
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)]
pub struct PaymentMethodId {
pub payment_method_id: String,
Expand Down
114 changes: 111 additions & 3 deletions crates/router/src/core/payment_methods/cards.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ use api_models::{
admin::{self, PaymentMethodsEnabled},
enums::{self as api_enums},
payment_methods::{
CardDetailsPaymentMethod, CardNetworkTypes, PaymentExperienceTypes, PaymentMethodsData,
RequestPaymentMethodTypes, RequiredFieldInfo, ResponsePaymentMethodIntermediate,
ResponsePaymentMethodTypes, ResponsePaymentMethodsEnabled,
BankAccountConnectorDetails, CardDetailsPaymentMethod, CardNetworkTypes, MaskedBankDetails,
PaymentExperienceTypes, PaymentMethodsData, RequestPaymentMethodTypes, RequiredFieldInfo,
ResponsePaymentMethodIntermediate, ResponsePaymentMethodTypes,
ResponsePaymentMethodsEnabled,
},
payments::BankCodeResponse,
surcharge_decision_configs as api_surcharge_decision_configs,
Expand Down Expand Up @@ -2210,13 +2211,41 @@ 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,
PaymentTokenData::temporary_generic(generate_id(consts::ID_LENGTH, "token")),
),
};

// 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
let pma = api::CustomerPaymentMethod {
payment_token: parent_payment_method_token.to_owned(),
Expand All @@ -2232,6 +2261,7 @@ pub async fn list_customer_payment_method(
payment_experience: Some(vec![api_models::enums::PaymentExperience::RedirectToUrl]),
created: Some(pm.created_at),
bank_transfer: pmd,
bank: bank_details,
requires_cvv,
};
customer_pms.push(pma.to_owned());
Expand Down Expand Up @@ -2356,6 +2386,84 @@ pub async fn get_lookup_key_from_locker(
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")]
pub async fn get_lookup_key_for_payout_method(
state: &routes::AppState,
Expand Down
1 change: 1 addition & 0 deletions crates/router/src/openapi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,7 @@ Never share your secret api keys. Keep them guarded and secure.
api_models::payments::PaymentAttemptResponse,
api_models::payments::CaptureResponse,
api_models::payment_methods::RequiredFieldInfo,
api_models::payment_methods::MaskedBankDetails,
api_models::refunds::RefundListRequest,
api_models::refunds::RefundListResponse,
api_models::payments::TimeRange,
Expand Down
19 changes: 19 additions & 0 deletions openapi/openapi_spec.json
Original file line number Diff line number Diff line change
Expand Up @@ -4828,6 +4828,14 @@
],
"nullable": true
},
"bank": {
"allOf": [
{
"$ref": "#/components/schemas/MaskedBankDetails"
}
],
"nullable": true
},
"requires_cvv": {
"type": "boolean",
"description": "Whether this payment method requires CVV to be collected",
Expand Down Expand Up @@ -6434,6 +6442,17 @@
}
]
},
"MaskedBankDetails": {
"type": "object",
"required": [
"mask"
],
"properties": {
"mask": {
"type": "string"
}
}
},
"MbWayRedirection": {
"type": "object",
"required": [
Expand Down

0 comments on commit 9989489

Please sign in to comment.