diff --git a/Cargo.lock b/Cargo.lock index 557504d98b95..e0b6c4f623e6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1990,6 +1990,7 @@ dependencies = [ "thiserror", "time", "tokio 1.37.0", + "url", "utoipa", "uuid", ] @@ -4311,6 +4312,7 @@ dependencies = [ "serde_json", "subtle", "time", + "url", "zeroize", ] diff --git a/api-reference/openapi_spec.json b/api-reference/openapi_spec.json index 4bb9346942bd..c164be75587b 100644 --- a/api-reference/openapi_spec.json +++ b/api-reference/openapi_spec.json @@ -9136,7 +9136,8 @@ "items": { "$ref": "#/components/schemas/PaymentMethodType" }, - "description": "An array of associated payment method types" + "description": "An array of associated payment method types", + "uniqueItems": true } } }, diff --git a/config/config.example.toml b/config/config.example.toml index 4e3747eb8f7a..ff7484e9cdbe 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -561,9 +561,9 @@ theme = "#1A1A1A" logo = "https://app.hyperswitch.io/HyperswitchFavicon.png" merchant_name = "HyperSwitch" [generic_link.payment_method_collect.enabled_payment_methods] -card = ["credit", "debit"] -bank_transfer = ["ach", "bacs", "sepa"] -wallet = ["paypal", "pix", "venmo"] +card = "credit,debit" +bank_transfer = "ach,bacs,sepa" +wallet = "paypal,pix,venmo" [generic_link.payout_link] sdk_url = "http://localhost:9090/0.16.7/v0/HyperLoader.js" @@ -573,9 +573,9 @@ theme = "#1A1A1A" logo = "https://app.hyperswitch.io/HyperswitchFavicon.png" merchant_name = "HyperSwitch" [generic_link.payout_link.enabled_payment_methods] -card = ["credit", "debit"] -bank_transfer = ["ach", "bacs", "sepa"] -wallet = ["paypal", "pix", "venmo"] +card = "credit,debit" +bank_transfer = "ach,bacs,sepa" +wallet = "paypal,pix,venmo" [payment_link] sdk_url = "http://localhost:9090/0.16.7/v0/HyperLoader.js" diff --git a/config/deployments/env_specific.toml b/config/deployments/env_specific.toml index 27567ec6cc74..7d32e76928b3 100644 --- a/config/deployments/env_specific.toml +++ b/config/deployments/env_specific.toml @@ -165,9 +165,9 @@ theme = "#4285F4" logo = "https://app.hyperswitch.io/HyperswitchFavicon.png" merchant_name = "HyperSwitch" [generic_link.payment_method_collect.enabled_payment_methods] -card = ["credit", "debit"] -bank_transfer = ["ach", "bacs", "sepa"] -wallet = ["paypal", "pix", "venmo"] +card = "credit,debit" +bank_transfer = "ach,bacs,sepa" +wallet = "paypal,pix,venmo" [generic_link.payout_link] sdk_url = "http://localhost:9090/0.16.7/v0/HyperLoader.js" @@ -177,9 +177,9 @@ theme = "#4285F4" logo = "https://app.hyperswitch.io/HyperswitchFavicon.png" merchant_name = "HyperSwitch" [generic_link.payout_link.enabled_payment_methods] -card = ["credit", "debit"] -bank_transfer = ["ach", "bacs", "sepa"] -wallet = ["paypal", "pix", "venmo"] +card = "credit,debit" +bank_transfer = "ach,bacs,sepa" +wallet = "paypal,pix,venmo" [payment_link] sdk_url = "http://localhost:9090/0.16.7/v0/HyperLoader.js" diff --git a/config/development.toml b/config/development.toml index 2aaf0e699220..40fc5cf071eb 100644 --- a/config/development.toml +++ b/config/development.toml @@ -576,9 +576,9 @@ theme = "#4285F4" logo = "https://app.hyperswitch.io/HyperswitchFavicon.png" merchant_name = "HyperSwitch" [generic_link.payment_method_collect.enabled_payment_methods] -card = ["credit", "debit"] -bank_transfer = ["ach", "bacs", "sepa"] -wallet = ["paypal", "pix", "venmo"] +card = "credit,debit" +bank_transfer = "ach,bacs,sepa" +wallet = "paypal,pix,venmo" [generic_link.payout_link] sdk_url = "http://localhost:9050/HyperLoader.js" @@ -588,9 +588,9 @@ theme = "#4285F4" logo = "https://app.hyperswitch.io/HyperswitchFavicon.png" merchant_name = "HyperSwitch" [generic_link.payout_link.enabled_payment_methods] -card = ["credit", "debit"] -bank_transfer = ["ach", "bacs", "sepa"] -wallet = ["paypal", "pix", "venmo"] +card = "credit,debit" +bank_transfer = "ach,bacs,sepa" +wallet = "paypal,pix,venmo" [payment_link] sdk_url = "http://localhost:9050/HyperLoader.js" diff --git a/config/docker_compose.toml b/config/docker_compose.toml index 2b934ceb20fd..0472204f632c 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -525,9 +525,9 @@ theme = "#4285F4" logo = "https://app.hyperswitch.io/HyperswitchFavicon.png" merchant_name = "HyperSwitch" [generic_link.payment_method_collect.enabled_payment_methods] -card = ["credit", "debit"] -bank_transfer = ["ach", "bacs", "sepa"] -wallet = ["paypal", "pix", "venmo"] +card = "credit,debit" +bank_transfer = "ach,bacs,sepa" +wallet = "paypal,pix,venmo" [generic_link.payout_link] sdk_url = "http://localhost:9090/0.16.7/v0/HyperLoader.js" @@ -537,6 +537,6 @@ theme = "#4285F4" logo = "https://app.hyperswitch.io/HyperswitchFavicon.png" merchant_name = "HyperSwitch" [generic_link.payout_link.enabled_payment_methods] -card = ["credit", "debit"] -bank_transfer = ["ach", "bacs", "sepa"] -wallet = ["paypal", "pix", "venmo"] +card = "credit,debit" +bank_transfer = "ach,bacs,sepa" +wallet = "paypal,pix,venmo" diff --git a/crates/api_models/src/payment_methods.rs b/crates/api_models/src/payment_methods.rs index 19f183d28f57..0e2fd7b70393 100644 --- a/crates/api_models/src/payment_methods.rs +++ b/crates/api_models/src/payment_methods.rs @@ -989,7 +989,7 @@ pub struct PaymentMethodCollectLinkResponse { /// URL to the form's link generated for collecting payment method details. #[schema(value_type = String, example = "https://sandbox.hyperswitch.io/payment_method/collect/pm_collect_link_2bdacf398vwzq5n422S1")] - pub link: masking::Secret, + pub link: masking::Secret, /// Redirect to this URL post completion #[schema(value_type = Option, example = "https://sandbox.hyperswitch.io/payment_method/collect/pm_collect_link_2bdacf398vwzq5n422S1/status")] @@ -1026,7 +1026,7 @@ pub struct PaymentMethodCollectLinkDetails { pub session_expiry: time::PrimitiveDateTime, pub return_url: Option, #[serde(flatten)] - pub ui_config: link_utils::GenericLinkUIConfigFormData, + pub ui_config: link_utils::GenericLinkUiConfigFormData, pub enabled_payment_methods: Option>, } @@ -1036,10 +1036,10 @@ pub struct PaymentMethodCollectLinkStatusDetails { pub customer_id: id_type::CustomerId, #[serde(with = "common_utils::custom_serde::iso8601")] pub session_expiry: time::PrimitiveDateTime, - pub return_url: Option, + pub return_url: Option, pub status: link_utils::PaymentMethodCollectStatus, #[serde(flatten)] - pub ui_config: link_utils::GenericLinkUIConfigFormData, + pub ui_config: link_utils::GenericLinkUiConfigFormData, } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)] diff --git a/crates/api_models/src/payouts.rs b/crates/api_models/src/payouts.rs index a7eb2cd568af..49a053dd603b 100644 --- a/crates/api_models/src/payouts.rs +++ b/crates/api_models/src/payouts.rs @@ -702,7 +702,7 @@ pub struct PayoutListFilters { pub struct PayoutLinkResponse { pub payout_link_id: String, #[schema(value_type = String)] - pub link: Secret, + pub link: Secret, } #[derive(Clone, Debug, serde::Deserialize, ToSchema, serde::Serialize)] @@ -720,9 +720,9 @@ pub struct PayoutLinkDetails { pub customer_id: id_type::CustomerId, #[serde(with = "common_utils::custom_serde::iso8601")] pub session_expiry: PrimitiveDateTime, - pub return_url: Option, + pub return_url: Option, #[serde(flatten)] - pub ui_config: link_utils::GenericLinkUIConfigFormData, + pub ui_config: link_utils::GenericLinkUiConfigFormData, pub enabled_payment_methods: Vec, pub amount: common_utils::types::StringMajorUnit, pub currency: common_enums::Currency, @@ -735,10 +735,10 @@ pub struct PayoutLinkStatusDetails { pub customer_id: id_type::CustomerId, #[serde(with = "common_utils::custom_serde::iso8601")] pub session_expiry: PrimitiveDateTime, - pub return_url: Option, + pub return_url: Option, pub status: api_enums::PayoutStatus, pub error_code: Option, pub error_message: Option, #[serde(flatten)] - pub ui_config: link_utils::GenericLinkUIConfigFormData, + pub ui_config: link_utils::GenericLinkUiConfigFormData, } diff --git a/crates/common_utils/Cargo.toml b/crates/common_utils/Cargo.toml index 6bf719da4821..58cdf447fd4d 100644 --- a/crates/common_utils/Cargo.toml +++ b/crates/common_utils/Cargo.toml @@ -41,6 +41,7 @@ strum = { version = "0.26.2", features = ["derive"] } thiserror = "1.0.58" time = { version = "0.3.35", features = ["serde", "serde-well-known", "std"] } tokio = { version = "1.37.0", features = ["macros", "rt-multi-thread"], optional = true } +url = { version = "2.5.0", features = ["serde"] } utoipa = { version = "4.2.0", features = ["preserve_order", "preserve_path_order"] } uuid = { version = "1.8.0", features = ["v7"] } diff --git a/crates/common_utils/src/link_utils.rs b/crates/common_utils/src/link_utils.rs index 393a8cb9a9ef..2960209dfc90 100644 --- a/crates/common_utils/src/link_utils.rs +++ b/crates/common_utils/src/link_utils.rs @@ -1,6 +1,6 @@ //! Common -use std::primitive::i64; +use std::{collections::HashSet, primitive::i64}; use common_enums::enums; use diesel::{ @@ -148,7 +148,7 @@ pub struct PayoutLinkData { /// Identifier for the payouts resource pub payout_id: String, /// Link to render the payout link - pub link: Secret, + pub link: url::Url, /// Client secret generated for authenticating frontend APIs pub client_secret: Secret, /// Expiry in seconds from the time it was created @@ -167,11 +167,11 @@ pub struct PayoutLinkData { crate::impl_to_sql_from_sql_json!(PayoutLinkData); /// Object for GenericLinkUiConfig -#[derive(Clone, Debug, Default, serde::Deserialize, Serialize, ToSchema)] +#[derive(Clone, Debug, serde::Deserialize, Serialize, ToSchema)] pub struct GenericLinkUiConfig { /// Merchant's display logo #[schema(value_type = Option, max_length = 255, example = "https://hyperswitch.io/favicon.ico")] - pub logo: Option, + pub logo: Option, /// Custom merchant name for the link #[schema(value_type = Option, max_length = 255, example = "Hyperswitch")] @@ -182,12 +182,12 @@ pub struct GenericLinkUiConfig { pub theme: Option, } -/// Object for GenericLinkUIConfigFormData -#[derive(Clone, Debug, Default, serde::Deserialize, Serialize, ToSchema)] -pub struct GenericLinkUIConfigFormData { +/// Object for GenericLinkUiConfigFormData +#[derive(Clone, Debug, serde::Deserialize, Serialize, ToSchema)] +pub struct GenericLinkUiConfigFormData { /// Merchant's display logo #[schema(value_type = String, max_length = 255, example = "https://hyperswitch.io/favicon.ico")] - pub logo: String, + pub logo: url::Url, /// Custom merchant name for the link #[schema(value_type = String, max_length = 255, example = "Hyperswitch")] @@ -206,6 +206,6 @@ pub struct EnabledPaymentMethod { pub payment_method: enums::PaymentMethod, /// An array of associated payment method types - #[schema(value_type = Vec)] - pub payment_method_types: Vec, + #[schema(value_type = HashSet)] + pub payment_method_types: HashSet, } diff --git a/crates/masking/Cargo.toml b/crates/masking/Cargo.toml index 404d4210f13b..5c73ed7ea328 100644 --- a/crates/masking/Cargo.toml +++ b/crates/masking/Cargo.toml @@ -25,6 +25,7 @@ serde = { version = "1", features = ["derive"], optional = true } serde_json = { version = "1.0.115", optional = true } subtle = "2.5.0" time = {version = "0.3.35", optional = true, features = ["serde-human-readable"] } +url = { version = "2.5.0", features = ["serde"] } zeroize = { version = "1.7", default-features = false } [dev-dependencies] diff --git a/crates/masking/src/serde.rs b/crates/masking/src/serde.rs index 86f00390d46e..45f7fbcb3d08 100644 --- a/crates/masking/src/serde.rs +++ b/crates/masking/src/serde.rs @@ -29,6 +29,7 @@ impl SerializableSecret for u8 {} impl SerializableSecret for u16 {} impl SerializableSecret for i8 {} impl SerializableSecret for i32 {} +impl SerializableSecret for url::Url {} #[cfg(feature = "time")] impl SerializableSecret for time::Date {} diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs index 2dc0afdd11ed..2762dd72af4e 100644 --- a/crates/router/src/configs/settings.rs +++ b/crates/router/src/configs/settings.rs @@ -213,21 +213,48 @@ pub struct GenericLink { pub payout_link: GenericLinkEnvConfig, } -#[derive(Debug, Deserialize, Clone, Default)] +#[derive(Debug, Deserialize, Clone)] pub struct GenericLinkEnvConfig { - pub sdk_url: String, + pub sdk_url: url::Url, pub expiry: u32, pub ui_config: GenericLinkEnvUiConfig, - pub enabled_payment_methods: HashMap>, + #[serde(deserialize_with = "deserialize_hashmap")] + pub enabled_payment_methods: HashMap>, } -#[derive(Debug, Deserialize, Clone, Default)] +impl Default for GenericLinkEnvConfig { + fn default() -> Self { + Self { + #[allow(clippy::expect_used)] + sdk_url: url::Url::parse("http://localhost:9050/HyperLoader.js") + .expect("Failed to parse default SDK URL"), + expiry: 900, + ui_config: GenericLinkEnvUiConfig::default(), + enabled_payment_methods: HashMap::default(), + } + } +} + +#[derive(Debug, Deserialize, Clone)] pub struct GenericLinkEnvUiConfig { - pub logo: String, + pub logo: url::Url, pub merchant_name: Secret, pub theme: String, } +#[allow(clippy::panic)] +impl Default for GenericLinkEnvUiConfig { + fn default() -> Self { + Self { + #[allow(clippy::expect_used)] + logo: url::Url::parse("https://hyperswitch.io/favicon.ico") + .expect("Failed to parse default logo URL"), + merchant_name: Secret::new("HyperSwitch".to_string()), + theme: "#4285F4".to_string(), + } + } +} + #[derive(Debug, Deserialize, Clone, Default)] pub struct PaymentLink { pub sdk_url: String, @@ -689,13 +716,7 @@ impl Settings { .with_list_parse_key("redis.cluster_urls") .with_list_parse_key("events.kafka.brokers") .with_list_parse_key("connectors.supported.wallets") - .with_list_parse_key("connector_request_reference_id_config.merchant_ids_send_payment_id_as_connector_request_id") - .with_list_parse_key("generic_link.payment_method_collect.enabled_payment_methods.card") - .with_list_parse_key("generic_link.payment_method_collect.enabled_payment_methods.bank_transfer") - .with_list_parse_key("generic_link.payment_method_collect.enabled_payment_methods.wallet") - .with_list_parse_key("generic_link.payout_link.enabled_payment_methods.card") - .with_list_parse_key("generic_link.payout_link.enabled_payment_methods.bank_transfer") - .with_list_parse_key("generic_link.payout_link.enabled_payment_methods.wallet"), + .with_list_parse_key("connector_request_reference_id_config.merchant_ids_send_payment_id_as_connector_request_id"), ) .build()?; @@ -841,6 +862,61 @@ pub struct ServerTls { pub certificate: PathBuf, } +fn deserialize_hashmap_inner( + value: HashMap, +) -> Result>, String> +where + K: Eq + std::str::FromStr + std::hash::Hash, + V: Eq + std::str::FromStr + std::hash::Hash, + ::Err: std::fmt::Display, + ::Err: std::fmt::Display, +{ + let (values, errors) = value + .into_iter() + .map( + |(k, v)| match (K::from_str(k.trim()), deserialize_hashset_inner(v)) { + (Err(error), _) => Err(format!( + "Unable to deserialize `{}` as `{}`: {error}", + k, + std::any::type_name::() + )), + (_, Err(error)) => Err(error), + (Ok(key), Ok(value)) => Ok((key, value)), + }, + ) + .fold( + (HashMap::new(), Vec::new()), + |(mut values, mut errors), result| match result { + Ok((key, value)) => { + values.insert(key, value); + (values, errors) + } + Err(error) => { + errors.push(error); + (values, errors) + } + }, + ); + if !errors.is_empty() { + Err(format!("Some errors occurred:\n{}", errors.join("\n"))) + } else { + Ok(values) + } +} + +fn deserialize_hashmap<'a, D, K, V>(deserializer: D) -> Result>, D::Error> +where + D: serde::Deserializer<'a>, + K: Eq + std::str::FromStr + std::hash::Hash, + V: Eq + std::str::FromStr + std::hash::Hash, + ::Err: std::fmt::Display, + ::Err: std::fmt::Display, +{ + use serde::de::Error; + deserialize_hashmap_inner(>::deserialize(deserializer)?) + .map_err(D::Error::custom) +} + fn deserialize_hashset_inner(value: impl AsRef) -> Result, String> where T: Eq + std::str::FromStr + std::hash::Hash, @@ -909,6 +985,114 @@ where })? } +#[cfg(test)] +mod hashmap_deserialization_test { + #![allow(clippy::unwrap_used)] + use std::collections::{HashMap, HashSet}; + + use serde::de::{ + value::{Error as ValueError, MapDeserializer}, + IntoDeserializer, + }; + + use super::deserialize_hashmap; + + #[test] + fn test_payment_method_and_payment_method_types() { + use diesel_models::enums::{PaymentMethod, PaymentMethodType}; + + let input_map: HashMap = serde_json::json!({ + "bank_transfer": "ach,bacs", + "wallet": "paypal,venmo", + }) + .as_object() + .unwrap() + .iter() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect(); + let deserializer: MapDeserializer< + '_, + std::collections::hash_map::IntoIter, + ValueError, + > = input_map.into_deserializer(); + let result = deserialize_hashmap::<'_, _, PaymentMethod, PaymentMethodType>(deserializer); + let expected_result = HashMap::from([ + ( + PaymentMethod::BankTransfer, + HashSet::from([PaymentMethodType::Ach, PaymentMethodType::Bacs]), + ), + ( + PaymentMethod::Wallet, + HashSet::from([PaymentMethodType::Paypal, PaymentMethodType::Venmo]), + ), + ]); + + assert!(result.is_ok()); + assert_eq!(result.unwrap(), expected_result); + } + + #[test] + fn test_payment_method_and_payment_method_types_with_spaces() { + use diesel_models::enums::{PaymentMethod, PaymentMethodType}; + + let input_map: HashMap = serde_json::json!({ + " bank_transfer ": " ach , bacs ", + "wallet ": " paypal , pix , venmo ", + }) + .as_object() + .unwrap() + .iter() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect(); + let deserializer: MapDeserializer< + '_, + std::collections::hash_map::IntoIter, + ValueError, + > = input_map.into_deserializer(); + let result = deserialize_hashmap::<'_, _, PaymentMethod, PaymentMethodType>(deserializer); + let expected_result = HashMap::from([ + ( + PaymentMethod::BankTransfer, + HashSet::from([PaymentMethodType::Ach, PaymentMethodType::Bacs]), + ), + ( + PaymentMethod::Wallet, + HashSet::from([ + PaymentMethodType::Paypal, + PaymentMethodType::Pix, + PaymentMethodType::Venmo, + ]), + ), + ]); + + assert!(result.is_ok()); + assert_eq!(result.unwrap(), expected_result); + } + + #[test] + fn test_payment_method_deserializer_error() { + use diesel_models::enums::{PaymentMethod, PaymentMethodType}; + + let input_map: HashMap = serde_json::json!({ + "unknown": "ach,bacs", + "wallet": "paypal,unknown", + }) + .as_object() + .unwrap() + .iter() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect(); + let deserializer: MapDeserializer< + '_, + std::collections::hash_map::IntoIter, + ValueError, + > = input_map.into_deserializer(); + let result = deserialize_hashmap::<'_, _, PaymentMethod, PaymentMethodType>(deserializer); + + assert!(result.is_err()); + } +} + #[cfg(test)] mod hashset_deserialization_test { #![allow(clippy::unwrap_used)] diff --git a/crates/router/src/configs/validations.rs b/crates/router/src/configs/validations.rs index 168ae3181466..441172b05cca 100644 --- a/crates/router/src/configs/validations.rs +++ b/crates/router/src/configs/validations.rs @@ -189,12 +189,6 @@ impl super::settings::GenericLinkEnvConfig { Err(ApplicationError::InvalidConfigurationValueError( "link's expiry should not be 0".into(), )) - })?; - - when(self.sdk_url.is_empty(), || { - Err(ApplicationError::InvalidConfigurationValueError( - "sdk_url to be integrated in the link cannot be empty".into(), - )) }) } } diff --git a/crates/router/src/core/generic_link/payment_method_collect/initiate/styles.css b/crates/router/src/core/generic_link/payment_method_collect/initiate/styles.css index 01ca0f20c8bb..89387ab225ae 100644 --- a/crates/router/src/core/generic_link/payment_method_collect/initiate/styles.css +++ b/crates/router/src/core/generic_link/payment_method_collect/initiate/styles.css @@ -28,7 +28,7 @@ body { .main, #payment-method-collect { height: 100vh; - width: 100vw; + width: 100%; } .main { @@ -45,7 +45,6 @@ body { } .main { - width: auto; min-width: 300px; } } diff --git a/crates/router/src/core/generic_link/payout_link/initiate/styles.css b/crates/router/src/core/generic_link/payout_link/initiate/styles.css index 305281cd50a9..63d94724bccb 100644 --- a/crates/router/src/core/generic_link/payout_link/initiate/styles.css +++ b/crates/router/src/core/generic_link/payout_link/initiate/styles.css @@ -28,7 +28,7 @@ body { .main, #payout-link { height: 100vh; - width: 100vw; + width: 100%; } .main { @@ -45,7 +45,6 @@ body { } .main { - width: auto; min-width: 300px; } } diff --git a/crates/router/src/core/generic_link/payout_link/status/script.js b/crates/router/src/core/generic_link/payout_link/status/script.js index ba19f6bff9e6..829006a43114 100644 --- a/crates/router/src/core/generic_link/payout_link/status/script.js +++ b/crates/router/src/core/generic_link/payout_link/status/script.js @@ -76,6 +76,7 @@ function renderStatusDetails(payoutDetails) { case "success": break; case "initiated": + case "requires_fulfillment": case "pending": statusInfo.statusImageSrc = "https://live.hyperswitch.io/payment-link-assets/pending.png"; @@ -91,7 +92,6 @@ function renderStatusDetails(payoutDetails) { case "requires_creation": case "requires_confirmation": case "requires_payout_method_data": - case "requires_fulfillment": case "requires_vendor_account_creation": default: statusInfo.statusImageSrc = diff --git a/crates/router/src/core/payment_methods.rs b/crates/router/src/core/payment_methods.rs index faa28ffdbc85..baf013927bf1 100644 --- a/crates/router/src/core/payment_methods.rs +++ b/crates/router/src/core/payment_methods.rs @@ -12,6 +12,7 @@ use diesel_models::{ }; use error_stack::{report, ResultExt}; use hyperswitch_domain_models::payments::{payment_attempt::PaymentAttempt, PaymentIntent}; +use masking::PeekInterface; use router_env::{instrument, tracing}; use time::Duration; @@ -140,11 +141,17 @@ pub async fn initiate_pm_collect_link( )?; // 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: pm_collect_link.url, + 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, @@ -209,7 +216,7 @@ pub async fn render_pm_collect_link( 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 { + let ui_config_data = common_utils::link_utils::GenericLinkUiConfigFormData { merchant_name: link_data .ui_config .merchant_name @@ -290,7 +297,7 @@ pub async fn render_pm_collect_link( let generic_form_data = services::GenericLinkFormData { js_data: serialized_js_content, css_data: serialized_css_content, - sdk_url: default_config.sdk_url.clone(), + sdk_url: default_config.sdk_url.to_string(), html_meta_tags: String::new(), }; Ok(services::ApplicationResponse::GenericLinkForm(Box::new( @@ -305,7 +312,15 @@ pub async fn render_pm_collect_link( 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, + 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, }; diff --git a/crates/router/src/core/payment_methods/validator.rs b/crates/router/src/core/payment_methods/validator.rs index 19530f8944e9..be34d3db38c8 100644 --- a/crates/router/src/core/payment_methods/validator.rs +++ b/crates/router/src/core/payment_methods/validator.rs @@ -113,7 +113,7 @@ pub async fn validate_request_and_initiate_payment_method_collect_link( { let enabled_payment_method = link_utils::EnabledPaymentMethod { payment_method, - payment_method_types, + payment_method_types: payment_method_types.into_iter().collect(), }; default_enabled_payout_methods.push(enabled_payment_method); } diff --git a/crates/router/src/core/payout_link.rs b/crates/router/src/core/payout_link.rs index ec5935d0abed..0039b019868e 100644 --- a/crates/router/src/core/payout_link.rs +++ b/crates/router/src/core/payout_link.rs @@ -15,7 +15,7 @@ use crate::{ errors, routes::{app::StorageInterface, SessionState}, services::{self, GenericLinks}, - types::{api::enums, domain}, + types::domain, }; pub async fn initiate_payout_link( @@ -64,7 +64,7 @@ pub async fn initiate_payout_link( let link_data = payout_link.link_data.clone(); let default_config = &state.conf.generic_link.payout_link; let default_ui_config = default_config.ui_config.clone(); - let ui_config_data = link_utils::GenericLinkUIConfigFormData { + let ui_config_data = link_utils::GenericLinkUiConfigFormData { merchant_name: link_data .ui_config .merchant_name @@ -161,7 +161,13 @@ pub async fn initiate_payout_link( payout_id: payout_link.primary_reference, customer_id: customer.customer_id, session_expiry: payout_link.expiry, - return_url: payout_link.return_url, + return_url: payout_link + .return_url + .as_ref() + .map(|url| url::Url::parse(url)) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to parse payout status link's return URL")?, ui_config: ui_config_data, enabled_payment_methods, amount, @@ -181,7 +187,7 @@ pub async fn initiate_payout_link( let generic_form_data = services::GenericLinkFormData { js_data: serialized_js_content, css_data: serialized_css_content, - sdk_url: default_config.sdk_url.clone(), + sdk_url: default_config.sdk_url.to_string(), html_meta_tags: String::new(), }; Ok(services::ApplicationResponse::GenericLinkForm(Box::new( @@ -196,7 +202,13 @@ pub async fn initiate_payout_link( payout_id: payout_link.primary_reference, customer_id: link_data.customer_id, session_expiry: payout_link.expiry, - return_url: payout_link.return_url, + return_url: payout_link + .return_url + .as_ref() + .map(|url| url::Url::parse(url)) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to parse payout status link's return URL")?, status: payout.status, error_code: payout_attempt.error_code, error_message: payout_attempt.error_message, @@ -287,12 +299,10 @@ pub async fn filter_payout_methods( } } } - for (pm, method_types) in payment_method_list_hm { - if !method_types.is_empty() { - let payment_method_types: Vec = - method_types.into_iter().collect(); + for (payment_method, payment_method_types) in payment_method_list_hm { + if !payment_method_types.is_empty() { let enabled_payment_method = link_utils::EnabledPaymentMethod { - payment_method: pm, + payment_method, payment_method_types, }; response.push(enabled_payment_method); diff --git a/crates/router/src/core/payouts.rs b/crates/router/src/core/payouts.rs index e968f8bb6fda..f6f78094943c 100644 --- a/crates/router/src/core/payouts.rs +++ b/crates/router/src/core/payouts.rs @@ -20,6 +20,7 @@ use error_stack::{report, ResultExt}; use futures::future::join_all; #[cfg(feature = "olap")] use hyperswitch_domain_models::errors::StorageError; +use masking::PeekInterface; #[cfg(feature = "payout_retry")] use retry::GsmValidation; #[cfg(feature = "olap")] @@ -379,7 +380,6 @@ pub async fn payouts_confirm_core( storage_enums::PayoutStatus::Ineligible, storage_enums::PayoutStatus::RequiresFulfillment, storage_enums::PayoutStatus::RequiresVendorAccountCreation, - storage_enums::PayoutStatus::RequiresVendorAccountCreation, ], "confirm", )?; @@ -1946,10 +1946,16 @@ pub async fn response_handler( connector_transaction_id: payout_attempt.connector_payout_id, priority: payouts.priority, attempts: None, - payout_link: payout_link.map(|payout_link| PayoutLinkResponse { - payout_link_id: payout_link.link_id.clone(), - link: payout_link.url, - }), + payout_link: payout_link + .map(|payout_link| { + url::Url::parse(payout_link.url.peek()).map(|link| PayoutLinkResponse { + payout_link_id: payout_link.link_id, + link: link.into(), + }) + }) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to parse payout link's URL")?, }; Ok(services::ApplicationResponse::Json(response)) } diff --git a/crates/router/src/core/payouts/validator.rs b/crates/router/src/core/payouts/validator.rs index 5bd5d62b6a11..3e69216167de 100644 --- a/crates/router/src/core/payouts/validator.rs +++ b/crates/router/src/core/payouts/validator.rs @@ -251,7 +251,10 @@ pub async fn create_payout_link( .session_expiry .as_ref() .map_or(default_config.expiry, |expiry| *expiry); - let link = Secret::new(format!("{base_url}/payout_link/{merchant_id}/{payout_id}")); + let url = format!("{base_url}/payout_link/{merchant_id}/{payout_id}"); + let link = url::Url::parse(&url) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable_lazy(|| format!("Failed to form payout link URL - {}", url))?; let req_enabled_payment_methods = payout_link_config_req .as_ref() .and_then(|req| req.enabled_payment_methods.to_owned()); @@ -301,7 +304,7 @@ pub async fn create_payout_link_db_entry( link_type: common_enums::GenericLinkType::PayoutLink, link_status: GenericLinkStatus::PayoutLink(PayoutLinkStatus::Initiated), link_data, - url: payout_link_data.link.clone(), + url: payout_link_data.link.to_string().into(), return_url, expiry: common_utils::date_time::now() + Duration::seconds(payout_link_data.session_expiry.into()), diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 0cf7de4d33c2..15265d49661c 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -224,7 +224,11 @@ impl AppState { .await .expect("Failed to create secret management client"); - let conf = secrets_transformers::fetch_raw_secrets(conf, &*secret_management_client).await; + let conf = Box::pin(secrets_transformers::fetch_raw_secrets( + conf, + &*secret_management_client, + )) + .await; #[allow(clippy::expect_used)] let encryption_client = conf