refactor: use hashmap deserializer for generic_link options (#5157)

Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
Kashif
2024-07-02 15:16:20 +05:30
committed by GitHub
parent 3bbdfb5a1c
commit a343f69dc4
23 changed files with 310 additions and 90 deletions

View File

@ -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<enums::PaymentMethod, Vec<enums::PaymentMethodType>>,
#[serde(deserialize_with = "deserialize_hashmap")]
pub enabled_payment_methods: HashMap<enums::PaymentMethod, HashSet<enums::PaymentMethodType>>,
}
#[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<String>,
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<SecuredSecret> {
.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<K, V>(
value: HashMap<String, String>,
) -> Result<HashMap<K, HashSet<V>>, String>
where
K: Eq + std::str::FromStr + std::hash::Hash,
V: Eq + std::str::FromStr + std::hash::Hash,
<K as std::str::FromStr>::Err: std::fmt::Display,
<V as std::str::FromStr>::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::<K>()
)),
(_, 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<HashMap<K, HashSet<V>>, D::Error>
where
D: serde::Deserializer<'a>,
K: Eq + std::str::FromStr + std::hash::Hash,
V: Eq + std::str::FromStr + std::hash::Hash,
<K as std::str::FromStr>::Err: std::fmt::Display,
<V as std::str::FromStr>::Err: std::fmt::Display,
{
use serde::de::Error;
deserialize_hashmap_inner(<HashMap<String, String>>::deserialize(deserializer)?)
.map_err(D::Error::custom)
}
fn deserialize_hashset_inner<T>(value: impl AsRef<str>) -> Result<HashSet<T>, 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<String, String> = 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<String, String>,
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<String, String> = 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<String, String>,
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<String, String> = 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<String, String>,
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)]

View File

@ -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(),
))
})
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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 =

View File

@ -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;
@ -145,11 +146,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,
@ -214,7 +221,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
@ -295,7 +302,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(
@ -310,7 +317,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,
};

View File

@ -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);
}

View File

@ -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<enums::PaymentMethodType> =
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);

View File

@ -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))
}

View File

@ -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()),

View File

@ -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