feat(router): add an api to migrate the payment method (#5186)

Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
Shankar Singh C
2024-07-10 22:01:13 +05:30
committed by GitHub
parent f7abcee625
commit 125699f89c
16 changed files with 696 additions and 31 deletions

View File

@ -130,6 +130,9 @@ pub async fn call_to_locker(
card_network: card.card_brand,
client_secret: None,
payment_method_data: None,
billing: None,
connector_mandate_details: None,
network_transaction_id: None,
};
let add_card_result = cards::add_card_hs(

View File

@ -95,6 +95,7 @@ pub async fn create_payment_method(
network_transaction_id: Option<String>,
storage_scheme: MerchantStorageScheme,
payment_method_billing_address: Option<Encryption>,
card_scheme: Option<String>,
) -> errors::CustomResult<storage::PaymentMethod, errors::ApiErrorResponse> {
let customer = db
.find_customer_by_customer_id_merchant_id(
@ -123,7 +124,7 @@ pub async fn create_payment_method(
payment_method: req.payment_method,
payment_method_type: req.payment_method_type,
payment_method_issuer: req.payment_method_issuer.clone(),
scheme: req.card_network.clone(),
scheme: req.card_network.clone().or(card_scheme),
metadata: pm_metadata.map(Secret::new),
payment_method_data,
connector_mandate_details,
@ -244,7 +245,7 @@ pub async fn get_or_insert_payment_method(
insert_payment_method(
db,
resp,
req,
&req,
key_store,
&merchant_account.merchant_id,
customer_id,
@ -252,7 +253,7 @@ pub async fn get_or_insert_payment_method(
None,
locker_id,
None,
None,
req.network_transaction_id.clone(),
merchant_account.storage_scheme,
None,
)
@ -266,6 +267,314 @@ pub async fn get_or_insert_payment_method(
}
}
pub async fn migrate_payment_method(
state: routes::SessionState,
req: api::PaymentMethodMigrate,
) -> errors::RouterResponse<api::PaymentMethodResponse> {
let merchant_id = &req.merchant_id;
let key_store = state
.store
.get_merchant_key_store_by_merchant_id(
merchant_id,
&state.store.get_master_key().to_vec().into(),
)
.await
.to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)?;
let merchant_account = state
.store
.find_merchant_account_by_merchant_id(merchant_id, &key_store)
.await
.to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)?;
let card_details = req.card.as_ref().get_required_value("card")?;
let card_number_validation_result =
cards::CardNumber::from_str(card_details.card_number.peek());
if let Some(connector_mandate_details) = &req.connector_mandate_details {
helpers::validate_merchant_connector_ids_in_connector_mandate_details(
&*state.store,
&key_store,
connector_mandate_details,
merchant_id,
)
.await?;
};
match card_number_validation_result {
Ok(card_number) => {
let payment_method_create_request =
api::PaymentMethodCreate::get_payment_method_create_from_payment_method_migrate(
card_number,
&req,
);
get_client_secret_or_add_payment_method(
state,
payment_method_create_request,
&merchant_account,
&key_store,
)
.await
}
Err(card_validation_error) => {
logger::debug!("Card number to be migrated is invalid, skip saving in locker {card_validation_error}");
skip_locker_call_and_migrate_payment_method(
state,
&req,
merchant_id.into(),
&key_store,
&merchant_account,
)
.await
}
}
}
pub async fn skip_locker_call_and_migrate_payment_method(
state: routes::SessionState,
req: &api::PaymentMethodMigrate,
merchant_id: String,
key_store: &domain::MerchantKeyStore,
merchant_account: &domain::MerchantAccount,
) -> errors::RouterResponse<api::PaymentMethodResponse> {
let db = &*state.store;
let customer_id = req.customer_id.clone().get_required_value("customer_id")?;
// In this case, since we do not have valid card details, recurring payments can only be done through connector mandate details.
let connector_mandate_details_req = req
.connector_mandate_details
.clone()
.get_required_value("connector mandate details")?;
let connector_mandate_details = serde_json::to_value(&connector_mandate_details_req)
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to parse connector mandate details")?;
let payment_method_billing_address = create_encrypted_data(key_store, req.billing.clone())
.await
.map(|details| details.into());
let customer = db
.find_customer_by_customer_id_merchant_id(
&customer_id,
&merchant_id,
key_store,
merchant_account.storage_scheme,
)
.await
.to_not_found_response(errors::ApiErrorResponse::CustomerNotFound)?;
let card = if let Some(card_details) = &req.card {
helpers::validate_card_expiry(&card_details.card_exp_month, &card_details.card_exp_year)?;
let card_number = card_details.card_number.clone();
let (card_isin, last4_digits) = get_card_bin_and_last4_digits_for_masked_card(
card_number.peek(),
)
.change_context(errors::ApiErrorResponse::InvalidRequestData {
message: "Invalid card number".to_string(),
})?;
if card_details.card_issuer.is_some()
&& card_details.card_network.is_some()
&& card_details.card_type.is_some()
&& card_details.card_issuing_country.is_some()
{
Some(api::CardDetailFromLocker {
scheme: card_details
.card_network
.clone()
.or(card_details.card_network.clone())
.map(|card_network| card_network.to_string()),
last4_digits: Some(last4_digits.clone()),
issuer_country: card_details
.card_issuing_country
.clone()
.or(card_details.card_issuing_country.clone()),
card_number: None,
expiry_month: Some(card_details.card_exp_month.clone()),
expiry_year: Some(card_details.card_exp_year.clone()),
card_token: None,
card_fingerprint: None,
card_holder_name: card_details.card_holder_name.clone(),
nick_name: card_details.nick_name.clone(),
card_isin: Some(card_isin.clone()),
card_issuer: card_details
.card_issuer
.clone()
.or(card_details.card_issuer.clone()),
card_network: card_details
.card_network
.clone()
.or(card_details.card_network.clone()),
card_type: card_details
.card_type
.clone()
.or(card_details.card_type.clone()),
saved_to_locker: false,
})
} else {
Some(
db.get_card_info(&card_isin)
.await
.map_err(|error| services::logger::error!(card_info_error=?error))
.ok()
.flatten()
.map(|card_info| {
logger::debug!("Fetching bin details");
api::CardDetailFromLocker {
scheme: card_details
.card_network
.clone()
.or(card_info.card_network.clone())
.map(|card_network| card_network.to_string()),
last4_digits: Some(last4_digits.clone()),
issuer_country: card_details
.card_issuing_country
.clone()
.or(card_info.card_issuing_country),
card_number: None,
expiry_month: Some(card_details.card_exp_month.clone()),
expiry_year: Some(card_details.card_exp_year.clone()),
card_token: None,
card_fingerprint: None,
card_holder_name: card_details.card_holder_name.clone(),
nick_name: card_details.nick_name.clone(),
card_isin: Some(card_isin.clone()),
card_issuer: card_details.card_issuer.clone().or(card_info.card_issuer),
card_network: card_details
.card_network
.clone()
.or(card_info.card_network),
card_type: card_details.card_type.clone().or(card_info.card_type),
saved_to_locker: false,
}
})
.unwrap_or_else(|| {
logger::debug!("Failed to fetch bin details");
api::CardDetailFromLocker {
scheme: card_details
.card_network
.clone()
.map(|card_network| card_network.to_string()),
last4_digits: Some(last4_digits.clone()),
issuer_country: card_details.card_issuing_country.clone(),
card_number: None,
expiry_month: Some(card_details.card_exp_month.clone()),
expiry_year: Some(card_details.card_exp_year.clone()),
card_token: None,
card_fingerprint: None,
card_holder_name: card_details.card_holder_name.clone(),
nick_name: card_details.nick_name.clone(),
card_isin: Some(card_isin.clone()),
card_issuer: card_details.card_issuer.clone(),
card_network: card_details.card_network.clone(),
card_type: card_details.card_type.clone(),
saved_to_locker: false,
}
}),
)
}
} else {
None
};
let payment_method_card_details = card
.as_ref()
.map(|card| PaymentMethodsData::Card(CardDetailsPaymentMethod::from(card.clone())));
let payment_method_data_encrypted =
create_encrypted_data(key_store, payment_method_card_details)
.await
.map(|details| details.into());
let payment_method_metadata: Option<serde_json::Value> =
req.metadata.as_ref().map(|data| data.peek()).cloned();
let payment_method_id = generate_id(consts::ID_LENGTH, "pm");
let current_time = common_utils::date_time::now();
let response = db
.insert_payment_method(
storage::PaymentMethodNew {
customer_id: customer_id.to_owned(),
merchant_id: merchant_id.to_string(),
payment_method_id: payment_method_id.to_string(),
locker_id: None,
payment_method: req.payment_method,
payment_method_type: req.payment_method_type,
payment_method_issuer: req.payment_method_issuer.clone(),
scheme: req.card_network.clone(),
metadata: payment_method_metadata.map(Secret::new),
payment_method_data: payment_method_data_encrypted,
connector_mandate_details: Some(connector_mandate_details),
customer_acceptance: None,
client_secret: None,
status: enums::PaymentMethodStatus::Active,
network_transaction_id: None,
payment_method_issuer_code: None,
accepted_currency: None,
token: None,
cardholder_name: None,
issuer_name: None,
issuer_country: None,
payer_country: None,
is_stored: None,
swift_code: None,
direct_debit_token: None,
created_at: current_time,
last_modified: current_time,
last_used_at: current_time,
payment_method_billing_address,
updated_by: None,
},
merchant_account.storage_scheme,
)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to add payment method in db")?;
logger::debug!("Payment method inserted in db");
if customer.default_payment_method_id.is_none() && req.payment_method.is_some() {
let _ = set_default_payment_method(
&*state.store,
merchant_id.to_string(),
key_store.clone(),
&customer_id,
payment_method_id.to_owned(),
merchant_account.storage_scheme,
)
.await
.map_err(|err| logger::error!(error=?err,"Failed to set the payment method as default"));
}
Ok(services::api::ApplicationResponse::Json(
api::PaymentMethodResponse::foreign_from((card, response)),
))
}
pub fn get_card_bin_and_last4_digits_for_masked_card(
masked_card_number: &str,
) -> Result<(String, String), cards::CardNumberValidationErr> {
let last4_digits = masked_card_number
.chars()
.rev()
.take(4)
.collect::<String>()
.chars()
.rev()
.collect::<String>();
let card_isin = masked_card_number.chars().take(6).collect::<String>();
cards::validate::validate_card_number_chars(&card_isin)
.and_then(|_| cards::validate::validate_card_number_chars(&last4_digits))?;
Ok((card_isin, last4_digits))
}
#[instrument(skip_all)]
pub async fn get_client_secret_or_add_payment_method(
state: routes::SessionState,
@ -282,6 +591,17 @@ pub async fn get_client_secret_or_add_payment_method(
#[cfg(feature = "payouts")]
let condition = req.card.is_some() || req.bank_transfer.is_some() || req.wallet.is_some();
let payment_method_billing_address = create_encrypted_data(key_store, req.billing.clone())
.await
.map(|details| details.into());
let connector_mandate_details = req
.connector_mandate_details
.clone()
.map(serde_json::to_value)
.transpose()
.change_context(errors::ApiErrorResponse::InternalServerError)?;
if condition {
Box::pin(add_payment_method(state, req, merchant_account, key_store)).await
} else {
@ -298,10 +618,11 @@ pub async fn get_client_secret_or_add_payment_method(
None,
None,
key_store,
None,
connector_mandate_details,
Some(enums::PaymentMethodStatus::AwaitingData),
None,
merchant_account.storage_scheme,
payment_method_billing_address,
None,
)
.await?;
@ -322,7 +643,7 @@ pub async fn get_client_secret_or_add_payment_method(
}
Ok(services::api::ApplicationResponse::Json(
api::PaymentMethodResponse::foreign_from(res),
api::PaymentMethodResponse::foreign_from((None, res)),
))
}
}
@ -544,6 +865,16 @@ pub async fn add_payment_method(
let merchant_id = &merchant_account.merchant_id;
let customer_id = req.customer_id.clone().get_required_value("customer_id")?;
let payment_method = req.payment_method.get_required_value("payment_method")?;
let payment_method_billing_address = create_encrypted_data(key_store, req.billing.clone())
.await
.map(|details| details.into());
let connector_mandate_details = req
.connector_mandate_details
.clone()
.map(serde_json::to_value)
.transpose()
.change_context(errors::ApiErrorResponse::InternalServerError)?;
let response = match payment_method {
#[cfg(feature = "payouts")]
@ -567,11 +898,20 @@ pub async fn add_payment_method(
},
api_enums::PaymentMethod::Card => match req.card.clone() {
Some(card) => {
helpers::validate_card_expiry(&card.card_exp_month, &card.card_exp_year)?;
let mut card_details = card;
card_details = helpers::populate_bin_details_for_payment_method_create(
card_details.clone(),
db,
)
.await;
helpers::validate_card_expiry(
&card_details.card_exp_month,
&card_details.card_exp_year,
)?;
Box::pin(add_card_to_locker(
&state,
req.clone(),
&card,
&card_details,
&customer_id,
merchant_account,
None,
@ -731,17 +1071,17 @@ pub async fn add_payment_method(
let pm = insert_payment_method(
db,
&resp,
req,
&req,
key_store,
merchant_id,
&customer_id,
pm_metadata.cloned(),
None,
locker_id,
None,
None,
connector_mandate_details,
req.network_transaction_id.clone(),
merchant_account.storage_scheme,
None,
payment_method_billing_address,
)
.await?;
@ -756,7 +1096,7 @@ pub async fn add_payment_method(
pub async fn insert_payment_method(
db: &dyn db::StorageInterface,
resp: &api::PaymentMethodResponse,
req: api::PaymentMethodCreate,
req: &api::PaymentMethodCreate,
key_store: &domain::MerchantKeyStore,
merchant_id: &str,
customer_id: &id_type::CustomerId,
@ -770,7 +1110,7 @@ pub async fn insert_payment_method(
) -> errors::RouterResult<diesel_models::PaymentMethod> {
let pm_card_details = resp
.card
.as_ref()
.clone()
.map(|card| PaymentMethodsData::Card(CardDetailsPaymentMethod::from(card.clone())));
let pm_data_encrypted = create_encrypted_data(key_store, pm_card_details)
.await
@ -778,7 +1118,7 @@ pub async fn insert_payment_method(
create_payment_method(
db,
&req,
req,
customer_id,
&resp.payment_method_id,
locker_id,
@ -792,6 +1132,10 @@ pub async fn insert_payment_method(
network_transaction_id,
storage_scheme,
payment_method_billing_address,
resp.card.clone().and_then(|card| {
card.card_network
.map(|card_network| card_network.to_string())
}),
)
.await
}
@ -904,6 +1248,9 @@ pub async fn update_customer_payment_method(
client_secret: pm.client_secret.clone(),
payment_method_data: None,
card_network: None,
billing: None,
connector_mandate_details: None,
network_transaction_id: None,
};
new_pm.validate()?;
@ -1135,7 +1482,7 @@ pub async fn add_card_to_locker(
errors::VaultError,
> {
metrics::STORED_TO_LOCKER.add(&metrics::CONTEXT, 1, &[]);
let add_card_to_hs_resp = common_utils::metrics::utils::record_operation_time(
let add_card_to_hs_resp = Box::pin(common_utils::metrics::utils::record_operation_time(
async {
add_card_hs(
state,
@ -1162,7 +1509,7 @@ pub async fn add_card_to_locker(
&metrics::CARD_ADD_TIME,
&metrics::CONTEXT,
&[router_env::opentelemetry::KeyValue::new("locker", "rust")],
)
))
.await?;
logger::debug!("card added to hyperswitch-card-vault");
@ -3677,12 +4024,8 @@ pub async fn get_card_details_with_locker_fallback(
});
Ok(if let Some(mut crd) = card_decrypted {
if crd.saved_to_locker {
crd.scheme.clone_from(&pm.scheme);
Some(crd)
} else {
None
}
crd.scheme.clone_from(&pm.scheme);
Some(crd)
} else {
logger::debug!(
"Getting card details from locker as it is not found in payment methods table"

View File

@ -348,7 +348,10 @@ pub fn mk_add_card_response_hs(
let card_isin = card_number.get_card_isin();
let card = api::CardDetailFromLocker {
scheme: None,
scheme: card
.card_network
.clone()
.map(|card_network| card_network.to_string()),
last4_digits: Some(last4_digits),
issuer_country: card.card_issuing_country,
card_number: Some(card.card_number.clone()),

View File

@ -1385,6 +1385,9 @@ pub(crate) async fn get_payment_method_create_request(
.map(|card_network| card_network.to_string()),
client_secret: None,
payment_method_data: None,
billing: None,
connector_mandate_details: None,
network_transaction_id: None,
};
Ok(payment_method_request)
}
@ -1404,6 +1407,9 @@ pub(crate) async fn get_payment_method_create_request(
card_network: None,
client_secret: None,
payment_method_data: None,
billing: None,
connector_mandate_details: None,
network_transaction_id: None,
};
Ok(payment_method_request)
@ -4074,6 +4080,63 @@ pub async fn get_additional_payment_data(
}
}
pub async fn populate_bin_details_for_payment_method_create(
card_details: api_models::payment_methods::CardDetail,
db: &dyn StorageInterface,
) -> api_models::payment_methods::CardDetail {
let card_isin: Option<_> = Some(card_details.card_number.get_card_isin());
if card_details.card_issuer.is_some()
&& card_details.card_network.is_some()
&& card_details.card_type.is_some()
&& card_details.card_issuing_country.is_some()
{
api::CardDetail {
card_issuer: card_details.card_issuer.to_owned(),
card_network: card_details.card_network.clone(),
card_type: card_details.card_type.to_owned(),
card_issuing_country: card_details.card_issuing_country.to_owned(),
card_exp_month: card_details.card_exp_month.clone(),
card_exp_year: card_details.card_exp_year.clone(),
card_holder_name: card_details.card_holder_name.clone(),
card_number: card_details.card_number.clone(),
nick_name: card_details.nick_name.clone(),
}
} else {
let card_info = card_isin
.clone()
.async_and_then(|card_isin| async move {
db.get_card_info(&card_isin)
.await
.map_err(|error| services::logger::error!(card_info_error=?error))
.ok()
})
.await
.flatten()
.map(|card_info| api::CardDetail {
card_issuer: card_info.card_issuer,
card_network: card_info.card_network.clone(),
card_type: card_info.card_type,
card_issuing_country: card_info.card_issuing_country,
card_exp_month: card_details.card_exp_month.clone(),
card_exp_year: card_details.card_exp_year.clone(),
card_holder_name: card_details.card_holder_name.clone(),
card_number: card_details.card_number.clone(),
nick_name: card_details.nick_name.clone(),
});
card_info.unwrap_or_else(|| api::CardDetail {
card_issuer: None,
card_network: None,
card_type: None,
card_issuing_country: None,
card_exp_month: card_details.card_exp_month.clone(),
card_exp_year: card_details.card_exp_year.clone(),
card_holder_name: card_details.card_holder_name.clone(),
card_number: card_details.card_number.clone(),
nick_name: card_details.nick_name.clone(),
})
}
}
pub fn validate_customer_access(
payment_intent: &PaymentIntent,
auth_flow: services::AuthFlow,
@ -5057,3 +5120,36 @@ pub async fn override_setup_future_usage_to_on_session<F: Clone>(
};
Ok(())
}
pub async fn validate_merchant_connector_ids_in_connector_mandate_details(
db: &dyn StorageInterface,
key_store: &domain::MerchantKeyStore,
connector_mandate_details: &api_models::payment_methods::PaymentsMandateReference,
merchant_id: &str,
) -> CustomResult<(), errors::ApiErrorResponse> {
let merchant_connector_account_list = db
.find_merchant_connector_account_by_merchant_id_and_disabled_list(
merchant_id,
true,
key_store,
)
.await
.to_not_found_response(errors::ApiErrorResponse::InternalServerError)?;
let merchant_connector_ids: Vec<String> = merchant_connector_account_list
.iter()
.map(|merchant_connector_account| merchant_connector_account.merchant_connector_id.clone())
.collect();
for merchant_connector_id in connector_mandate_details.0.keys() {
if !merchant_connector_ids.contains(merchant_connector_id) {
Err(errors::ApiErrorResponse::InvalidDataValue {
field_name: "merchant_connector_id",
})
.attach_printable_lazy(|| {
format!("{merchant_connector_id} invalid merchant connector id in connector_mandate_details")
})?
}
}
Ok(())
}

View File

@ -318,6 +318,10 @@ where
network_transaction_id,
merchant_account.storage_scheme,
encrypted_payment_method_billing_address,
resp.card.and_then(|card| {
card.card_network
.map(|card_network| card_network.to_string())
}),
)
.await
} else {
@ -397,7 +401,7 @@ where
payment_methods::cards::insert_payment_method(
db,
&resp,
payment_method_create_request.clone(),
&payment_method_create_request.clone(),
key_store,
&merchant_account.merchant_id,
&customer_id,
@ -602,6 +606,10 @@ where
network_transaction_id,
merchant_account.storage_scheme,
encrypted_payment_method_billing_address,
resp.card.and_then(|card| {
card.card_network
.map(|card_network| card_network.to_string())
}),
)
.await?;
};

View File

@ -391,6 +391,9 @@ pub async fn save_payout_data_to_locker(
card_network: None,
client_secret: None,
payment_method_data: None,
billing: None,
connector_mandate_details: None,
network_transaction_id: None,
};
let pm_data = card_isin
@ -472,6 +475,9 @@ pub async fn save_payout_data_to_locker(
card_network: None,
client_secret: None,
payment_method_data: None,
billing: None,
connector_mandate_details: None,
network_transaction_id: None,
},
)
};
@ -495,6 +501,7 @@ pub async fn save_payout_data_to_locker(
None,
merchant_account.storage_scheme,
None,
None,
)
.await?;
}

View File

@ -478,7 +478,6 @@ async fn store_bank_details_in_payment_methods(
last_used_at: now,
connector_mandate_details: None,
customer_acceptance: None,
network_transaction_id: None,
client_secret: None,
payment_method_billing_address: None,