mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-29 09:07:09 +08:00
refactor(router): Remove card exp validation for migration api (#6460)
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
@ -76,7 +76,9 @@ use crate::{
|
|||||||
},
|
},
|
||||||
core::{
|
core::{
|
||||||
errors::{self, StorageErrorExt},
|
errors::{self, StorageErrorExt},
|
||||||
payment_methods::{network_tokenization, transformers as payment_methods, vault},
|
payment_methods::{
|
||||||
|
migration, network_tokenization, transformers as payment_methods, vault,
|
||||||
|
},
|
||||||
payments::{
|
payments::{
|
||||||
helpers,
|
helpers,
|
||||||
routing::{self, SessionFlowRoutingInput},
|
routing::{self, SessionFlowRoutingInput},
|
||||||
@ -410,7 +412,7 @@ pub async fn migrate_payment_method(
|
|||||||
card_number,
|
card_number,
|
||||||
&req,
|
&req,
|
||||||
);
|
);
|
||||||
get_client_secret_or_add_payment_method(
|
get_client_secret_or_add_payment_method_for_migration(
|
||||||
&state,
|
&state,
|
||||||
payment_method_create_request,
|
payment_method_create_request,
|
||||||
merchant_account,
|
merchant_account,
|
||||||
@ -448,7 +450,7 @@ pub async fn populate_bin_details_for_masked_card(
|
|||||||
card_details: &api_models::payment_methods::MigrateCardDetail,
|
card_details: &api_models::payment_methods::MigrateCardDetail,
|
||||||
db: &dyn db::StorageInterface,
|
db: &dyn db::StorageInterface,
|
||||||
) -> errors::CustomResult<api::CardDetailFromLocker, errors::ApiErrorResponse> {
|
) -> errors::CustomResult<api::CardDetailFromLocker, errors::ApiErrorResponse> {
|
||||||
helpers::validate_card_expiry(&card_details.card_exp_month, &card_details.card_exp_year)?;
|
migration::validate_card_expiry(&card_details.card_exp_month, &card_details.card_exp_year)?;
|
||||||
let card_number = card_details.card_number.clone();
|
let card_number = card_details.card_number.clone();
|
||||||
|
|
||||||
let (card_isin, _last4_digits) = get_card_bin_and_last4_digits_for_masked_card(
|
let (card_isin, _last4_digits) = get_card_bin_and_last4_digits_for_masked_card(
|
||||||
@ -887,6 +889,96 @@ pub async fn get_client_secret_or_add_payment_method(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(all(
|
||||||
|
any(feature = "v1", feature = "v2"),
|
||||||
|
not(feature = "payment_methods_v2")
|
||||||
|
))]
|
||||||
|
#[instrument(skip_all)]
|
||||||
|
pub async fn get_client_secret_or_add_payment_method_for_migration(
|
||||||
|
state: &routes::SessionState,
|
||||||
|
req: api::PaymentMethodCreate,
|
||||||
|
merchant_account: &domain::MerchantAccount,
|
||||||
|
key_store: &domain::MerchantKeyStore,
|
||||||
|
) -> errors::RouterResponse<api::PaymentMethodResponse> {
|
||||||
|
let merchant_id = merchant_account.get_id();
|
||||||
|
let customer_id = req.customer_id.clone().get_required_value("customer_id")?;
|
||||||
|
|
||||||
|
#[cfg(not(feature = "payouts"))]
|
||||||
|
let condition = req.card.is_some();
|
||||||
|
#[cfg(feature = "payouts")]
|
||||||
|
let condition = req.card.is_some() || req.bank_transfer.is_some() || req.wallet.is_some();
|
||||||
|
let key_manager_state = state.into();
|
||||||
|
let payment_method_billing_address: Option<Encryptable<Secret<serde_json::Value>>> = req
|
||||||
|
.billing
|
||||||
|
.clone()
|
||||||
|
.async_map(|billing| create_encrypted_data(&key_manager_state, key_store, billing))
|
||||||
|
.await
|
||||||
|
.transpose()
|
||||||
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||||
|
.attach_printable("Unable to encrypt Payment method billing address")?;
|
||||||
|
|
||||||
|
let connector_mandate_details = req
|
||||||
|
.connector_mandate_details
|
||||||
|
.clone()
|
||||||
|
.map(serde_json::to_value)
|
||||||
|
.transpose()
|
||||||
|
.change_context(errors::ApiErrorResponse::InternalServerError)?;
|
||||||
|
|
||||||
|
if condition {
|
||||||
|
Box::pin(save_migration_payment_method(
|
||||||
|
state,
|
||||||
|
req,
|
||||||
|
merchant_account,
|
||||||
|
key_store,
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
} else {
|
||||||
|
let payment_method_id = generate_id(consts::ID_LENGTH, "pm");
|
||||||
|
|
||||||
|
let res = create_payment_method(
|
||||||
|
state,
|
||||||
|
&req,
|
||||||
|
&customer_id,
|
||||||
|
payment_method_id.as_str(),
|
||||||
|
None,
|
||||||
|
merchant_id,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
key_store,
|
||||||
|
connector_mandate_details,
|
||||||
|
Some(enums::PaymentMethodStatus::AwaitingData),
|
||||||
|
None,
|
||||||
|
merchant_account.storage_scheme,
|
||||||
|
payment_method_billing_address.map(Into::into),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if res.status == enums::PaymentMethodStatus::AwaitingData {
|
||||||
|
add_payment_method_status_update_task(
|
||||||
|
&*state.store,
|
||||||
|
&res,
|
||||||
|
enums::PaymentMethodStatus::AwaitingData,
|
||||||
|
enums::PaymentMethodStatus::Inactive,
|
||||||
|
merchant_id,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||||
|
.attach_printable(
|
||||||
|
"Failed to add payment method status update task in process tracker",
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(services::api::ApplicationResponse::Json(
|
||||||
|
api::PaymentMethodResponse::foreign_from((None, res)),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[instrument(skip_all)]
|
#[instrument(skip_all)]
|
||||||
pub fn authenticate_pm_client_secret_and_check_expiry(
|
pub fn authenticate_pm_client_secret_and_check_expiry(
|
||||||
req_client_secret: &String,
|
req_client_secret: &String,
|
||||||
@ -1380,6 +1472,264 @@ pub async fn add_payment_method(
|
|||||||
Ok(services::ApplicationResponse::Json(resp))
|
Ok(services::ApplicationResponse::Json(resp))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(all(
|
||||||
|
any(feature = "v1", feature = "v2"),
|
||||||
|
not(feature = "payment_methods_v2")
|
||||||
|
))]
|
||||||
|
#[instrument(skip_all)]
|
||||||
|
pub async fn save_migration_payment_method(
|
||||||
|
state: &routes::SessionState,
|
||||||
|
req: api::PaymentMethodCreate,
|
||||||
|
merchant_account: &domain::MerchantAccount,
|
||||||
|
key_store: &domain::MerchantKeyStore,
|
||||||
|
) -> errors::RouterResponse<api::PaymentMethodResponse> {
|
||||||
|
req.validate()?;
|
||||||
|
let db = &*state.store;
|
||||||
|
let merchant_id = merchant_account.get_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 key_manager_state = state.into();
|
||||||
|
let payment_method_billing_address: Option<Encryptable<Secret<serde_json::Value>>> = req
|
||||||
|
.billing
|
||||||
|
.clone()
|
||||||
|
.async_map(|billing| create_encrypted_data(&key_manager_state, key_store, billing))
|
||||||
|
.await
|
||||||
|
.transpose()
|
||||||
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||||
|
.attach_printable("Unable to encrypt Payment method billing address")?;
|
||||||
|
|
||||||
|
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")]
|
||||||
|
api_enums::PaymentMethod::BankTransfer => match req.bank_transfer.clone() {
|
||||||
|
Some(bank) => add_bank_to_locker(
|
||||||
|
state,
|
||||||
|
req.clone(),
|
||||||
|
merchant_account,
|
||||||
|
key_store,
|
||||||
|
&bank,
|
||||||
|
&customer_id,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||||
|
.attach_printable("Add PaymentMethod Failed"),
|
||||||
|
_ => Ok(store_default_payment_method(
|
||||||
|
&req,
|
||||||
|
&customer_id,
|
||||||
|
merchant_id,
|
||||||
|
)),
|
||||||
|
},
|
||||||
|
api_enums::PaymentMethod::Card => match req.card.clone() {
|
||||||
|
Some(card) => {
|
||||||
|
let mut card_details = card;
|
||||||
|
card_details = helpers::populate_bin_details_for_payment_method_create(
|
||||||
|
card_details.clone(),
|
||||||
|
db,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
migration::validate_card_expiry(
|
||||||
|
&card_details.card_exp_month,
|
||||||
|
&card_details.card_exp_year,
|
||||||
|
)?;
|
||||||
|
Box::pin(add_card_to_locker(
|
||||||
|
state,
|
||||||
|
req.clone(),
|
||||||
|
&card_details,
|
||||||
|
&customer_id,
|
||||||
|
merchant_account,
|
||||||
|
None,
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||||
|
.attach_printable("Add Card Failed")
|
||||||
|
}
|
||||||
|
_ => Ok(store_default_payment_method(
|
||||||
|
&req,
|
||||||
|
&customer_id,
|
||||||
|
merchant_id,
|
||||||
|
)),
|
||||||
|
},
|
||||||
|
_ => Ok(store_default_payment_method(
|
||||||
|
&req,
|
||||||
|
&customer_id,
|
||||||
|
merchant_id,
|
||||||
|
)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let (mut resp, duplication_check) = response?;
|
||||||
|
|
||||||
|
match duplication_check {
|
||||||
|
Some(duplication_check) => match duplication_check {
|
||||||
|
payment_methods::DataDuplicationCheck::Duplicated => {
|
||||||
|
let existing_pm = get_or_insert_payment_method(
|
||||||
|
state,
|
||||||
|
req.clone(),
|
||||||
|
&mut resp,
|
||||||
|
merchant_account,
|
||||||
|
&customer_id,
|
||||||
|
key_store,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
resp.client_secret = existing_pm.client_secret;
|
||||||
|
}
|
||||||
|
payment_methods::DataDuplicationCheck::MetaDataChanged => {
|
||||||
|
if let Some(card) = req.card.clone() {
|
||||||
|
let existing_pm = get_or_insert_payment_method(
|
||||||
|
state,
|
||||||
|
req.clone(),
|
||||||
|
&mut resp,
|
||||||
|
merchant_account,
|
||||||
|
&customer_id,
|
||||||
|
key_store,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let client_secret = existing_pm.client_secret.clone();
|
||||||
|
|
||||||
|
delete_card_from_locker(
|
||||||
|
state,
|
||||||
|
&customer_id,
|
||||||
|
merchant_id,
|
||||||
|
existing_pm
|
||||||
|
.locker_id
|
||||||
|
.as_ref()
|
||||||
|
.unwrap_or(&existing_pm.payment_method_id),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let add_card_resp = add_card_hs(
|
||||||
|
state,
|
||||||
|
req.clone(),
|
||||||
|
&card,
|
||||||
|
&customer_id,
|
||||||
|
merchant_account,
|
||||||
|
api::enums::LockerChoice::HyperswitchCardVault,
|
||||||
|
Some(
|
||||||
|
existing_pm
|
||||||
|
.locker_id
|
||||||
|
.as_ref()
|
||||||
|
.unwrap_or(&existing_pm.payment_method_id),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
if let Err(err) = add_card_resp {
|
||||||
|
logger::error!(vault_err=?err);
|
||||||
|
db.delete_payment_method_by_merchant_id_payment_method_id(
|
||||||
|
&(state.into()),
|
||||||
|
key_store,
|
||||||
|
merchant_id,
|
||||||
|
&resp.payment_method_id,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.to_not_found_response(errors::ApiErrorResponse::PaymentMethodNotFound)?;
|
||||||
|
|
||||||
|
Err(report!(errors::ApiErrorResponse::InternalServerError)
|
||||||
|
.attach_printable("Failed while updating card metadata changes"))?
|
||||||
|
};
|
||||||
|
|
||||||
|
let existing_pm_data =
|
||||||
|
get_card_details_without_locker_fallback(&existing_pm, state).await?;
|
||||||
|
|
||||||
|
let updated_card = Some(api::CardDetailFromLocker {
|
||||||
|
scheme: existing_pm.scheme.clone(),
|
||||||
|
last4_digits: Some(card.card_number.get_last4()),
|
||||||
|
issuer_country: card
|
||||||
|
.card_issuing_country
|
||||||
|
.or(existing_pm_data.issuer_country),
|
||||||
|
card_isin: Some(card.card_number.get_card_isin()),
|
||||||
|
card_number: Some(card.card_number),
|
||||||
|
expiry_month: Some(card.card_exp_month),
|
||||||
|
expiry_year: Some(card.card_exp_year),
|
||||||
|
card_token: None,
|
||||||
|
card_fingerprint: None,
|
||||||
|
card_holder_name: card
|
||||||
|
.card_holder_name
|
||||||
|
.or(existing_pm_data.card_holder_name),
|
||||||
|
nick_name: card.nick_name.or(existing_pm_data.nick_name),
|
||||||
|
card_network: card.card_network.or(existing_pm_data.card_network),
|
||||||
|
card_issuer: card.card_issuer.or(existing_pm_data.card_issuer),
|
||||||
|
card_type: card.card_type.or(existing_pm_data.card_type),
|
||||||
|
saved_to_locker: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
let updated_pmd = updated_card.as_ref().map(|card| {
|
||||||
|
PaymentMethodsData::Card(CardDetailsPaymentMethod::from(card.clone()))
|
||||||
|
});
|
||||||
|
let pm_data_encrypted: Option<Encryptable<Secret<serde_json::Value>>> =
|
||||||
|
updated_pmd
|
||||||
|
.async_map(|updated_pmd| {
|
||||||
|
create_encrypted_data(&key_manager_state, key_store, updated_pmd)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.transpose()
|
||||||
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||||
|
.attach_printable("Unable to encrypt payment method data")?;
|
||||||
|
|
||||||
|
let pm_update = storage::PaymentMethodUpdate::PaymentMethodDataUpdate {
|
||||||
|
payment_method_data: pm_data_encrypted.map(Into::into),
|
||||||
|
};
|
||||||
|
|
||||||
|
db.update_payment_method(
|
||||||
|
&(state.into()),
|
||||||
|
key_store,
|
||||||
|
existing_pm,
|
||||||
|
pm_update,
|
||||||
|
merchant_account.storage_scheme,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||||
|
.attach_printable("Failed to add payment method in db")?;
|
||||||
|
|
||||||
|
resp.client_secret = client_secret;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
let pm_metadata = resp.metadata.as_ref().map(|data| data.peek());
|
||||||
|
|
||||||
|
let locker_id = if resp.payment_method == Some(api_enums::PaymentMethod::Card)
|
||||||
|
|| resp.payment_method == Some(api_enums::PaymentMethod::BankTransfer)
|
||||||
|
{
|
||||||
|
Some(resp.payment_method_id)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
resp.payment_method_id = generate_id(consts::ID_LENGTH, "pm");
|
||||||
|
let pm = insert_payment_method(
|
||||||
|
state,
|
||||||
|
&resp,
|
||||||
|
&req,
|
||||||
|
key_store,
|
||||||
|
merchant_id,
|
||||||
|
&customer_id,
|
||||||
|
pm_metadata.cloned(),
|
||||||
|
None,
|
||||||
|
locker_id,
|
||||||
|
connector_mandate_details,
|
||||||
|
req.network_transaction_id.clone(),
|
||||||
|
merchant_account.storage_scheme,
|
||||||
|
payment_method_billing_address.map(Into::into),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
resp.client_secret = pm.client_secret;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(services::ApplicationResponse::Json(resp))
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(all(
|
#[cfg(all(
|
||||||
any(feature = "v1", feature = "v2"),
|
any(feature = "v1", feature = "v2"),
|
||||||
not(feature = "payment_methods_v2")
|
not(feature = "payment_methods_v2")
|
||||||
|
|||||||
@ -2,7 +2,9 @@ use actix_multipart::form::{bytes::Bytes, text::Text, MultipartForm};
|
|||||||
use api_models::payment_methods::{PaymentMethodMigrationResponse, PaymentMethodRecord};
|
use api_models::payment_methods::{PaymentMethodMigrationResponse, PaymentMethodRecord};
|
||||||
use csv::Reader;
|
use csv::Reader;
|
||||||
use error_stack::ResultExt;
|
use error_stack::ResultExt;
|
||||||
|
use masking::PeekInterface;
|
||||||
use rdkafka::message::ToBytes;
|
use rdkafka::message::ToBytes;
|
||||||
|
use router_env::{instrument, tracing};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
core::{errors, payment_methods::cards::migrate_payment_method},
|
core::{errors, payment_methods::cards::migrate_payment_method},
|
||||||
@ -102,3 +104,48 @@ pub fn get_payment_method_records(
|
|||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(skip_all)]
|
||||||
|
pub fn validate_card_expiry(
|
||||||
|
card_exp_month: &masking::Secret<String>,
|
||||||
|
card_exp_year: &masking::Secret<String>,
|
||||||
|
) -> errors::CustomResult<(), errors::ApiErrorResponse> {
|
||||||
|
let exp_month = card_exp_month
|
||||||
|
.peek()
|
||||||
|
.to_string()
|
||||||
|
.parse::<u8>()
|
||||||
|
.change_context(errors::ApiErrorResponse::InvalidDataValue {
|
||||||
|
field_name: "card_exp_month",
|
||||||
|
})?;
|
||||||
|
::cards::CardExpirationMonth::try_from(exp_month).change_context(
|
||||||
|
errors::ApiErrorResponse::PreconditionFailed {
|
||||||
|
message: "Invalid Expiry Month".to_string(),
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let year_str = card_exp_year.peek().to_string();
|
||||||
|
|
||||||
|
validate_card_exp_year(year_str).change_context(
|
||||||
|
errors::ApiErrorResponse::PreconditionFailed {
|
||||||
|
message: "Invalid Expiry Year".to_string(),
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_card_exp_year(year: String) -> Result<(), errors::ValidationError> {
|
||||||
|
let year_str = year.to_string();
|
||||||
|
if year_str.len() == 2 || year_str.len() == 4 {
|
||||||
|
year_str
|
||||||
|
.parse::<u16>()
|
||||||
|
.map_err(|_| errors::ValidationError::InvalidValue {
|
||||||
|
message: "card_exp_year".to_string(),
|
||||||
|
})?;
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(errors::ValidationError::InvalidValue {
|
||||||
|
message: "invalid card expiration year".to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user