Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(payment_methods): add support for tokenising bank details and fetching masked details while listing #2585

Merged
merged 26 commits into from
Nov 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
005074d
fix(router): associate parent payment token with payment method id as…
vspecky Sep 12, 2023
2518651
Merge branch 'main' into locker-id-as-hyps-token-for-cards
vspecky Sep 13, 2023
a7fe34f
fix(router): set the payment method in payment attempt when retrievin…
vspecky Sep 14, 2023
3a94814
Merge branch 'main' into locker-id-as-hyps-token-for-cards
vspecky Sep 20, 2023
0b5d8de
refactor(router): address review comments
vspecky Sep 20, 2023
3646fca
chore: merge with 'main' and resolve conflicts/errors
vspecky Oct 10, 2023
c67e69a
chore: address clippy lints
vspecky Oct 10, 2023
cd69029
chore: address clippy lints
vspecky Oct 10, 2023
eedb829
Merge branch 'main' into locker-id-as-hyps-token-for-cards
vspecky Oct 10, 2023
b8004e3
fix(router): fix serde deserialization type name
vspecky Oct 12, 2023
71e88c0
feat(payment_methods): add support for tokenizing bank details and fe…
Chethan-rao Oct 14, 2023
caba4de
Merge branch 'main' into locker-id-as-hyps-token-for-cards
Chethan-rao Oct 14, 2023
27f700e
Merge branch 'locker-id-as-hyps-token-for-cards' of github.com:juspay…
Chethan-rao Oct 14, 2023
644b0df
refactor: sync up with 2130 pr
Chethan-rao Oct 14, 2023
3065bb7
regenerate openapi spec
Chethan-rao Oct 14, 2023
55fa6ed
refactor(router): pass down the key store to make_pm_data
vspecky Oct 16, 2023
c0ccd6d
Merge branch 'main' into locker-id-as-hyps-token-for-cards
vspecky Oct 16, 2023
0481f5e
fix(router): dont make card_cvc mandatory in saved cards flow
vspecky Oct 16, 2023
b2acbc6
Merge branch 'locker-id-as-hyps-token-for-cards' of github.com:juspay…
Chethan-rao Oct 16, 2023
eb7cf88
fix(router): fix type error
vspecky Oct 17, 2023
d222453
refactor(router): make payment token data enum internally tagged and …
vspecky Oct 17, 2023
58aa903
Merge branch 'locker-id-as-hyps-token-for-cards' of github.com:juspay…
Chethan-rao Oct 18, 2023
1d7f6aa
refactor: sync up with locker-id-as-hyps-token-for-cards
Chethan-rao Oct 18, 2023
9a06653
Merge branch 'main' of github.com:juspay/hyperswitch into support_for…
Chethan-rao Nov 22, 2023
40ad6a0
add comments
Chethan-rao Nov 22, 2023
fe2de07
chore: run formatter
github-actions[bot] Nov 22, 2023
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
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
Loading