pub mod cards; pub mod migration; pub mod network_tokenization; pub mod surcharge_decision_configs; pub mod transformers; pub mod utils; mod validator; pub mod vault; use std::borrow::Cow; #[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] use std::collections::HashSet; #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] use std::str::FromStr; #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] pub use api_models::enums as api_enums; pub use api_models::enums::Connector; use api_models::payment_methods; #[cfg(feature = "payouts")] pub use api_models::{enums::PayoutConnectors, payouts as payout_types}; #[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] use common_utils::ext_traits::Encode; use common_utils::{consts::DEFAULT_LOCALE, id_type}; #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] use common_utils::{ crypto::{self, Encryptable}, ext_traits::{AsyncExt, Encode, StringExt, ValueExt}, fp_utils::when, generate_id, request::RequestContent, types as util_types, }; use diesel_models::{ enums, GenericLinkNew, PaymentMethodCollectLink, PaymentMethodCollectLinkData, }; use error_stack::{report, ResultExt}; #[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] use hyperswitch_domain_models::api::{GenericLinks, GenericLinksData}; use hyperswitch_domain_models::payments::{payment_attempt::PaymentAttempt, PaymentIntent}; #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] use masking::ExposeInterface; use masking::{PeekInterface, Secret}; use router_env::{instrument, tracing}; use time::Duration; use super::{ errors::{RouterResponse, StorageErrorExt}, pm_auth, }; #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] use crate::{ configs::settings, core::{payment_methods::transformers as pm_transforms, utils as core_utils}, headers, logger, routes::payment_methods as pm_routes, services::encryption, types::{ api::{self, payment_methods::PaymentMethodCreateExt}, payment_methods as pm_types, storage::PaymentMethodListContext, }, utils::ext_traits::OptionExt, }; use crate::{ consts, core::{ errors::{self, RouterResult}, payments::helpers as payment_helpers, }, routes::{app::StorageInterface, SessionState}, services, types::{ domain, storage::{self, enums as storage_enums}, }, }; const PAYMENT_METHOD_STATUS_UPDATE_TASK: &str = "PAYMENT_METHOD_STATUS_UPDATE"; const PAYMENT_METHOD_STATUS_TAG: &str = "PAYMENT_METHOD_STATUS"; #[instrument(skip_all)] pub async fn retrieve_payment_method_core( pm_data: &Option, state: &SessionState, payment_intent: &PaymentIntent, payment_attempt: &PaymentAttempt, merchant_key_store: &domain::MerchantKeyStore, business_profile: Option<&domain::Profile>, ) -> RouterResult<(Option, Option)> { match pm_data { pm_opt @ Some(pm @ domain::PaymentMethodData::Card(_)) => { let payment_token = payment_helpers::store_payment_method_data_in_vault( state, payment_attempt, payment_intent, enums::PaymentMethod::Card, pm, merchant_key_store, business_profile, ) .await?; Ok((pm_opt.to_owned(), payment_token)) } pm_opt @ Some(pm @ domain::PaymentMethodData::BankDebit(_)) => { let payment_token = payment_helpers::store_payment_method_data_in_vault( state, payment_attempt, payment_intent, enums::PaymentMethod::BankDebit, pm, merchant_key_store, business_profile, ) .await?; Ok((pm_opt.to_owned(), payment_token)) } pm @ Some(domain::PaymentMethodData::PayLater(_)) => Ok((pm.to_owned(), None)), pm @ Some(domain::PaymentMethodData::Crypto(_)) => Ok((pm.to_owned(), None)), pm @ Some(domain::PaymentMethodData::Upi(_)) => Ok((pm.to_owned(), None)), pm @ Some(domain::PaymentMethodData::Voucher(_)) => Ok((pm.to_owned(), None)), pm @ Some(domain::PaymentMethodData::Reward) => Ok((pm.to_owned(), None)), pm @ Some(domain::PaymentMethodData::RealTimePayment(_)) => Ok((pm.to_owned(), None)), pm @ Some(domain::PaymentMethodData::CardRedirect(_)) => Ok((pm.to_owned(), None)), pm @ Some(domain::PaymentMethodData::GiftCard(_)) => Ok((pm.to_owned(), None)), pm @ Some(domain::PaymentMethodData::OpenBanking(_)) => Ok((pm.to_owned(), None)), pm_opt @ Some(pm @ domain::PaymentMethodData::BankTransfer(_)) => { let payment_token = payment_helpers::store_payment_method_data_in_vault( state, payment_attempt, payment_intent, enums::PaymentMethod::BankTransfer, pm, merchant_key_store, business_profile, ) .await?; Ok((pm_opt.to_owned(), payment_token)) } pm_opt @ Some(pm @ domain::PaymentMethodData::Wallet(_)) => { let payment_token = payment_helpers::store_payment_method_data_in_vault( state, payment_attempt, payment_intent, enums::PaymentMethod::Wallet, pm, merchant_key_store, business_profile, ) .await?; Ok((pm_opt.to_owned(), payment_token)) } pm_opt @ Some(pm @ domain::PaymentMethodData::BankRedirect(_)) => { let payment_token = payment_helpers::store_payment_method_data_in_vault( state, payment_attempt, payment_intent, enums::PaymentMethod::BankRedirect, pm, merchant_key_store, business_profile, ) .await?; Ok((pm_opt.to_owned(), payment_token)) } _ => Ok((None, None)), } } pub async fn initiate_pm_collect_link( state: SessionState, merchant_account: domain::MerchantAccount, key_store: domain::MerchantKeyStore, req: payment_methods::PaymentMethodCollectLinkRequest, ) -> RouterResponse { // Validate request and initiate flow let pm_collect_link_data = validator::validate_request_and_initiate_payment_method_collect_link( &state, &merchant_account, &key_store, &req, ) .await?; // Create DB entries let pm_collect_link = create_pm_collect_db_entry( &state, &merchant_account, &pm_collect_link_data, req.return_url.clone(), ) .await?; let customer_id = id_type::CustomerId::try_from(Cow::from(pm_collect_link.primary_reference)) .change_context(errors::ApiErrorResponse::InvalidDataValue { field_name: "customer_id", })?; // Return response let url = pm_collect_link.url.peek(); let response = payment_methods::PaymentMethodCollectLinkResponse { pm_collect_link_id: pm_collect_link.link_id, customer_id, expiry: pm_collect_link.expiry, link: url::Url::parse(url) .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable_lazy(|| { format!("Failed to parse the payment method collect link - {}", url) })? .into(), return_url: pm_collect_link.return_url, ui_config: pm_collect_link.link_data.ui_config, enabled_payment_methods: pm_collect_link.link_data.enabled_payment_methods, }; Ok(services::ApplicationResponse::Json(response)) } pub async fn create_pm_collect_db_entry( state: &SessionState, merchant_account: &domain::MerchantAccount, pm_collect_link_data: &PaymentMethodCollectLinkData, return_url: Option, ) -> RouterResult { let db: &dyn StorageInterface = &*state.store; let link_data = serde_json::to_value(pm_collect_link_data) .map_err(|_| report!(errors::ApiErrorResponse::InternalServerError)) .attach_printable("Failed to convert PaymentMethodCollectLinkData to Value")?; let pm_collect_link = GenericLinkNew { link_id: pm_collect_link_data.pm_collect_link_id.to_string(), primary_reference: pm_collect_link_data .customer_id .get_string_repr() .to_string(), merchant_id: merchant_account.get_id().to_owned(), link_type: common_enums::GenericLinkType::PaymentMethodCollect, link_data, url: pm_collect_link_data.link.clone(), return_url, expiry: common_utils::date_time::now() + Duration::seconds(pm_collect_link_data.session_expiry.into()), ..Default::default() }; db.insert_pm_collect_link(pm_collect_link) .await .to_duplicate_response(errors::ApiErrorResponse::GenericDuplicateError { message: "payment method collect link already exists".to_string(), }) } #[cfg(all(feature = "v2", feature = "customer_v2"))] pub async fn render_pm_collect_link( _state: SessionState, _merchant_account: domain::MerchantAccount, _key_store: domain::MerchantKeyStore, _req: payment_methods::PaymentMethodCollectLinkRenderRequest, ) -> RouterResponse { todo!() } #[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] pub async fn render_pm_collect_link( state: SessionState, merchant_account: domain::MerchantAccount, key_store: domain::MerchantKeyStore, req: payment_methods::PaymentMethodCollectLinkRenderRequest, ) -> RouterResponse { let db: &dyn StorageInterface = &*state.store; // Fetch pm collect link let pm_collect_link = db .find_pm_collect_link_by_link_id(&req.pm_collect_link_id) .await .to_not_found_response(errors::ApiErrorResponse::GenericNotFoundError { message: "payment method collect link not found".to_string(), })?; // Check status and return form data accordingly let has_expired = common_utils::date_time::now() > pm_collect_link.expiry; let status = pm_collect_link.link_status; let link_data = pm_collect_link.link_data; let default_config = &state.conf.generic_link.payment_method_collect; let default_ui_config = default_config.ui_config.clone(); let ui_config_data = common_utils::link_utils::GenericLinkUiConfigFormData { merchant_name: link_data .ui_config .merchant_name .unwrap_or(default_ui_config.merchant_name), logo: link_data.ui_config.logo.unwrap_or(default_ui_config.logo), theme: link_data .ui_config .theme .clone() .unwrap_or(default_ui_config.theme.clone()), }; match status { common_utils::link_utils::PaymentMethodCollectStatus::Initiated => { // if expired, send back expired status page if has_expired { let expired_link_data = services::GenericExpiredLinkData { title: "Payment collect link has expired".to_string(), message: "This payment collect link has expired.".to_string(), theme: link_data.ui_config.theme.unwrap_or(default_ui_config.theme), }; Ok(services::ApplicationResponse::GenericLinkForm(Box::new( GenericLinks { allowed_domains: HashSet::from([]), data: GenericLinksData::ExpiredLink(expired_link_data), locale: DEFAULT_LOCALE.to_string(), }, ))) // else, send back form link } else { let customer_id = id_type::CustomerId::try_from(Cow::from( pm_collect_link.primary_reference.clone(), )) .change_context(errors::ApiErrorResponse::InvalidDataValue { field_name: "customer_id", })?; // Fetch customer let customer = db .find_customer_by_customer_id_merchant_id( &(&state).into(), &customer_id, &req.merchant_id, &key_store, merchant_account.storage_scheme, ) .await .change_context(errors::ApiErrorResponse::InvalidRequestData { message: format!( "Customer [{}] not found for link_id - {}", pm_collect_link.primary_reference, pm_collect_link.link_id ), }) .attach_printable(format!( "customer [{}] not found", pm_collect_link.primary_reference ))?; let js_data = payment_methods::PaymentMethodCollectLinkDetails { publishable_key: Secret::new(merchant_account.publishable_key), client_secret: link_data.client_secret.clone(), pm_collect_link_id: pm_collect_link.link_id, customer_id: customer.customer_id, session_expiry: pm_collect_link.expiry, return_url: pm_collect_link.return_url, ui_config: ui_config_data, enabled_payment_methods: link_data.enabled_payment_methods, }; let serialized_css_content = String::new(); let serialized_js_content = format!( "window.__PM_COLLECT_DETAILS = {}", js_data .encode_to_string_of_json() .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to serialize PaymentMethodCollectLinkDetails")? ); let generic_form_data = services::GenericLinkFormData { js_data: serialized_js_content, css_data: serialized_css_content, sdk_url: default_config.sdk_url.to_string(), html_meta_tags: String::new(), }; Ok(services::ApplicationResponse::GenericLinkForm(Box::new( GenericLinks { allowed_domains: HashSet::from([]), data: GenericLinksData::PaymentMethodCollect(generic_form_data), locale: DEFAULT_LOCALE.to_string(), }, ))) } } // Send back status page status => { let js_data = payment_methods::PaymentMethodCollectLinkStatusDetails { pm_collect_link_id: pm_collect_link.link_id, customer_id: link_data.customer_id, session_expiry: pm_collect_link.expiry, return_url: pm_collect_link .return_url .as_ref() .map(|url| url::Url::parse(url)) .transpose() .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable( "Failed to parse return URL for payment method collect's status link", )?, ui_config: ui_config_data, status, }; let serialized_css_content = String::new(); let serialized_js_content = format!( "window.__PM_COLLECT_DETAILS = {}", js_data .encode_to_string_of_json() .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable( "Failed to serialize PaymentMethodCollectLinkStatusDetails" )? ); let generic_status_data = services::GenericLinkStatusData { js_data: serialized_js_content, css_data: serialized_css_content, }; Ok(services::ApplicationResponse::GenericLinkForm(Box::new( GenericLinks { allowed_domains: HashSet::from([]), data: GenericLinksData::PaymentMethodCollectStatus(generic_status_data), locale: DEFAULT_LOCALE.to_string(), }, ))) } } } fn generate_task_id_for_payment_method_status_update_workflow( key_id: &str, runner: &storage::ProcessTrackerRunner, task: &str, ) -> String { format!("{runner}_{task}_{key_id}") } #[cfg(all( any(feature = "v1", feature = "v2"), not(feature = "payment_methods_v2") ))] pub async fn add_payment_method_status_update_task( db: &dyn StorageInterface, payment_method: &domain::PaymentMethod, prev_status: enums::PaymentMethodStatus, curr_status: enums::PaymentMethodStatus, merchant_id: &id_type::MerchantId, ) -> Result<(), errors::ProcessTrackerError> { let created_at = payment_method.created_at; let schedule_time = created_at.saturating_add(Duration::seconds(consts::DEFAULT_SESSION_EXPIRY)); let tracking_data = storage::PaymentMethodStatusTrackingData { payment_method_id: payment_method.get_id().clone(), prev_status, curr_status, merchant_id: merchant_id.to_owned(), }; let runner = storage::ProcessTrackerRunner::PaymentMethodStatusUpdateWorkflow; let task = PAYMENT_METHOD_STATUS_UPDATE_TASK; let tag = [PAYMENT_METHOD_STATUS_TAG]; let process_tracker_id = generate_task_id_for_payment_method_status_update_workflow( payment_method.get_id().as_str(), &runner, task, ); let process_tracker_entry = storage::ProcessTrackerNew::new( process_tracker_id, task, runner, tag, tracking_data, schedule_time, ) .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to construct PAYMENT_METHOD_STATUS_UPDATE process tracker task")?; db .insert_process(process_tracker_entry) .await .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable_lazy(|| { format!( "Failed while inserting PAYMENT_METHOD_STATUS_UPDATE reminder to process_tracker for payment_method_id: {}", payment_method.get_id().clone() ) })?; Ok(()) } #[cfg(feature = "v2")] #[allow(clippy::too_many_arguments)] #[instrument(skip_all)] pub async fn retrieve_payment_method_with_token( _state: &SessionState, _merchant_key_store: &domain::MerchantKeyStore, _token_data: &storage::PaymentTokenData, _payment_intent: &PaymentIntent, _card_token_data: Option<&domain::CardToken>, _customer: &Option, _storage_scheme: common_enums::enums::MerchantStorageScheme, _mandate_id: Option, _payment_method_info: Option, _business_profile: &domain::Profile, ) -> RouterResult { todo!() } #[cfg(all( any(feature = "v1", feature = "v2"), not(feature = "payment_methods_v2") ))] #[instrument(skip_all)] #[allow(clippy::too_many_arguments)] pub async fn retrieve_payment_method_with_token( state: &SessionState, merchant_key_store: &domain::MerchantKeyStore, token_data: &storage::PaymentTokenData, payment_intent: &PaymentIntent, card_token_data: Option<&domain::CardToken>, customer: &Option, storage_scheme: common_enums::enums::MerchantStorageScheme, mandate_id: Option, payment_method_info: Option, business_profile: &domain::Profile, ) -> RouterResult { let token = match token_data { storage::PaymentTokenData::TemporaryGeneric(generic_token) => { payment_helpers::retrieve_payment_method_with_temporary_token( state, &generic_token.token, payment_intent, merchant_key_store, card_token_data, ) .await? .map( |(payment_method_data, payment_method)| storage::PaymentMethodDataWithId { payment_method_data: Some(payment_method_data), payment_method: Some(payment_method), payment_method_id: None, }, ) .unwrap_or_default() } storage::PaymentTokenData::Temporary(generic_token) => { payment_helpers::retrieve_payment_method_with_temporary_token( state, &generic_token.token, payment_intent, merchant_key_store, card_token_data, ) .await? .map( |(payment_method_data, payment_method)| storage::PaymentMethodDataWithId { payment_method_data: Some(payment_method_data), payment_method: Some(payment_method), payment_method_id: None, }, ) .unwrap_or_default() } storage::PaymentTokenData::Permanent(card_token) => { payment_helpers::retrieve_card_with_permanent_token( state, card_token.locker_id.as_ref().unwrap_or(&card_token.token), card_token .payment_method_id .as_ref() .unwrap_or(&card_token.token), payment_intent, card_token_data, merchant_key_store, storage_scheme, mandate_id, payment_method_info, business_profile, ) .await .map(|card| Some((card, enums::PaymentMethod::Card)))? .map( |(payment_method_data, payment_method)| storage::PaymentMethodDataWithId { payment_method_data: Some(payment_method_data), payment_method: Some(payment_method), payment_method_id: Some( card_token .payment_method_id .as_ref() .unwrap_or(&card_token.token) .to_string(), ), }, ) .unwrap_or_default() } storage::PaymentTokenData::PermanentCard(card_token) => { payment_helpers::retrieve_card_with_permanent_token( state, card_token.locker_id.as_ref().unwrap_or(&card_token.token), card_token .payment_method_id .as_ref() .unwrap_or(&card_token.token), payment_intent, card_token_data, merchant_key_store, storage_scheme, mandate_id, payment_method_info, business_profile, ) .await .map(|card| Some((card, enums::PaymentMethod::Card)))? .map( |(payment_method_data, payment_method)| storage::PaymentMethodDataWithId { payment_method_data: Some(payment_method_data), payment_method: Some(payment_method), payment_method_id: Some( card_token .payment_method_id .as_ref() .unwrap_or(&card_token.token) .to_string(), ), }, ) .unwrap_or_default() } storage::PaymentTokenData::AuthBankDebit(auth_token) => { pm_auth::retrieve_payment_method_from_auth_service( state, merchant_key_store, auth_token, payment_intent, customer, ) .await? .map( |(payment_method_data, payment_method)| storage::PaymentMethodDataWithId { payment_method_data: Some(payment_method_data), payment_method: Some(payment_method), payment_method_id: None, }, ) .unwrap_or_default() } storage::PaymentTokenData::WalletToken(_) => storage::PaymentMethodDataWithId { payment_method: None, payment_method_data: None, payment_method_id: None, }, }; Ok(token) } #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] #[instrument(skip_all)] pub(crate) async fn get_payment_method_create_request( payment_method_data: Option<&domain::PaymentMethodData>, payment_method: Option, payment_method_type: Option, customer_id: &Option, billing_name: Option>, ) -> RouterResult { match payment_method_data { Some(pm_data) => match payment_method { Some(payment_method) => match pm_data { domain::PaymentMethodData::Card(card) => { let card_detail = payment_methods::CardDetail { card_number: card.card_number.clone(), card_exp_month: card.card_exp_month.clone(), card_exp_year: card.card_exp_year.clone(), card_holder_name: billing_name, nick_name: card.nick_name.clone(), card_issuing_country: card .card_issuing_country .as_ref() .map(|c| api_enums::CountryAlpha2::from_str(c)) .transpose() .ok() .flatten(), card_network: card.card_network.clone(), card_issuer: card.card_issuer.clone(), card_type: card .card_type .as_ref() .map(|c| payment_methods::CardType::from_str(c)) .transpose() .ok() .flatten(), }; let payment_method_request = payment_methods::PaymentMethodCreate { payment_method, payment_method_type: payment_method_type .get_required_value("Payment_method_type") .change_context(errors::ApiErrorResponse::MissingRequiredField { field_name: "payment_method_data", })?, metadata: None, customer_id: customer_id .clone() .get_required_value("customer_id") .change_context(errors::ApiErrorResponse::MissingRequiredField { field_name: "customer_id", })?, payment_method_data: payment_methods::PaymentMethodCreateData::Card( card_detail, ), billing: None, }; Ok(payment_method_request) } _ => Err(report!(errors::ApiErrorResponse::MissingRequiredField { field_name: "payment_method_data" }) .attach_printable("Payment method data is incorrect")), }, None => Err(report!(errors::ApiErrorResponse::MissingRequiredField { field_name: "payment_method_type" }) .attach_printable("PaymentMethodType Required")), }, None => Err(report!(errors::ApiErrorResponse::MissingRequiredField { field_name: "payment_method_data" }) .attach_printable("PaymentMethodData required Or Card is already saved")), } } #[cfg(all( any(feature = "v1", feature = "v2"), not(feature = "payment_methods_v2") ))] #[instrument(skip_all)] pub(crate) async fn get_payment_method_create_request( payment_method_data: Option<&domain::PaymentMethodData>, payment_method: Option, payment_method_type: Option, customer_id: &Option, billing_name: Option>, ) -> RouterResult { match payment_method_data { Some(pm_data) => match payment_method { Some(payment_method) => match pm_data { domain::PaymentMethodData::Card(card) => { let card_detail = payment_methods::CardDetail { card_number: card.card_number.clone(), card_exp_month: card.card_exp_month.clone(), card_exp_year: card.card_exp_year.clone(), card_holder_name: billing_name, nick_name: card.nick_name.clone(), card_issuing_country: card.card_issuing_country.clone(), card_network: card.card_network.clone(), card_issuer: card.card_issuer.clone(), card_type: card.card_type.clone(), }; let payment_method_request = payment_methods::PaymentMethodCreate { payment_method: Some(payment_method), payment_method_type, payment_method_issuer: card.card_issuer.clone(), payment_method_issuer_code: None, #[cfg(feature = "payouts")] bank_transfer: None, #[cfg(feature = "payouts")] wallet: None, card: Some(card_detail), metadata: None, customer_id: customer_id.clone(), card_network: card .card_network .as_ref() .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) } _ => { let payment_method_request = payment_methods::PaymentMethodCreate { payment_method: Some(payment_method), payment_method_type, payment_method_issuer: None, payment_method_issuer_code: None, #[cfg(feature = "payouts")] bank_transfer: None, #[cfg(feature = "payouts")] wallet: None, card: None, metadata: None, customer_id: customer_id.clone(), card_network: None, client_secret: None, payment_method_data: None, billing: None, connector_mandate_details: None, network_transaction_id: None, }; Ok(payment_method_request) } }, None => Err(report!(errors::ApiErrorResponse::MissingRequiredField { field_name: "payment_method_type" }) .attach_printable("PaymentMethodType Required")), }, None => Err(report!(errors::ApiErrorResponse::MissingRequiredField { field_name: "payment_method_data" }) .attach_printable("PaymentMethodData required Or Card is already saved")), } } #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] #[instrument(skip_all)] pub async fn create_payment_method( state: &SessionState, req: api::PaymentMethodCreate, merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, ) -> RouterResponse { req.validate()?; let db = &*state.store; let merchant_id = merchant_account.get_id(); let customer_id = req.customer_id.to_owned(); db.find_customer_by_merchant_reference_id_merchant_id( &(state.into()), &customer_id, merchant_account.get_id(), key_store, merchant_account.storage_scheme, ) .await .to_not_found_response(errors::ApiErrorResponse::CustomerNotFound)?; let payment_method_billing_address: Option>> = req .billing .clone() .async_map(|billing| cards::create_encrypted_data(state, key_store, billing)) .await .transpose() .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Unable to encrypt Payment method billing address")?; // create pm let payment_method_id = id_type::GlobalPaymentMethodId::generate("random_cell_id") .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Unable to generate GlobalPaymentMethodId")?; let payment_method = create_payment_method_for_intent( state, req.metadata.clone(), &customer_id, payment_method_id, merchant_id, key_store, merchant_account.storage_scheme, payment_method_billing_address.map(Into::into), ) .await .attach_printable("Failed to add Payment method to DB")?; let payment_method_data = pm_types::PaymentMethodVaultingData::from(req.payment_method_data); let vaulting_result = vault_payment_method( state, &payment_method_data, merchant_account, key_store, None, ) .await; let response = match vaulting_result { Ok(resp) => { let pm_update = create_pm_additional_data_update( &payment_method_data, state, key_store, Some(resp.vault_id.get_string_repr().clone()), Some(req.payment_method), Some(req.payment_method_type), ) .await .attach_printable("Unable to create Payment method data")?; let payment_method = db .update_payment_method( &(state.into()), key_store, payment_method, pm_update, merchant_account.storage_scheme, ) .await .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to update payment method in db")?; let resp = pm_transforms::generate_payment_method_response(&payment_method)?; Ok(resp) } Err(e) => { let pm_update = storage::PaymentMethodUpdate::StatusUpdate { status: Some(enums::PaymentMethodStatus::Inactive), }; db.update_payment_method( &(state.into()), key_store, payment_method, pm_update, merchant_account.storage_scheme, ) .await .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to update payment method in db")?; Err(e) } }?; Ok(services::ApplicationResponse::Json(response)) } #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] #[instrument(skip_all)] pub async fn payment_method_intent_create( state: &SessionState, req: api::PaymentMethodIntentCreate, merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, ) -> RouterResponse { let db = &*state.store; let merchant_id = merchant_account.get_id(); let customer_id = req.customer_id.to_owned(); db.find_customer_by_merchant_reference_id_merchant_id( &(state.into()), &customer_id, merchant_account.get_id(), key_store, merchant_account.storage_scheme, ) .await .to_not_found_response(errors::ApiErrorResponse::CustomerNotFound)?; let payment_method_billing_address: Option>> = req .billing .clone() .async_map(|billing| cards::create_encrypted_data(state, key_store, billing)) .await .transpose() .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Unable to encrypt Payment method billing address")?; // create pm entry let payment_method_id = id_type::GlobalPaymentMethodId::generate("random_cell_id") .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Unable to generate GlobalPaymentMethodId")?; let payment_method = create_payment_method_for_intent( state, req.metadata.clone(), &customer_id, payment_method_id, merchant_id, key_store, merchant_account.storage_scheme, payment_method_billing_address.map(Into::into), ) .await .attach_printable("Failed to add Payment method to DB")?; let resp = pm_transforms::generate_payment_method_response(&payment_method)?; Ok(services::ApplicationResponse::Json(resp)) } #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] #[instrument(skip_all)] pub async fn payment_method_intent_confirm( state: &SessionState, req: api::PaymentMethodIntentConfirm, merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, pm_id: String, ) -> RouterResponse { req.validate()?; let db = &*state.store; let client_secret = req.client_secret.clone(); let pm_id = id_type::GlobalPaymentMethodId::generate_from_string(pm_id) .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Unable to generate GlobalPaymentMethodId")?; let payment_method = db .find_payment_method( &(state.into()), key_store, &pm_id, merchant_account.storage_scheme, ) .await .change_context(errors::ApiErrorResponse::PaymentMethodNotFound) .attach_printable("Unable to find payment method")?; when( cards::authenticate_pm_client_secret_and_check_expiry(&client_secret, &payment_method)?, || Err(errors::ApiErrorResponse::ClientSecretExpired), )?; when( payment_method.status != enums::PaymentMethodStatus::AwaitingData, || { Err(errors::ApiErrorResponse::InvalidRequestData { message: "Invalid pm_id provided: This Payment method cannot be confirmed" .to_string(), }) }, )?; let customer_id = payment_method.customer_id.to_owned(); db.find_customer_by_merchant_reference_id_merchant_id( &(state.into()), &customer_id, merchant_account.get_id(), key_store, merchant_account.storage_scheme, ) .await .to_not_found_response(errors::ApiErrorResponse::CustomerNotFound)?; let payment_method_data = pm_types::PaymentMethodVaultingData::from(req.payment_method_data); let vaulting_result = vault_payment_method( state, &payment_method_data, merchant_account, key_store, None, ) .await; let response = match vaulting_result { Ok(resp) => { let pm_update = create_pm_additional_data_update( &payment_method_data, state, key_store, Some(resp.vault_id.get_string_repr().clone()), Some(req.payment_method), Some(req.payment_method_type), ) .await .attach_printable("Unable to create Payment method data")?; let payment_method = db .update_payment_method( &(state.into()), key_store, payment_method, pm_update, merchant_account.storage_scheme, ) .await .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to update payment method in db")?; let resp = pm_transforms::generate_payment_method_response(&payment_method)?; Ok(resp) } Err(e) => { let pm_update = storage::PaymentMethodUpdate::StatusUpdate { status: Some(enums::PaymentMethodStatus::Inactive), }; db.update_payment_method( &(state.into()), key_store, payment_method, pm_update, merchant_account.storage_scheme, ) .await .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to update payment method in db")?; Err(e) } }?; Ok(services::ApplicationResponse::Json(response)) } #[cfg(all( feature = "v2", feature = "payment_methods_v2", feature = "customer_v2" ))] #[instrument(skip_all)] #[allow(clippy::too_many_arguments)] pub async fn create_payment_method_in_db( state: &SessionState, req: &api::PaymentMethodCreate, customer_id: &id_type::CustomerId, payment_method_id: id_type::GlobalPaymentMethodId, locker_id: Option, merchant_id: &id_type::MerchantId, pm_metadata: Option, customer_acceptance: Option, payment_method_data: crypto::OptionalEncryptableValue, key_store: &domain::MerchantKeyStore, connector_mandate_details: Option, status: Option, network_transaction_id: Option, storage_scheme: enums::MerchantStorageScheme, payment_method_billing_address: crypto::OptionalEncryptableValue, card_scheme: Option, ) -> errors::CustomResult { let db = &*state.store; let client_secret = pm_types::PaymentMethodClientSecret::generate(&payment_method_id); let current_time = common_utils::date_time::now(); let response = db .insert_payment_method( &state.into(), key_store, domain::PaymentMethod { customer_id: customer_id.to_owned(), merchant_id: merchant_id.to_owned(), id: payment_method_id, locker_id, payment_method: Some(req.payment_method), payment_method_type: Some(req.payment_method_type), metadata: pm_metadata, payment_method_data, connector_mandate_details, customer_acceptance, client_secret: Some(client_secret), status: status.unwrap_or(enums::PaymentMethodStatus::Active), network_transaction_id: network_transaction_id.to_owned(), created_at: current_time, last_modified: current_time, last_used_at: current_time, payment_method_billing_address, updated_by: None, version: domain::consts::API_VERSION, locker_fingerprint_id: None, network_token_locker_id: None, network_token_payment_method_data: None, network_token_requestor_reference_id: None, }, storage_scheme, ) .await .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to add payment method in db")?; Ok(response) } #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] #[instrument(skip_all)] #[allow(clippy::too_many_arguments)] pub async fn create_payment_method_for_intent( state: &SessionState, metadata: Option, customer_id: &id_type::CustomerId, payment_method_id: id_type::GlobalPaymentMethodId, merchant_id: &id_type::MerchantId, key_store: &domain::MerchantKeyStore, storage_scheme: enums::MerchantStorageScheme, payment_method_billing_address: crypto::OptionalEncryptableValue, ) -> errors::CustomResult { let db = &*state.store; let client_secret = pm_types::PaymentMethodClientSecret::generate(&payment_method_id); let current_time = common_utils::date_time::now(); let response = db .insert_payment_method( &state.into(), key_store, domain::PaymentMethod { customer_id: customer_id.to_owned(), merchant_id: merchant_id.to_owned(), id: payment_method_id, locker_id: None, payment_method: None, payment_method_type: None, metadata, payment_method_data: None, connector_mandate_details: None, customer_acceptance: None, client_secret: Some(client_secret), status: enums::PaymentMethodStatus::AwaitingData, network_transaction_id: None, created_at: current_time, last_modified: current_time, last_used_at: current_time, payment_method_billing_address, updated_by: None, version: domain::consts::API_VERSION, locker_fingerprint_id: None, network_token_locker_id: None, network_token_payment_method_data: None, network_token_requestor_reference_id: None, }, storage_scheme, ) .await .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to add payment method in db")?; Ok(response) } #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] pub async fn create_pm_additional_data_update( pmd: &pm_types::PaymentMethodVaultingData, state: &SessionState, key_store: &domain::MerchantKeyStore, vault_id: Option, payment_method: Option, payment_method_type: Option, ) -> RouterResult { let card = match pmd { pm_types::PaymentMethodVaultingData::Card(card) => { api::PaymentMethodsData::Card(card.clone().into()) } }; let pmd: Encryptable> = cards::create_encrypted_data(state, key_store, card) .await .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Unable to encrypt Payment method data")?; let pm_update = storage::PaymentMethodUpdate::AdditionalDataUpdate { status: Some(enums::PaymentMethodStatus::Active), locker_id: vault_id, payment_method, payment_method_type, payment_method_data: Some(pmd.into()), network_token_requestor_reference_id: None, network_token_locker_id: None, network_token_payment_method_data: None, }; Ok(pm_update) } #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] #[instrument(skip_all)] pub async fn vault_payment_method( state: &SessionState, pmd: &pm_types::PaymentMethodVaultingData, merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, existing_vault_id: Option, ) -> RouterResult { let db = &*state.store; // get fingerprint_id from locker let fingerprint_id_from_locker = cards::get_fingerprint_id_from_locker(state, pmd) .await .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to get fingerprint_id from vault")?; // throw back error if payment method is duplicated when( Some( db.find_payment_method_by_fingerprint_id( &(state.into()), key_store, &fingerprint_id_from_locker, ) .await .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to find payment method by fingerprint_id")?, ) .is_some(), || { Err(report!(errors::ApiErrorResponse::DuplicatePaymentMethod) .attach_printable("Cannot vault duplicate payment method")) }, )?; let resp_from_locker = cards::vault_payment_method_in_locker(state, merchant_account, pmd, existing_vault_id) .await .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to vault payment method in locker")?; Ok(resp_from_locker) } #[cfg(all( feature = "v2", feature = "payment_methods_v2", feature = "customer_v2" ))] async fn get_pm_list_context( state: &SessionState, payment_method: &enums::PaymentMethod, _key_store: &domain::MerchantKeyStore, pm: &domain::PaymentMethod, _parent_payment_method_token: Option, is_payment_associated: bool, ) -> Result, error_stack::Report> { let payment_method_retrieval_context = match payment_method { enums::PaymentMethod::Card => { let card_details = cards::get_card_details_with_locker_fallback(pm, state).await?; card_details.as_ref().map(|card| PaymentMethodListContext { card_details: Some(card.clone()), #[cfg(feature = "payouts")] bank_transfer_details: None, hyperswitch_token_data: is_payment_associated.then_some( storage::PaymentTokenData::permanent_card( Some(pm.get_id().clone()), pm.locker_id .clone() .or(Some(pm.get_id().get_string_repr().to_owned())), pm.locker_id .clone() .unwrap_or(pm.get_id().get_string_repr().to_owned()), ), ), }) } enums::PaymentMethod::BankDebit => { // Retrieve the pm_auth connector details so that it can be tokenized let bank_account_token_data = cards::get_bank_account_connector_details(pm) .await .unwrap_or_else(|err| { logger::error!(error=?err); None }); bank_account_token_data.map(|data| { let token_data = storage::PaymentTokenData::AuthBankDebit(data); PaymentMethodListContext { card_details: None, #[cfg(feature = "payouts")] bank_transfer_details: None, hyperswitch_token_data: is_payment_associated.then_some(token_data), } }) } _ => Some(PaymentMethodListContext { card_details: None, #[cfg(feature = "payouts")] bank_transfer_details: None, hyperswitch_token_data: is_payment_associated.then_some( storage::PaymentTokenData::temporary_generic(generate_id( consts::ID_LENGTH, "token", )), ), }), }; Ok(payment_method_retrieval_context) } #[cfg(all( feature = "v2", feature = "payment_methods_v2", feature = "customer_v2" ))] pub async fn list_customer_payment_method_util( state: SessionState, merchant_account: domain::MerchantAccount, key_store: domain::MerchantKeyStore, req: Option, customer_id: Option, is_payment_associated: bool, ) -> RouterResponse { let limit = req.as_ref().and_then(|pml_req| pml_req.limit); let (customer_id, payment_intent) = if is_payment_associated { let cloned_secret = req.and_then(|r| r.client_secret.clone()); let payment_intent = payment_helpers::verify_payment_intent_time_and_client_secret( &state, &merchant_account, &key_store, cloned_secret, ) .await?; ( payment_intent .as_ref() .and_then(|pi| pi.customer_id.clone()), payment_intent, ) } else { (customer_id, None) }; let resp = if let Some(cust) = customer_id { Box::pin(list_customer_payment_method( &state, merchant_account, key_store, payment_intent, &cust, limit, is_payment_associated, )) .await? } else { let response = api::CustomerPaymentMethodsListResponse { customer_payment_methods: Vec::new(), is_guest_customer: Some(true), }; services::ApplicationResponse::Json(response) }; Ok(resp) } #[cfg(all( feature = "v2", feature = "payment_methods_v2", feature = "customer_v2" ))] pub async fn list_customer_payment_method( state: &SessionState, merchant_account: domain::MerchantAccount, key_store: domain::MerchantKeyStore, payment_intent: Option, customer_id: &id_type::CustomerId, limit: Option, is_payment_associated: bool, ) -> RouterResponse { let db = &*state.store; let key_manager_state = &(state).into(); // let key = key_store.key.get_inner().peek(); let customer = db .find_customer_by_merchant_reference_id_merchant_id( key_manager_state, customer_id, merchant_account.get_id(), &key_store, merchant_account.storage_scheme, ) .await .to_not_found_response(errors::ApiErrorResponse::CustomerNotFound)?; let payments_info = payment_intent .async_map(|pi| { pm_types::SavedPMLPaymentsInfo::form_payments_info( pi, &merchant_account, db, key_manager_state, &key_store, ) }) .await .transpose()?; let saved_payment_methods = db .find_payment_method_by_customer_id_merchant_id_status( key_manager_state, &key_store, customer_id, merchant_account.get_id(), common_enums::PaymentMethodStatus::Active, limit, merchant_account.storage_scheme, ) .await .to_not_found_response(errors::ApiErrorResponse::PaymentMethodNotFound)?; let mut filtered_saved_payment_methods_ctx = Vec::new(); for pm in saved_payment_methods.into_iter() { let payment_method = pm.payment_method.get_required_value("payment_method")?; let parent_payment_method_token = is_payment_associated.then(|| generate_id(consts::ID_LENGTH, "token")); let pm_list_context = get_pm_list_context( state, &payment_method, &key_store, &pm, parent_payment_method_token.clone(), is_payment_associated, ) .await?; if let Some(ctx) = pm_list_context { filtered_saved_payment_methods_ctx.push((ctx, parent_payment_method_token, pm)); } } let pm_list_futures = filtered_saved_payment_methods_ctx .into_iter() .map(|ctx| { generate_saved_pm_response( state, &key_store, &merchant_account, ctx, &customer, payments_info.as_ref(), ) }) .collect::>(); let final_result = futures::future::join_all(pm_list_futures).await; let mut customer_pms = Vec::new(); for result in final_result.into_iter() { let pma = result.attach_printable("saved pm list failed")?; customer_pms.push(pma); } let mut response = api::CustomerPaymentMethodsListResponse { customer_payment_methods: customer_pms, is_guest_customer: is_payment_associated.then_some(false), //to return this key only when the request is tied to a payment intent }; if is_payment_associated { Box::pin(cards::perform_surcharge_ops( payments_info.as_ref().map(|pi| pi.payment_intent.clone()), state, merchant_account, key_store, payments_info.and_then(|pi| pi.business_profile), &mut response, )) .await?; } Ok(services::ApplicationResponse::Json(response)) } #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] async fn generate_saved_pm_response( state: &SessionState, key_store: &domain::MerchantKeyStore, merchant_account: &domain::MerchantAccount, pm_list_context: ( PaymentMethodListContext, Option, domain::PaymentMethod, ), customer: &domain::Customer, payment_info: Option<&pm_types::SavedPMLPaymentsInfo>, ) -> Result> { let (pm_list_context, parent_payment_method_token, pm) = pm_list_context; let payment_method = pm.payment_method.get_required_value("payment_method")?; let bank_details = if payment_method == enums::PaymentMethod::BankDebit { cards::get_masked_bank_details(&pm) .await .unwrap_or_else(|err| { logger::error!(error=?err); None }) } else { None }; let payment_method_billing = pm .payment_method_billing_address .clone() .map(|decrypted_data| decrypted_data.into_inner().expose()) .map(|decrypted_value| decrypted_value.parse_value("payment_method_billing_address")) .transpose() .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("unable to parse payment method billing address details")?; let connector_mandate_details = pm .connector_mandate_details .clone() .map(|val| val.parse_value::("PaymentsMandateReference")) .transpose() .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to deserialize to Payment Mandate Reference ")?; let (is_connector_agnostic_mit_enabled, requires_cvv, off_session_payment_flag, profile_id) = payment_info .map(|pi| { ( pi.is_connector_agnostic_mit_enabled, pi.requires_cvv, pi.off_session_payment_flag, pi.business_profile .as_ref() .map(|profile| profile.get_id().to_owned()), ) }) .unwrap_or((false, false, false, Default::default())); let mca_enabled = cards::get_mca_status( state, key_store, profile_id, merchant_account.get_id(), is_connector_agnostic_mit_enabled, connector_mandate_details, pm.network_transaction_id.as_ref(), ) .await?; let requires_cvv = if is_connector_agnostic_mit_enabled { requires_cvv && !(off_session_payment_flag && (pm.connector_mandate_details.is_some() || pm.network_transaction_id.is_some())) } else { requires_cvv && !(off_session_payment_flag && pm.connector_mandate_details.is_some()) }; let pmd = if let Some(card) = pm_list_context.card_details.as_ref() { Some(api::PaymentMethodListData::Card(card.clone())) } else if cfg!(feature = "payouts") { pm_list_context .bank_transfer_details .clone() .map(api::PaymentMethodListData::Bank) } else { None }; let pma = api::CustomerPaymentMethod { payment_token: parent_payment_method_token.clone(), payment_method_id: pm.get_id().get_string_repr().to_owned(), customer_id: pm.customer_id.to_owned(), payment_method, payment_method_type: pm.payment_method_type, payment_method_data: pmd, metadata: pm.metadata.clone(), recurring_enabled: mca_enabled, created: Some(pm.created_at), bank: bank_details, surcharge_details: None, requires_cvv: requires_cvv && !(off_session_payment_flag && pm.connector_mandate_details.is_some()), last_used_at: Some(pm.last_used_at), is_default: customer.default_payment_method_id.is_some() && customer.default_payment_method_id.as_deref() == Some(pm.get_id().get_string_repr()), billing: payment_method_billing, }; payment_info .async_map(|pi| { pi.perform_payment_ops(state, parent_payment_method_token, &pma, pm_list_context) }) .await .transpose()?; Ok(pma) } #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] #[instrument(skip_all)] pub async fn retrieve_payment_method( state: SessionState, pm: api::PaymentMethodId, key_store: domain::MerchantKeyStore, merchant_account: domain::MerchantAccount, ) -> RouterResponse { let db = state.store.as_ref(); let pm_id = id_type::GlobalPaymentMethodId::generate_from_string(pm.payment_method_id) .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Unable to generate GlobalPaymentMethodId")?; let payment_method = db .find_payment_method( &((&state).into()), &key_store, &pm_id, merchant_account.storage_scheme, ) .await .to_not_found_response(errors::ApiErrorResponse::PaymentMethodNotFound)?; let pmd = payment_method .payment_method_data .clone() .map(|x| x.into_inner().expose()) .and_then(|v| serde_json::from_value::(v).ok()) .and_then(|pmd| match pmd { api::PaymentMethodsData::Card(card) => { Some(api::PaymentMethodResponseData::Card(card.into())) } _ => None, }); let resp = api::PaymentMethodResponse { merchant_id: payment_method.merchant_id.to_owned(), customer_id: payment_method.customer_id.to_owned(), payment_method_id: payment_method.id.get_string_repr().to_string(), payment_method: payment_method.payment_method, payment_method_type: payment_method.payment_method_type, metadata: payment_method.metadata.clone(), created: Some(payment_method.created_at), recurring_enabled: false, last_used_at: Some(payment_method.last_used_at), client_secret: payment_method.client_secret.clone(), payment_method_data: pmd, }; Ok(services::ApplicationResponse::Json(resp)) } #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] #[instrument(skip_all)] pub async fn update_payment_method( state: SessionState, merchant_account: domain::MerchantAccount, req: api::PaymentMethodUpdate, payment_method_id: &str, key_store: domain::MerchantKeyStore, ) -> RouterResponse { let db = state.store.as_ref(); let pm_id = id_type::GlobalPaymentMethodId::generate_from_string(payment_method_id.to_string()) .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Unable to generate GlobalPaymentMethodId")?; let payment_method = db .find_payment_method( &((&state).into()), &key_store, &pm_id, merchant_account.storage_scheme, ) .await .to_not_found_response(errors::ApiErrorResponse::PaymentMethodNotFound)?; let current_vault_id = payment_method.locker_id.clone(); when( payment_method.status == enums::PaymentMethodStatus::AwaitingData, || { Err(errors::ApiErrorResponse::InvalidRequestData { message: "This Payment method is awaiting data and hence cannot be updated" .to_string(), }) }, )?; let pmd: pm_types::PaymentMethodVaultingData = cards::retrieve_payment_method_from_vault( &state, &merchant_account, &payment_method.customer_id, &payment_method, ) .await .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to retrieve payment method from vault")? .data .expose() .parse_struct("PaymentMethodCreateData") .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to parse PaymentMethodCreateData")?; let vault_request_data = pm_transforms::generate_pm_vaulting_req_from_update_request(pmd, req.payment_method_data); let vaulting_response = vault_payment_method( &state, &vault_request_data, &merchant_account, &key_store, current_vault_id, // using current vault_id for now, will have to refactor this ) // to generate new one on each vaulting later on .await .attach_printable("Failed to add payment method in vault")?; let pm_update = create_pm_additional_data_update( &vault_request_data, &state, &key_store, Some(vaulting_response.vault_id.get_string_repr().clone()), payment_method.payment_method, payment_method.payment_method_type, ) .await .attach_printable("Unable to create Payment method data")?; let payment_method = db .update_payment_method( &((&state).into()), &key_store, payment_method, pm_update, merchant_account.storage_scheme, ) .await .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to update payment method in db")?; let response = pm_transforms::generate_payment_method_response(&payment_method)?; // Add a PT task to handle payment_method delete from vault Ok(services::ApplicationResponse::Json(response)) } #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] impl pm_types::SavedPMLPaymentsInfo { pub async fn form_payments_info( payment_intent: PaymentIntent, merchant_account: &domain::MerchantAccount, db: &dyn StorageInterface, key_manager_state: &util_types::keymanager::KeyManagerState, key_store: &domain::MerchantKeyStore, ) -> RouterResult { let requires_cvv = db .find_config_by_key_unwrap_or( format!( "{}_requires_cvv", merchant_account.get_id().get_string_repr() ) .as_str(), Some("true".to_string()), ) .await .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to fetch requires_cvv config")? .config != "false"; let off_session_payment_flag = matches!( payment_intent.setup_future_usage, Some(common_enums::FutureUsage::OffSession) ); let profile_id = &payment_intent.profile_id; let business_profile = core_utils::validate_and_get_business_profile( db, key_manager_state, key_store, Some(profile_id), merchant_account.get_id(), ) .await?; let is_connector_agnostic_mit_enabled = business_profile .as_ref() .and_then(|business_profile| business_profile.is_connector_agnostic_mit_enabled) .unwrap_or(false); Ok(Self { payment_intent, business_profile, requires_cvv, off_session_payment_flag, is_connector_agnostic_mit_enabled, }) } pub async fn perform_payment_ops( &self, state: &SessionState, parent_payment_method_token: Option, pma: &api::CustomerPaymentMethod, pm_list_context: PaymentMethodListContext, ) -> RouterResult<()> { let token = parent_payment_method_token .as_ref() .get_required_value("parent_payment_method_token")?; let hyperswitch_token_data = pm_list_context .hyperswitch_token_data .get_required_value("PaymentTokenData")?; let intent_fulfillment_time = self .business_profile .as_ref() .and_then(|b_profile| b_profile.get_order_fulfillment_time()) .unwrap_or(common_utils::consts::DEFAULT_INTENT_FULFILLMENT_TIME); pm_routes::ParentPaymentMethodToken::create_key_for_token((token, pma.payment_method)) .insert(intent_fulfillment_time, hyperswitch_token_data, state) .await?; if let Some(metadata) = pma.metadata.clone() { let pm_metadata_vec: pm_transforms::PaymentMethodMetadata = metadata .parse_value("PaymentMethodMetadata") .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable( "Failed to deserialize metadata to PaymentmethodMetadata struct", )?; let redis_conn = state .store .get_redis_conn() .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to get redis connection")?; for pm_metadata in pm_metadata_vec.payment_method_tokenization { let key = format!( "pm_token_{}_{}_{}", token, pma.payment_method, pm_metadata.0 ); redis_conn .set_key_with_expiry(&key, pm_metadata.1, intent_fulfillment_time) .await .change_context(errors::StorageError::KVError) .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to add data in redis")?; } } Ok(()) } }