pub mod cards; pub mod migration; 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; 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::CustomerId}; 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}; use masking::PeekInterface; use router_env::{instrument, tracing}; use time::Duration; use super::errors::{RouterResponse, StorageErrorExt}; use crate::{ consts, core::{ errors::{self, RouterResult}, payments::helpers, pm_auth as core_pm_auth, }, routes::{app::StorageInterface, SessionState}, services, types::{domain, storage}, }; 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( pm_data: &Option, state: &SessionState, payment_intent: &PaymentIntent, payment_attempt: &PaymentAttempt, merchant_key_store: &domain::MerchantKeyStore, business_profile: Option<&domain::BusinessProfile>, ) -> RouterResult<(Option, Option)> { match pm_data { pm_opt @ Some(pm @ domain::PaymentMethodData::Card(_)) => { let payment_token = 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 @ Some(domain::PaymentMethodData::PayLater(_)) => Ok((pm.to_owned(), None)), pm @ Some(domain::PaymentMethodData::Crypto(_)) => Ok((pm.to_owned(), None)), pm @ Some(domain::PaymentMethodData::BankDebit(_)) => 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 = 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 = 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 = 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 = 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 = 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: masking::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.get_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}") } pub async fn add_payment_method_status_update_task( db: &dyn StorageInterface, payment_method: &diesel_models::PaymentMethod, prev_status: enums::PaymentMethodStatus, curr_status: enums::PaymentMethodStatus, merchant_id: &common_utils::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.payment_method_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.payment_method_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.payment_method_id.clone() ) })?; Ok(()) } #[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, ) -> RouterResult { let token = match token_data { storage::PaymentTokenData::TemporaryGeneric(generic_token) => { 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) => { 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) => { 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, ) .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) => { 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, ) .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) => { core_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) }