mirror of
				https://github.com/juspay/hyperswitch.git
				synced 2025-10-31 18:17:13 +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
	 Prasunna Soppa
					Prasunna Soppa