diff --git a/Cargo.lock b/Cargo.lock index 8249ae4f70..2c33920066 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2022,6 +2022,7 @@ dependencies = [ "masking", "md5", "nanoid", + "nutype", "once_cell", "phonenumber", "proptest", @@ -4249,6 +4250,27 @@ dependencies = [ "thiserror", ] +[[package]] +name = "kinded" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce4bdbb2f423660b19f0e9f7115182214732d8dd5f840cd0a3aee3e22562f34c" +dependencies = [ + "kinded_macros", +] + +[[package]] +name = "kinded_macros" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13b4ddc5dcb32f45dac3d6f606da2a52fdb9964a18427e63cd5ef6c0d13288d" +dependencies = [ + "convert_case 0.6.0", + "proc-macro2", + "quote", + "syn 2.0.57", +] + [[package]] name = "language-tags" version = "0.3.2" @@ -4795,6 +4817,29 @@ dependencies = [ "libc", ] +[[package]] +name = "nutype" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "801187d4ee2f03db47daf0f5fc335a7b1b94f60f47942293060b762641b83f2e" +dependencies = [ + "nutype_macros", +] + +[[package]] +name = "nutype_macros" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e96467936d36285839340d692fcd974106d9bc203e36f55a477e0243737a8af7" +dependencies = [ + "cfg-if 1.0.0", + "kinded", + "proc-macro2", + "quote", + "syn 2.0.57", + "urlencoding", +] + [[package]] name = "oauth2" version = "4.4.2" @@ -4857,6 +4902,7 @@ version = "0.1.0" dependencies = [ "api_models", "common_utils", + "router_env", "serde_json", "utoipa", ] @@ -6076,7 +6122,6 @@ dependencies = [ "nanoid", "num_cpus", "once_cell", - "openapi", "openidconnect", "openssl", "pm_auth", diff --git a/api-reference/openapi_spec.json b/api-reference/openapi_spec.json index 04a1083891..c75cabb767 100644 --- a/api-reference/openapi_spec.json +++ b/api-reference/openapi_spec.json @@ -11105,7 +11105,8 @@ "type": "string", "description": "The identifier for the Merchant Account", "example": "y3oqhf46pyzuxjbcn2giaqnb44", - "maxLength": 255 + "maxLength": 64, + "minLength": 1 }, "merchant_name": { "type": "string", @@ -11179,7 +11180,7 @@ }, "metadata": { "type": "object", - "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object.", + "description": "Metadata is useful for storing additional, unstructured information on an object", "nullable": true }, "publishable_key": { @@ -11209,7 +11210,7 @@ }, "organization_id": { "type": "string", - "description": "The id of the organization to which the merchant belongs to", + "description": "The id of the organization to which the merchant belongs to, if not passed an organization is created", "nullable": true }, "pm_collect_link_config": { @@ -11259,7 +11260,7 @@ "type": "string", "description": "The identifier for the Merchant Account", "example": "y3oqhf46pyzuxjbcn2giaqnb44", - "maxLength": 255 + "maxLength": 64 }, "merchant_name": { "type": "string", @@ -11339,7 +11340,7 @@ }, "metadata": { "type": "object", - "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object.", + "description": "Metadata is useful for storing additional, unstructured information on an object.", "nullable": true }, "locker_id": { diff --git a/crates/analytics/src/active_payments/core.rs b/crates/analytics/src/active_payments/core.rs index 772de43c35..b5ca3e2d0b 100644 --- a/crates/analytics/src/active_payments/core.rs +++ b/crates/analytics/src/active_payments/core.rs @@ -19,8 +19,8 @@ use crate::{ #[instrument(skip_all)] pub async fn get_metrics( pool: &AnalyticsProvider, - publishable_key: Option<&String>, - merchant_id: Option<&String>, + publishable_key: &String, + merchant_id: &String, req: GetActivePaymentsMetricRequest, ) -> AnalyticsResult> { let mut metrics_accumulator: HashMap< @@ -28,80 +28,60 @@ pub async fn get_metrics( ActivePaymentsMetricsAccumulator, > = HashMap::new(); - if let Some(publishable_key) = publishable_key { - if let Some(merchant_id) = merchant_id { - let mut set = tokio::task::JoinSet::new(); - for metric_type in req.metrics.iter().cloned() { - let publishable_key_scoped = publishable_key.to_owned(); - let merchant_id_scoped = merchant_id.to_owned(); - let pool = pool.clone(); - set.spawn(async move { - let data = pool - .get_active_payments_metrics( - &metric_type, - &merchant_id_scoped, - &publishable_key_scoped, - &req.time_range, - ) - .await - .change_context(AnalyticsError::UnknownError); - (metric_type, data) - }); - } - - while let Some((metric, data)) = set - .join_next() + let mut set = tokio::task::JoinSet::new(); + for metric_type in req.metrics.iter().cloned() { + let publishable_key_scoped = publishable_key.to_owned(); + let merchant_id_scoped = merchant_id.to_owned(); + let pool = pool.clone(); + set.spawn(async move { + let data = pool + .get_active_payments_metrics( + &metric_type, + &merchant_id_scoped, + &publishable_key_scoped, + &req.time_range, + ) .await - .transpose() - .change_context(AnalyticsError::UnknownError)? - { - logger::info!("Logging metric: {metric} Result: {:?}", data); - for (id, value) in data? { - let metrics_builder = metrics_accumulator.entry(id).or_default(); - match metric { - ActivePaymentsMetrics::ActivePayments => { - metrics_builder.active_payments.add_metrics_bucket(&value) - } - } - } - - logger::debug!( - "Analytics Accumulated Results: metric: {}, results: {:#?}", - metric, - metrics_accumulator - ); - } - - let query_data: Vec = metrics_accumulator - .into_iter() - .map(|(id, val)| MetricsBucketResponse { - values: val.collect(), - dimensions: id, - }) - .collect(); - - Ok(MetricsResponse { - query_data, - meta_data: [AnalyticsMetadata { - current_time_range: req.time_range, - }], - }) - } else { - logger::error!("Merchant ID not present"); - Ok(MetricsResponse { - query_data: vec![], - meta_data: [AnalyticsMetadata { - current_time_range: req.time_range, - }], - }) - } - } else { - logger::error!("Publishable key not present for merchant ID"); - Ok(MetricsResponse { - query_data: vec![], - meta_data: [AnalyticsMetadata { - current_time_range: req.time_range, - }], - }) + .change_context(AnalyticsError::UnknownError); + (metric_type, data) + }); } + + while let Some((metric, data)) = set + .join_next() + .await + .transpose() + .change_context(AnalyticsError::UnknownError)? + { + logger::info!("Logging metric: {metric} Result: {:?}", data); + for (id, value) in data? { + let metrics_builder = metrics_accumulator.entry(id).or_default(); + match metric { + ActivePaymentsMetrics::ActivePayments => { + metrics_builder.active_payments.add_metrics_bucket(&value) + } + } + } + + logger::debug!( + "Analytics Accumulated Results: metric: {}, results: {:#?}", + metric, + metrics_accumulator + ); + } + + let query_data: Vec = metrics_accumulator + .into_iter() + .map(|(id, val)| MetricsBucketResponse { + values: val.collect(), + dimensions: id, + }) + .collect(); + + Ok(MetricsResponse { + query_data, + meta_data: [AnalyticsMetadata { + current_time_range: req.time_range, + }], + }) } diff --git a/crates/analytics/src/auth_events/core.rs b/crates/analytics/src/auth_events/core.rs index 5ee34d7a36..e50a6aaf53 100644 --- a/crates/analytics/src/auth_events/core.rs +++ b/crates/analytics/src/auth_events/core.rs @@ -5,7 +5,7 @@ use api_models::analytics::{ AnalyticsMetadata, GetAuthEventMetricRequest, MetricsResponse, }; use error_stack::ResultExt; -use router_env::{instrument, logger, tracing}; +use router_env::{instrument, tracing}; use super::AuthEventMetricsAccumulator; use crate::{ @@ -18,7 +18,7 @@ use crate::{ pub async fn get_metrics( pool: &AnalyticsProvider, merchant_id: &String, - publishable_key: Option<&String>, + publishable_key: &String, req: GetAuthEventMetricRequest, ) -> AnalyticsResult> { let mut metrics_accumulator: HashMap< @@ -26,86 +26,76 @@ pub async fn get_metrics( AuthEventMetricsAccumulator, > = HashMap::new(); - if let Some(publishable_key) = publishable_key { - let mut set = tokio::task::JoinSet::new(); - for metric_type in req.metrics.iter().cloned() { - let req = req.clone(); - let merchant_id_scoped = merchant_id.to_owned(); - let publishable_key_scoped = publishable_key.to_owned(); - let pool = pool.clone(); - set.spawn(async move { - let data = pool - .get_auth_event_metrics( - &metric_type, - &merchant_id_scoped, - &publishable_key_scoped, - &req.time_series.map(|t| t.granularity), - &req.time_range, - ) - .await - .change_context(AnalyticsError::UnknownError); - (metric_type, data) - }); - } + let mut set = tokio::task::JoinSet::new(); + for metric_type in req.metrics.iter().cloned() { + let req = req.clone(); + let merchant_id_scoped = merchant_id.to_owned(); + let publishable_key_scoped = publishable_key.to_owned(); + let pool = pool.clone(); + set.spawn(async move { + let data = pool + .get_auth_event_metrics( + &metric_type, + &merchant_id_scoped, + &publishable_key_scoped, + &req.time_series.map(|t| t.granularity), + &req.time_range, + ) + .await + .change_context(AnalyticsError::UnknownError); + (metric_type, data) + }); + } - while let Some((metric, data)) = set - .join_next() - .await - .transpose() - .change_context(AnalyticsError::UnknownError)? - { - for (id, value) in data? { - let metrics_builder = metrics_accumulator.entry(id).or_default(); - match metric { - AuthEventMetrics::ThreeDsSdkCount => metrics_builder - .three_ds_sdk_count - .add_metrics_bucket(&value), - AuthEventMetrics::AuthenticationAttemptCount => metrics_builder - .authentication_attempt_count - .add_metrics_bucket(&value), - AuthEventMetrics::AuthenticationSuccessCount => metrics_builder - .authentication_success_count - .add_metrics_bucket(&value), - AuthEventMetrics::ChallengeFlowCount => metrics_builder - .challenge_flow_count - .add_metrics_bucket(&value), - AuthEventMetrics::ChallengeAttemptCount => metrics_builder - .challenge_attempt_count - .add_metrics_bucket(&value), - AuthEventMetrics::ChallengeSuccessCount => metrics_builder - .challenge_success_count - .add_metrics_bucket(&value), - AuthEventMetrics::FrictionlessFlowCount => metrics_builder - .frictionless_flow_count - .add_metrics_bucket(&value), - AuthEventMetrics::FrictionlessSuccessCount => metrics_builder - .frictionless_success_count - .add_metrics_bucket(&value), - } + while let Some((metric, data)) = set + .join_next() + .await + .transpose() + .change_context(AnalyticsError::UnknownError)? + { + for (id, value) in data? { + let metrics_builder = metrics_accumulator.entry(id).or_default(); + match metric { + AuthEventMetrics::ThreeDsSdkCount => metrics_builder + .three_ds_sdk_count + .add_metrics_bucket(&value), + AuthEventMetrics::AuthenticationAttemptCount => metrics_builder + .authentication_attempt_count + .add_metrics_bucket(&value), + AuthEventMetrics::AuthenticationSuccessCount => metrics_builder + .authentication_success_count + .add_metrics_bucket(&value), + AuthEventMetrics::ChallengeFlowCount => metrics_builder + .challenge_flow_count + .add_metrics_bucket(&value), + AuthEventMetrics::ChallengeAttemptCount => metrics_builder + .challenge_attempt_count + .add_metrics_bucket(&value), + AuthEventMetrics::ChallengeSuccessCount => metrics_builder + .challenge_success_count + .add_metrics_bucket(&value), + AuthEventMetrics::FrictionlessFlowCount => metrics_builder + .frictionless_flow_count + .add_metrics_bucket(&value), + AuthEventMetrics::FrictionlessSuccessCount => metrics_builder + .frictionless_success_count + .add_metrics_bucket(&value), } } - - let query_data: Vec = metrics_accumulator - .into_iter() - .map(|(id, val)| MetricsBucketResponse { - values: val.collect(), - dimensions: id, - }) - .collect(); - - Ok(MetricsResponse { - query_data, - meta_data: [AnalyticsMetadata { - current_time_range: req.time_range, - }], - }) - } else { - logger::error!("Publishable key not present for merchant ID"); - Ok(MetricsResponse { - query_data: vec![], - meta_data: [AnalyticsMetadata { - current_time_range: req.time_range, - }], - }) } + + let query_data: Vec = metrics_accumulator + .into_iter() + .map(|(id, val)| MetricsBucketResponse { + values: val.collect(), + dimensions: id, + }) + .collect(); + + Ok(MetricsResponse { + query_data, + meta_data: [AnalyticsMetadata { + current_time_range: req.time_range, + }], + }) } diff --git a/crates/analytics/src/sdk_events/core.rs b/crates/analytics/src/sdk_events/core.rs index 1dcf4143aa..51709f37e4 100644 --- a/crates/analytics/src/sdk_events/core.rs +++ b/crates/analytics/src/sdk_events/core.rs @@ -26,17 +26,17 @@ use crate::{ pub async fn sdk_events_core( pool: &AnalyticsProvider, req: SdkEventsRequest, - publishable_key: String, + publishable_key: &str, ) -> AnalyticsResult> { match pool { AnalyticsProvider::Sqlx(_) => Err(FiltersError::NotImplemented( "SDK Events not implemented for SQLX", )) .attach_printable("SQL Analytics is not implemented for Sdk Events"), - AnalyticsProvider::Clickhouse(pool) => get_sdk_event(&publishable_key, req, pool).await, + AnalyticsProvider::Clickhouse(pool) => get_sdk_event(publishable_key, req, pool).await, AnalyticsProvider::CombinedSqlx(_sqlx_pool, ckh_pool) | AnalyticsProvider::CombinedCkh(_sqlx_pool, ckh_pool) => { - get_sdk_event(&publishable_key, req, ckh_pool).await + get_sdk_event(publishable_key, req, ckh_pool).await } } .switch() @@ -45,7 +45,7 @@ pub async fn sdk_events_core( #[instrument(skip_all)] pub async fn get_metrics( pool: &AnalyticsProvider, - publishable_key: Option<&String>, + publishable_key: &String, req: GetSdkEventMetricRequest, ) -> AnalyticsResult> { let mut metrics_accumulator: HashMap< @@ -53,102 +53,90 @@ pub async fn get_metrics( SdkEventMetricsAccumulator, > = HashMap::new(); - if let Some(publishable_key) = publishable_key { - let mut set = tokio::task::JoinSet::new(); - for metric_type in req.metrics.iter().cloned() { - let req = req.clone(); - let publishable_key_scoped = publishable_key.to_owned(); - let pool = pool.clone(); - set.spawn(async move { - let data = pool - .get_sdk_event_metrics( - &metric_type, - &req.group_by_names.clone(), - &publishable_key_scoped, - &req.filters, - &req.time_series.map(|t| t.granularity), - &req.time_range, - ) - .await - .change_context(AnalyticsError::UnknownError); - (metric_type, data) - }); - } - - while let Some((metric, data)) = set - .join_next() - .await - .transpose() - .change_context(AnalyticsError::UnknownError)? - { - logger::info!("Logging Result {:?}", data); - for (id, value) in data? { - let metrics_builder = metrics_accumulator.entry(id).or_default(); - match metric { - SdkEventMetrics::PaymentAttempts => { - metrics_builder.payment_attempts.add_metrics_bucket(&value) - } - SdkEventMetrics::PaymentMethodsCallCount => metrics_builder - .payment_methods_call_count - .add_metrics_bucket(&value), - SdkEventMetrics::SdkRenderedCount => metrics_builder - .sdk_rendered_count - .add_metrics_bucket(&value), - SdkEventMetrics::SdkInitiatedCount => metrics_builder - .sdk_initiated_count - .add_metrics_bucket(&value), - SdkEventMetrics::PaymentMethodSelectedCount => metrics_builder - .payment_method_selected_count - .add_metrics_bucket(&value), - SdkEventMetrics::PaymentDataFilledCount => metrics_builder - .payment_data_filled_count - .add_metrics_bucket(&value), - SdkEventMetrics::AveragePaymentTime => metrics_builder - .average_payment_time - .add_metrics_bucket(&value), - SdkEventMetrics::LoadTime => { - metrics_builder.load_time.add_metrics_bucket(&value) - } - } - } - - logger::debug!( - "Analytics Accumulated Results: metric: {}, results: {:#?}", - metric, - metrics_accumulator - ); - } - - let query_data: Vec = metrics_accumulator - .into_iter() - .map(|(id, val)| MetricsBucketResponse { - values: val.collect(), - dimensions: id, - }) - .collect(); - - Ok(MetricsResponse { - query_data, - meta_data: [AnalyticsMetadata { - current_time_range: req.time_range, - }], - }) - } else { - logger::error!("Publishable key not present for merchant ID"); - Ok(MetricsResponse { - query_data: vec![], - meta_data: [AnalyticsMetadata { - current_time_range: req.time_range, - }], - }) + let mut set = tokio::task::JoinSet::new(); + for metric_type in req.metrics.iter().cloned() { + let req = req.clone(); + let publishable_key_scoped = publishable_key.to_owned(); + let pool = pool.clone(); + set.spawn(async move { + let data = pool + .get_sdk_event_metrics( + &metric_type, + &req.group_by_names.clone(), + &publishable_key_scoped, + &req.filters, + &req.time_series.map(|t| t.granularity), + &req.time_range, + ) + .await + .change_context(AnalyticsError::UnknownError); + (metric_type, data) + }); } + + while let Some((metric, data)) = set + .join_next() + .await + .transpose() + .change_context(AnalyticsError::UnknownError)? + { + logger::info!("Logging Result {:?}", data); + for (id, value) in data? { + let metrics_builder = metrics_accumulator.entry(id).or_default(); + match metric { + SdkEventMetrics::PaymentAttempts => { + metrics_builder.payment_attempts.add_metrics_bucket(&value) + } + SdkEventMetrics::PaymentMethodsCallCount => metrics_builder + .payment_methods_call_count + .add_metrics_bucket(&value), + SdkEventMetrics::SdkRenderedCount => metrics_builder + .sdk_rendered_count + .add_metrics_bucket(&value), + SdkEventMetrics::SdkInitiatedCount => metrics_builder + .sdk_initiated_count + .add_metrics_bucket(&value), + SdkEventMetrics::PaymentMethodSelectedCount => metrics_builder + .payment_method_selected_count + .add_metrics_bucket(&value), + SdkEventMetrics::PaymentDataFilledCount => metrics_builder + .payment_data_filled_count + .add_metrics_bucket(&value), + SdkEventMetrics::AveragePaymentTime => metrics_builder + .average_payment_time + .add_metrics_bucket(&value), + SdkEventMetrics::LoadTime => metrics_builder.load_time.add_metrics_bucket(&value), + } + } + + logger::debug!( + "Analytics Accumulated Results: metric: {}, results: {:#?}", + metric, + metrics_accumulator + ); + } + + let query_data: Vec = metrics_accumulator + .into_iter() + .map(|(id, val)| MetricsBucketResponse { + values: val.collect(), + dimensions: id, + }) + .collect(); + + Ok(MetricsResponse { + query_data, + meta_data: [AnalyticsMetadata { + current_time_range: req.time_range, + }], + }) } #[allow(dead_code)] pub async fn get_filters( pool: &AnalyticsProvider, req: GetSdkEventFiltersRequest, - publishable_key: Option<&String>, + publishable_key: &String, ) -> AnalyticsResult { use api_models::analytics::{sdk_events::SdkEventDimensions, SdkEventFilterValue}; @@ -157,46 +145,37 @@ pub async fn get_filters( let mut res = SdkEventFiltersResponse::default(); - if let Some(publishable_key) = publishable_key { - for dim in req.group_by_names { - let values = match pool { - AnalyticsProvider::Sqlx(_pool) => Err(FiltersError::NotImplemented( - "SDK Events not implemented for SQLX", - )) - .attach_printable("SQL Analytics is not implemented for SDK Events"), - AnalyticsProvider::Clickhouse(pool) => { - get_sdk_event_filter_for_dimension(dim, publishable_key, &req.time_range, pool) - .await - } - AnalyticsProvider::CombinedSqlx(_sqlx_pool, ckh_pool) - | AnalyticsProvider::CombinedCkh(_sqlx_pool, ckh_pool) => { - get_sdk_event_filter_for_dimension( - dim, - publishable_key, - &req.time_range, - ckh_pool, - ) + for dim in req.group_by_names { + let values = match pool { + AnalyticsProvider::Sqlx(_pool) => Err(FiltersError::NotImplemented( + "SDK Events not implemented for SQLX", + )) + .attach_printable("SQL Analytics is not implemented for SDK Events"), + AnalyticsProvider::Clickhouse(pool) => { + get_sdk_event_filter_for_dimension(dim, publishable_key, &req.time_range, pool) + .await + } + AnalyticsProvider::CombinedSqlx(_sqlx_pool, ckh_pool) + | AnalyticsProvider::CombinedCkh(_sqlx_pool, ckh_pool) => { + get_sdk_event_filter_for_dimension(dim, publishable_key, &req.time_range, ckh_pool) .await - } } - .change_context(AnalyticsError::UnknownError)? - .into_iter() - .filter_map(|fil: SdkEventFilter| match dim { - SdkEventDimensions::PaymentMethod => fil.payment_method, - SdkEventDimensions::Platform => fil.platform, - SdkEventDimensions::BrowserName => fil.browser_name, - SdkEventDimensions::Source => fil.source, - SdkEventDimensions::Component => fil.component, - SdkEventDimensions::PaymentExperience => fil.payment_experience, - }) - .collect::>(); - res.query_data.push(SdkEventFilterValue { - dimension: dim, - values, - }) } - } else { - router_env::logger::error!("Publishable key not found for merchant"); + .change_context(AnalyticsError::UnknownError)? + .into_iter() + .filter_map(|fil: SdkEventFilter| match dim { + SdkEventDimensions::PaymentMethod => fil.payment_method, + SdkEventDimensions::Platform => fil.platform, + SdkEventDimensions::BrowserName => fil.browser_name, + SdkEventDimensions::Source => fil.source, + SdkEventDimensions::Component => fil.component, + SdkEventDimensions::PaymentExperience => fil.payment_experience, + }) + .collect::>(); + res.query_data.push(SdkEventFilterValue { + dimension: dim, + values, + }) } Ok(res) diff --git a/crates/api_models/Cargo.toml b/crates/api_models/Cargo.toml index 80737cfbc4..9df13c85a8 100644 --- a/crates/api_models/Cargo.toml +++ b/crates/api_models/Cargo.toml @@ -21,6 +21,7 @@ frm = [] olap = [] openapi = ["common_enums/openapi", "olap", "backwards_compatibility", "business_profile_routing", "connector_choice_mca_id", "recon", "dummy_connector", "olap"] recon = [] +v2 = [] [dependencies] actix-web = { version = "4.5.1", optional = true } diff --git a/crates/api_models/src/admin.rs b/crates/api_models/src/admin.rs index cb268262fb..00efae6f71 100644 --- a/crates/api_models/src/admin.rs +++ b/crates/api_models/src/admin.rs @@ -1,39 +1,46 @@ use std::collections::HashMap; +#[cfg(feature = "v2")] +use common_utils::new_type; use common_utils::{ consts, - crypto::{Encryptable, OptionalEncryptableName}, - link_utils, pii, + crypto::Encryptable, + errors::{self, CustomResult}, + ext_traits::Encode, + id_type, link_utils, pii, }; +#[cfg(not(feature = "v2"))] +use common_utils::{crypto::OptionalEncryptableName, ext_traits::ValueExt}; +#[cfg(feature = "v2")] +use masking::ExposeInterface; use masking::Secret; use serde::{Deserialize, Serialize}; use url; use utoipa::ToSchema; use super::payments::AddressDetails; -use crate::{ - enums, - enums::{self as api_enums}, - payment_methods, -}; +#[cfg(not(feature = "v2"))] +use crate::routing; +use crate::{enums as api_enums, payment_methods}; #[derive(Clone, Debug, Deserialize, ToSchema, Serialize)] pub struct MerchantAccountListRequest { pub organization_id: String, } +#[cfg(not(feature = "v2"))] #[derive(Clone, Debug, Deserialize, ToSchema, Serialize)] #[serde(deny_unknown_fields)] pub struct MerchantAccountCreate { /// The identifier for the Merchant Account - #[schema(max_length = 255, example = "y3oqhf46pyzuxjbcn2giaqnb44")] - pub merchant_id: String, + #[schema(value_type = String, max_length = 64, min_length = 1, example = "y3oqhf46pyzuxjbcn2giaqnb44")] + pub merchant_id: id_type::MerchantId, /// Name of the Merchant Account #[schema(value_type= Option,example = "NewAge Retailer")] pub merchant_name: Option>, - /// Details about the merchant + /// Details about the merchant, can contain phone and emails of primary and secondary contact person pub merchant_details: Option, /// The URL to redirect after the completion of the operation @@ -72,7 +79,7 @@ pub struct MerchantAccountCreate { #[schema(default = false, example = true)] pub redirect_to_merchant_with_http_post: Option, - /// You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object. + /// Metadata is useful for storing additional, unstructured information on an object #[schema(value_type = Option, example = r#"{ "city": "NY", "unit": "245" }"#)] pub metadata: Option, @@ -93,7 +100,7 @@ pub struct MerchantAccountCreate { #[schema(value_type = Option,example = json!({"type": "single", "data": "signifyd"}))] pub frm_routing_algorithm: Option, - /// The id of the organization to which the merchant belongs to + /// The id of the organization to which the merchant belongs to, if not passed an organization is created pub organization_id: Option, /// Default payment method collect link config @@ -101,11 +108,135 @@ pub struct MerchantAccountCreate { pub pm_collect_link_config: Option, } +#[cfg(not(feature = "v2"))] +impl MerchantAccountCreate { + pub fn get_merchant_reference_id(&self) -> id_type::MerchantId { + self.merchant_id.clone() + } + + pub fn get_payment_response_hash_key(&self) -> Option { + self.payment_response_hash_key.clone().or(Some( + common_utils::crypto::generate_cryptographically_secure_random_string(64), + )) + } + + pub fn get_primary_details_as_value( + &self, + ) -> CustomResult { + self.primary_business_details + .clone() + .unwrap_or_default() + .encode_to_value() + } + + pub fn get_pm_link_config_as_value( + &self, + ) -> CustomResult, errors::ParsingError> { + self.pm_collect_link_config + .as_ref() + .map(|pm_collect_link_config| pm_collect_link_config.encode_to_value()) + .transpose() + } + + pub fn get_merchant_details_as_secret( + &self, + ) -> CustomResult, errors::ParsingError> { + self.merchant_details + .as_ref() + .map(|merchant_details| merchant_details.encode_to_value().map(Secret::new)) + .transpose() + } + + pub fn get_metadata_as_secret( + &self, + ) -> CustomResult, errors::ParsingError> { + self.metadata + .as_ref() + .map(|metadata| metadata.encode_to_value().map(Secret::new)) + .transpose() + } + + pub fn get_webhook_details_as_value( + &self, + ) -> CustomResult, errors::ParsingError> { + self.webhook_details + .as_ref() + .map(|webhook_details| webhook_details.encode_to_value()) + .transpose() + } + + pub fn parse_routing_algorithm(&self) -> CustomResult<(), errors::ParsingError> { + match self.routing_algorithm { + Some(ref routing_algorithm) => { + let _: routing::RoutingAlgorithm = + routing_algorithm.clone().parse_value("RoutingAlgorithm")?; + Ok(()) + } + None => Ok(()), + } + } + + // Get the enable payment response hash as a boolean, where the default value is true + pub fn get_enable_payment_response_hash(&self) -> bool { + self.enable_payment_response_hash.unwrap_or(true) + } +} + +#[cfg(feature = "v2")] +#[derive(Clone, Debug, Deserialize, ToSchema, Serialize)] +#[serde(deny_unknown_fields)] +pub struct MerchantAccountCreate { + /// Name of the Merchant Account, This will be used as a prefix to generate the id + #[schema(value_type= String, max_length = 64, example = "NewAge Retailer")] + pub merchant_name: Secret, + + /// Details about the merchant, contains phone and emails of primary and secondary contact person. + pub merchant_details: Option, + + /// Metadata is useful for storing additional, unstructured information about the merchant account. + #[schema(value_type = Option, example = r#"{ "city": "NY", "unit": "245" }"#)] + pub metadata: Option, + + /// The id of the organization to which the merchant belongs to. Please use the organization endpoint to create an organization + pub organization_id: String, +} + +#[cfg(feature = "v2")] +impl MerchantAccountCreate { + pub fn get_merchant_reference_id(&self) -> id_type::MerchantId { + id_type::MerchantId::from_merchant_name(self.merchant_name.clone().expose()) + } + + pub fn get_merchant_details_as_secret( + &self, + ) -> CustomResult, errors::ParsingError> { + self.merchant_details + .as_ref() + .map(|merchant_details| merchant_details.encode_to_value().map(Secret::new)) + .transpose() + } + + pub fn get_metadata_as_secret( + &self, + ) -> CustomResult, errors::ParsingError> { + self.metadata + .as_ref() + .map(|metadata| metadata.encode_to_value().map(Secret::new)) + .transpose() + } + + pub fn get_primary_details_as_value( + &self, + ) -> CustomResult { + Vec::::new().encode_to_value() + } +} + #[derive(Clone, Debug, Deserialize, Serialize, ToSchema)] pub struct AuthenticationConnectorDetails { /// List of authentication connectors #[schema(value_type = Vec)] - pub authentication_connectors: Vec, + pub authentication_connectors: Vec, /// URL of the (customer service) website that will be shown to the shopper in case of technical errors during the 3D Secure 2 process. pub three_ds_requestor_url: String, } @@ -196,10 +327,11 @@ pub struct MerchantAccountUpdate { pub pm_collect_link_config: Option, } +#[cfg(not(feature = "v2"))] #[derive(Clone, Debug, ToSchema, Serialize)] pub struct MerchantAccountResponse { /// The identifier for the Merchant Account - #[schema(max_length = 255, example = "y3oqhf46pyzuxjbcn2giaqnb44")] + #[schema(max_length = 64, example = "y3oqhf46pyzuxjbcn2giaqnb44")] pub merchant_id: String, /// Name of the Merchant Account @@ -252,7 +384,7 @@ pub struct MerchantAccountResponse { #[schema(example = "AH3423bkjbkjdsfbkj")] pub publishable_key: Option, - /// You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object. + /// Metadata is useful for storing additional, unstructured information on an object. #[schema(value_type = Option, example = r#"{ "city": "NY", "unit": "245" }"#)] pub metadata: Option, @@ -280,13 +412,47 @@ pub struct MerchantAccountResponse { /// Used to indicate the status of the recon module for a merchant account #[schema(value_type = ReconStatus, example = "not_requested")] - pub recon_status: enums::ReconStatus, + pub recon_status: api_enums::ReconStatus, /// Default payment method collect link config #[schema(value_type = Option)] pub pm_collect_link_config: Option, } +#[cfg(feature = "v2")] +#[derive(Clone, Debug, ToSchema, Serialize)] +pub struct MerchantAccountResponse { + /// The identifier for the Merchant Account + #[schema(max_length = 64, example = "y3oqhf46pyzuxjbcn2giaqnb44")] + pub id: String, + + /// Name of the Merchant Account + #[schema(value_type = String,example = "NewAge Retailer")] + pub merchant_name: Secret, + + /// Details about the merchant + #[schema(value_type = Option)] + pub merchant_details: Option>, + + /// API key that will be used for server side API access + #[schema(example = "AH3423bkjbkjdsfbkj")] + pub publishable_key: String, + + /// Metadata is useful for storing additional, unstructured information on an object. + #[schema(value_type = Option, example = r#"{ "city": "NY", "unit": "245" }"#)] + pub metadata: Option, + + /// The id of the organization which the merchant is associated with + pub organization_id: String, + + /// A boolean value to indicate if the merchant has recon service is enabled or not, by default value is false + pub is_recon_enabled: bool, + + /// Used to indicate the status of the recon module for a merchant account + #[schema(value_type = ReconStatus, example = "not_requested")] + pub recon_status: api_enums::ReconStatus, +} + #[derive(Clone, Debug, Deserialize, ToSchema, Serialize)] #[serde(deny_unknown_fields)] pub struct MerchantDetails { diff --git a/crates/common_utils/Cargo.toml b/crates/common_utils/Cargo.toml index 568dc00a97..4595321070 100644 --- a/crates/common_utils/Cargo.toml +++ b/crates/common_utils/Cargo.toml @@ -53,6 +53,7 @@ common_enums = { version = "0.1.0", path = "../common_enums" } masking = { version = "0.1.0", path = "../masking" } router_derive = { version = "0.1.0", path = "../router_derive" } router_env = { version = "0.1.0", path = "../router_env", features = ["log_extra_implicit_fields", "log_custom_entries_to_extra"], optional = true } +nutype = { version = "0.4.2", features = ["serde"] } [target.'cfg(not(target_os = "windows"))'.dependencies] signal-hook-tokio = { version = "0.3.1", features = ["futures-v0_3"], optional = true } diff --git a/crates/common_utils/src/consts.rs b/crates/common_utils/src/consts.rs index efb60149c0..a9229c7402 100644 --- a/crates/common_utils/src/consts.rs +++ b/crates/common_utils/src/consts.rs @@ -98,3 +98,6 @@ pub const MAX_ALLOWED_MERCHANT_REFERENCE_ID_LENGTH: u8 = 64; /// Minimum allowed length for MerchantReferenceId pub const MIN_REQUIRED_MERCHANT_REFERENCE_ID_LENGTH: u8 = 1; + +/// Maximum allowed length for MerchantName +pub const MAX_ALLOWED_MERCHANT_NAME_LENGTH: usize = 64; diff --git a/crates/common_utils/src/id_type.rs b/crates/common_utils/src/id_type.rs index 5edbed799f..2e6764d355 100644 --- a/crates/common_utils/src/id_type.rs +++ b/crates/common_utils/src/id_type.rs @@ -1,4 +1,5 @@ //! Common ID types +//! The id type can be used to create specific id types with custom behaviour use std::{ borrow::Cow, @@ -6,6 +7,7 @@ use std::{ }; mod customer; +mod merchant; pub use customer::CustomerId; use diesel::{ @@ -15,18 +17,24 @@ use diesel::{ serialize::{Output, ToSql}, sql_types, }; +pub use merchant::MerchantId; use serde::{Deserialize, Serialize}; use thiserror::Error; use crate::{fp_utils::when, generate_id_with_default_len}; +#[inline] +fn is_valid_id_character(input_char: &char) -> bool { + input_char.is_ascii_alphanumeric() || matches!(input_char, '_' | '-') +} + /// This functions checks for the input string to contain valid characters /// Returns Some(char) if there are any invalid characters, else None fn get_invalid_input_character(input_string: Cow<'static, str>) -> Option { input_string .trim() .chars() - .find(|char| !char.is_ascii_alphanumeric() && !matches!(char, '_' | '-')) + .find(|char| !is_valid_id_character(char)) } #[derive(Debug, PartialEq, Serialize, Clone, Eq)] @@ -74,14 +82,14 @@ impl AlphaNumericId { } } -/// A common type of id that can be used for merchant reference ids +/// A common type of id that can be used for reference ids with length constraint #[derive(Debug, Clone, Serialize, PartialEq, Eq, AsExpression)] #[diesel(sql_type = sql_types::Text)] -pub(crate) struct MerchantReferenceId(AlphaNumericId); +pub(crate) struct LengthId(AlphaNumericId); /// Error generated from violation of constraints for MerchantReferenceId #[derive(Debug, Deserialize, Serialize, Error, PartialEq, Eq)] -pub(crate) enum MerchantReferenceIdError { +pub(crate) enum LengthIdError { #[error("the maximum allowed length for this field is {MAX_LENGTH}")] /// Maximum length of string violated MaxLengthViolated, @@ -95,32 +103,32 @@ pub(crate) enum MerchantReferenceIdError for MerchantReferenceIdError<0, 0> { +impl From for LengthIdError<0, 0> { fn from(alphanumeric_id_error: AlphaNumericIdError) -> Self { Self::AlphanumericIdError(alphanumeric_id_error) } } -impl MerchantReferenceId { +impl LengthId { /// Generates new [MerchantReferenceId] from the given input string pub fn from( input_string: Cow<'static, str>, - ) -> Result> { + ) -> Result> { let trimmed_input_string = input_string.trim().to_string(); let length_of_input_string = u8::try_from(trimmed_input_string.len()) - .map_err(|_| MerchantReferenceIdError::MaxLengthViolated)?; + .map_err(|_| LengthIdError::MaxLengthViolated)?; when(length_of_input_string > MAX_LENGTH, || { - Err(MerchantReferenceIdError::MaxLengthViolated) + Err(LengthIdError::MaxLengthViolated) })?; when(length_of_input_string < MIN_LENGTH, || { - Err(MerchantReferenceIdError::MinLengthViolated) + Err(LengthIdError::MinLengthViolated) })?; let alphanumeric_id = match AlphaNumericId::from(trimmed_input_string.into()) { Ok(valid_alphanumeric_id) => valid_alphanumeric_id, - Err(error) => Err(MerchantReferenceIdError::AlphanumericIdError(error))?, + Err(error) => Err(LengthIdError::AlphanumericIdError(error))?, }; Ok(Self(alphanumeric_id)) @@ -130,10 +138,15 @@ impl MerchantReferenceId Self { Self(AlphaNumericId::new(prefix)) } + + /// Use this function only if you are sure that the length is within the range + pub(crate) fn new_unchecked(alphanumeric_id: AlphaNumericId) -> Self { + Self(alphanumeric_id) + } } impl<'de, const MAX_LENGTH: u8, const MIN_LENGTH: u8> Deserialize<'de> - for MerchantReferenceId + for LengthId { fn deserialize(deserializer: D) -> Result where @@ -145,7 +158,7 @@ impl<'de, const MAX_LENGTH: u8, const MIN_LENGTH: u8> Deserialize<'de> } impl ToSql - for MerchantReferenceId + for LengthId where DB: Backend, String: ToSql, @@ -155,16 +168,14 @@ where } } -impl Display - for MerchantReferenceId -{ +impl Display for LengthId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.0 .0) } } impl FromSql - for MerchantReferenceId + for LengthId where DB: Backend, String: FromSql, @@ -235,7 +246,7 @@ mod merchant_reference_id_tests { #[test] fn test_valid_reference_id() { let parsed_merchant_reference_id = - serde_json::from_str::>(VALID_REF_ID_JSON); + serde_json::from_str::>(VALID_REF_ID_JSON); dbg!(&parsed_merchant_reference_id); @@ -244,18 +255,16 @@ mod merchant_reference_id_tests { #[test] fn test_invalid_ref_id() { - let parsed_merchant_reference_id = serde_json::from_str::< - MerchantReferenceId, - >(INVALID_REF_ID_JSON); + let parsed_merchant_reference_id = + serde_json::from_str::>(INVALID_REF_ID_JSON); assert!(parsed_merchant_reference_id.is_err()); } #[test] fn test_invalid_ref_id_error_message() { - let parsed_merchant_reference_id = serde_json::from_str::< - MerchantReferenceId, - >(INVALID_REF_ID_JSON); + let parsed_merchant_reference_id = + serde_json::from_str::>(INVALID_REF_ID_JSON); let expected_error_message = r#"value `cus abcdefghijklmnopqrstuv` contains invalid character ` `"#.to_string(); @@ -269,9 +278,8 @@ mod merchant_reference_id_tests { #[test] fn test_invalid_ref_id_length() { - let parsed_merchant_reference_id = serde_json::from_str::< - MerchantReferenceId, - >(INVALID_REF_ID_LENGTH); + let parsed_merchant_reference_id = + serde_json::from_str::>(INVALID_REF_ID_LENGTH); dbg!(&parsed_merchant_reference_id); @@ -285,15 +293,11 @@ mod merchant_reference_id_tests { #[test] fn test_invalid_ref_id_length_error_type() { let parsed_merchant_reference_id = - MerchantReferenceId::::from(INVALID_REF_ID_LENGTH.into()); + LengthId::::from(INVALID_REF_ID_LENGTH.into()); dbg!(&parsed_merchant_reference_id); - assert!( - parsed_merchant_reference_id.is_err_and(|error_type| matches!( - error_type, - MerchantReferenceIdError::MaxLengthViolated - )) - ); + assert!(parsed_merchant_reference_id + .is_err_and(|error_type| matches!(error_type, LengthIdError::MaxLengthViolated))); } } diff --git a/crates/common_utils/src/id_type/customer.rs b/crates/common_utils/src/id_type/customer.rs index 4e1256504f..f56957025b 100644 --- a/crates/common_utils/src/id_type/customer.rs +++ b/crates/common_utils/src/id_type/customer.rs @@ -13,17 +13,14 @@ use serde::{Deserialize, Serialize}; use crate::{ consts::{MAX_ALLOWED_MERCHANT_REFERENCE_ID_LENGTH, MIN_REQUIRED_MERCHANT_REFERENCE_ID_LENGTH}, errors, generate_customer_id_of_default_length, - id_type::MerchantReferenceId, + id_type::LengthId, }; /// A type for customer_id that can be used for customer ids #[derive(Clone, Serialize, Deserialize, PartialEq, Eq, AsExpression)] #[diesel(sql_type = sql_types::Text)] pub struct CustomerId( - MerchantReferenceId< - MAX_ALLOWED_MERCHANT_REFERENCE_ID_LENGTH, - MIN_REQUIRED_MERCHANT_REFERENCE_ID_LENGTH, - >, + LengthId, ); impl Default for CustomerId { @@ -53,7 +50,7 @@ where impl CustomerId { pub(crate) fn new( - merchant_ref_id: MerchantReferenceId< + merchant_ref_id: LengthId< MAX_ALLOWED_MERCHANT_REFERENCE_ID_LENGTH, MIN_REQUIRED_MERCHANT_REFERENCE_ID_LENGTH, >, @@ -68,7 +65,7 @@ impl CustomerId { /// Create a Customer id from string pub fn from(input_string: Cow<'static, str>) -> Result { - let merchant_ref_id = MerchantReferenceId::from(input_string).change_context( + let merchant_ref_id = LengthId::from(input_string).change_context( errors::ValidationError::IncorrectValueProvided { field_name: "customer_id", }, @@ -83,10 +80,8 @@ impl masking::SerializableSecret for CustomerId {} impl ToSql for CustomerId where DB: Backend, - MerchantReferenceId< - MAX_ALLOWED_MERCHANT_REFERENCE_ID_LENGTH, - MIN_REQUIRED_MERCHANT_REFERENCE_ID_LENGTH, - >: ToSql, + LengthId: + ToSql, { fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, DB>) -> diesel::serialize::Result { self.0.to_sql(out) @@ -96,13 +91,11 @@ where impl FromSql for CustomerId where DB: Backend, - MerchantReferenceId< - MAX_ALLOWED_MERCHANT_REFERENCE_ID_LENGTH, - MIN_REQUIRED_MERCHANT_REFERENCE_ID_LENGTH, - >: FromSql, + LengthId: + FromSql, { fn from_sql(value: DB::RawValue<'_>) -> diesel::deserialize::Result { - MerchantReferenceId::< + LengthId::< MAX_ALLOWED_MERCHANT_REFERENCE_ID_LENGTH, MIN_REQUIRED_MERCHANT_REFERENCE_ID_LENGTH, >::from_sql(value) diff --git a/crates/common_utils/src/id_type/merchant.rs b/crates/common_utils/src/id_type/merchant.rs new file mode 100644 index 0000000000..3accdc3a36 --- /dev/null +++ b/crates/common_utils/src/id_type/merchant.rs @@ -0,0 +1,114 @@ +//! Contains the id type for merchant account +//! +//! Ids for merchant account are derived from the merchant name +//! If there are any special characters, they are removed + +use std::{borrow::Cow, fmt::Debug}; + +use diesel::{ + backend::Backend, + deserialize::FromSql, + expression::AsExpression, + serialize::{Output, ToSql}, + sql_types, Queryable, +}; +use error_stack::{Result, ResultExt}; +use serde::{Deserialize, Serialize}; + +use crate::{ + consts::{MAX_ALLOWED_MERCHANT_REFERENCE_ID_LENGTH, MIN_REQUIRED_MERCHANT_REFERENCE_ID_LENGTH}, + errors, generate_id_with_default_len, generate_ref_id_with_default_length, + id_type::{AlphaNumericId, LengthId}, + new_type::MerchantName, +}; + +/// A type for merchant_id that can be used for merchant ids +#[derive(Clone, Serialize, Deserialize, PartialEq, Eq, AsExpression)] +#[diesel(sql_type = sql_types::Text)] +pub struct MerchantId( + LengthId, +); + +/// This is to display the `MerchantId` as MerchantId(abcd) +impl Debug for MerchantId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_tuple("MerchantId").field(&self.0 .0 .0).finish() + } +} + +impl Queryable for MerchantId +where + DB: Backend, + Self: FromSql, +{ + type Row = Self; + + fn build(row: Self::Row) -> diesel::deserialize::Result { + Ok(row) + } +} + +impl Default for MerchantId { + fn default() -> Self { + Self(generate_ref_id_with_default_length("mer")) + } +} + +impl MerchantId { + /// Get the string representation of merchant id + pub fn get_string_repr(&self) -> &str { + &self.0 .0 .0 + } + + /// Create a Merchant id from string + pub fn from(input_string: Cow<'static, str>) -> Result { + let length_id = LengthId::from(input_string).change_context( + errors::ValidationError::IncorrectValueProvided { + field_name: "merchant_id", + }, + )?; + + Ok(Self(length_id)) + } + + /// Create a Merchant id from MerchantName + pub fn from_merchant_name(merchant_name: MerchantName) -> Self { + let merchant_name_string = merchant_name.into_inner(); + + let merchant_id_prefix = merchant_name_string.trim().to_lowercase().replace(' ', ""); + + let alphanumeric_id = + AlphaNumericId::new_unchecked(generate_id_with_default_len(&merchant_id_prefix)); + let length_id = LengthId::new_unchecked(alphanumeric_id); + + Self(length_id) + } +} + +impl masking::SerializableSecret for MerchantId {} + +impl ToSql for MerchantId +where + DB: Backend, + LengthId: + ToSql, +{ + fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, DB>) -> diesel::serialize::Result { + self.0.to_sql(out) + } +} + +impl FromSql for MerchantId +where + DB: Backend, + LengthId: + FromSql, +{ + fn from_sql(value: DB::RawValue<'_>) -> diesel::deserialize::Result { + LengthId::< + MAX_ALLOWED_MERCHANT_REFERENCE_ID_LENGTH, + MIN_REQUIRED_MERCHANT_REFERENCE_ID_LENGTH, + >::from_sql(value) + .map(Self) + } +} diff --git a/crates/common_utils/src/lib.rs b/crates/common_utils/src/lib.rs index d1449f2a7d..39826944a1 100644 --- a/crates/common_utils/src/lib.rs +++ b/crates/common_utils/src/lib.rs @@ -6,7 +6,7 @@ use masking::{PeekInterface, Secret}; use crate::{ consts::ID_LENGTH, - id_type::{CustomerId, MerchantReferenceId}, + id_type::{CustomerId, LengthId}, }; pub mod access_token; @@ -21,6 +21,7 @@ pub mod fp_utils; pub mod id_type; pub mod link_utils; pub mod macros; +pub mod new_type; pub mod pii; #[allow(missing_docs)] // Todo: add docs pub mod request; @@ -207,16 +208,16 @@ pub fn generate_id(length: usize, prefix: &str) -> String { format!("{}_{}", prefix, nanoid::nanoid!(length, &consts::ALPHABETS)) } -/// Generate a MerchantRefId with the default length -fn generate_merchant_ref_id_with_default_length( +/// Generate a ReferenceId with the default length with the given prefix +fn generate_ref_id_with_default_length( prefix: &str, -) -> MerchantReferenceId { - MerchantReferenceId::::new(prefix) +) -> LengthId { + LengthId::::new(prefix) } -/// Generate a customer id with default length +/// Generate a customer id with default length, with prefix as `cus` pub fn generate_customer_id_of_default_length() -> CustomerId { - CustomerId::new(generate_merchant_ref_id_with_default_length("cus")) + CustomerId::new(generate_ref_id_with_default_length("cus")) } /// Generate a nanoid with the given prefix and a default length @@ -272,7 +273,7 @@ mod nanoid_tests { #[test] fn test_generate_merchant_ref_id_with_default_length() { - let ref_id = MerchantReferenceId::< + let ref_id = LengthId::< MAX_ALLOWED_MERCHANT_REFERENCE_ID_LENGTH, MIN_REQUIRED_MERCHANT_REFERENCE_ID_LENGTH, >::from(generate_id_with_default_len("def").into()); diff --git a/crates/common_utils/src/new_type.rs b/crates/common_utils/src/new_type.rs new file mode 100644 index 0000000000..01d23448d2 --- /dev/null +++ b/crates/common_utils/src/new_type.rs @@ -0,0 +1,10 @@ +//! Contains new types with restrictions +use crate::consts::MAX_ALLOWED_MERCHANT_NAME_LENGTH; + +#[nutype::nutype( + derive(Clone, Serialize, Deserialize, Debug), + validate(len_char_min = 1, len_char_max = MAX_ALLOWED_MERCHANT_NAME_LENGTH) +)] +pub struct MerchantName(String); + +impl masking::SerializableSecret for MerchantName {} diff --git a/crates/hyperswitch_domain_models/src/lib.rs b/crates/hyperswitch_domain_models/src/lib.rs index 7771d2db50..fdafe308a4 100644 --- a/crates/hyperswitch_domain_models/src/lib.rs +++ b/crates/hyperswitch_domain_models/src/lib.rs @@ -1,5 +1,6 @@ pub mod errors; pub mod mandates; +pub mod merchant_account; pub mod payment_address; pub mod payment_method_data; pub mod payments; diff --git a/crates/hyperswitch_domain_models/src/merchant_account.rs b/crates/hyperswitch_domain_models/src/merchant_account.rs new file mode 100644 index 0000000000..e42f21dc7c --- /dev/null +++ b/crates/hyperswitch_domain_models/src/merchant_account.rs @@ -0,0 +1,298 @@ +use common_utils::{ + crypto::{OptionalEncryptableName, OptionalEncryptableValue}, + date_time, + errors::{CustomResult, ValidationError}, + ext_traits::ValueExt, + pii, +}; +use diesel_models::{ + encryption::Encryption, enums::MerchantStorageScheme, + merchant_account::MerchantAccountUpdateInternal, +}; +use error_stack::ResultExt; +use masking::{PeekInterface, Secret}; +use router_env::logger; + +use crate::type_encryption::{decrypt, AsyncLift}; + +#[derive(Clone, Debug, serde::Serialize)] +pub struct MerchantAccount { + pub id: Option, + pub merchant_id: String, + pub return_url: Option, + pub enable_payment_response_hash: bool, + pub payment_response_hash_key: Option, + pub redirect_to_merchant_with_http_post: bool, + pub merchant_name: OptionalEncryptableName, + pub merchant_details: OptionalEncryptableValue, + pub webhook_details: Option, + pub sub_merchants_enabled: Option, + pub parent_merchant_id: Option, + pub publishable_key: String, + pub storage_scheme: MerchantStorageScheme, + pub locker_id: Option, + pub metadata: Option, + pub routing_algorithm: Option, + pub primary_business_details: serde_json::Value, + pub frm_routing_algorithm: Option, + pub created_at: time::PrimitiveDateTime, + pub modified_at: time::PrimitiveDateTime, + pub intent_fulfillment_time: Option, + pub payout_routing_algorithm: Option, + pub organization_id: String, + pub is_recon_enabled: bool, + pub default_profile: Option, + pub recon_status: diesel_models::enums::ReconStatus, + pub payment_link_config: Option, + pub pm_collect_link_config: Option, +} + +#[allow(clippy::large_enum_variant)] +#[derive(Debug)] +pub enum MerchantAccountUpdate { + Update { + merchant_name: OptionalEncryptableName, + merchant_details: OptionalEncryptableValue, + return_url: Option, + webhook_details: Option, + sub_merchants_enabled: Option, + parent_merchant_id: Option, + enable_payment_response_hash: Option, + payment_response_hash_key: Option, + redirect_to_merchant_with_http_post: Option, + publishable_key: Option, + locker_id: Option, + metadata: Option, + routing_algorithm: Option, + primary_business_details: Option, + intent_fulfillment_time: Option, + frm_routing_algorithm: Option, + payout_routing_algorithm: Option, + default_profile: Option>, + payment_link_config: Option, + pm_collect_link_config: Option, + }, + StorageSchemeUpdate { + storage_scheme: MerchantStorageScheme, + }, + ReconUpdate { + recon_status: diesel_models::enums::ReconStatus, + }, + UnsetDefaultProfile, + ModifiedAtUpdate, +} + +impl From for MerchantAccountUpdateInternal { + fn from(merchant_account_update: MerchantAccountUpdate) -> Self { + let now = date_time::now(); + + match merchant_account_update { + MerchantAccountUpdate::Update { + merchant_name, + merchant_details, + return_url, + webhook_details, + routing_algorithm, + sub_merchants_enabled, + parent_merchant_id, + enable_payment_response_hash, + payment_response_hash_key, + redirect_to_merchant_with_http_post, + publishable_key, + locker_id, + metadata, + primary_business_details, + intent_fulfillment_time, + frm_routing_algorithm, + payout_routing_algorithm, + default_profile, + payment_link_config, + pm_collect_link_config, + } => Self { + merchant_name: merchant_name.map(Encryption::from), + merchant_details: merchant_details.map(Encryption::from), + frm_routing_algorithm, + return_url, + webhook_details, + routing_algorithm, + sub_merchants_enabled, + parent_merchant_id, + enable_payment_response_hash, + payment_response_hash_key, + redirect_to_merchant_with_http_post, + publishable_key, + locker_id, + metadata, + primary_business_details, + modified_at: Some(now), + intent_fulfillment_time, + payout_routing_algorithm, + default_profile, + payment_link_config, + pm_collect_link_config, + ..Default::default() + }, + MerchantAccountUpdate::StorageSchemeUpdate { storage_scheme } => Self { + storage_scheme: Some(storage_scheme), + modified_at: Some(now), + ..Default::default() + }, + MerchantAccountUpdate::ReconUpdate { recon_status } => Self { + recon_status: Some(recon_status), + modified_at: Some(now), + ..Default::default() + }, + MerchantAccountUpdate::UnsetDefaultProfile => Self { + default_profile: Some(None), + modified_at: Some(now), + ..Default::default() + }, + MerchantAccountUpdate::ModifiedAtUpdate => Self { + modified_at: Some(date_time::now()), + ..Default::default() + }, + } + } +} + +#[async_trait::async_trait] +impl super::behaviour::Conversion for MerchantAccount { + type DstType = diesel_models::merchant_account::MerchantAccount; + type NewDstType = diesel_models::merchant_account::MerchantAccountNew; + async fn convert(self) -> CustomResult { + Ok(diesel_models::merchant_account::MerchantAccount { + id: self.id.ok_or(ValidationError::MissingRequiredField { + field_name: "id".to_string(), + })?, + merchant_id: self.merchant_id, + return_url: self.return_url, + enable_payment_response_hash: self.enable_payment_response_hash, + payment_response_hash_key: self.payment_response_hash_key, + redirect_to_merchant_with_http_post: self.redirect_to_merchant_with_http_post, + merchant_name: self.merchant_name.map(|name| name.into()), + merchant_details: self.merchant_details.map(|details| details.into()), + webhook_details: self.webhook_details, + sub_merchants_enabled: self.sub_merchants_enabled, + parent_merchant_id: self.parent_merchant_id, + publishable_key: Some(self.publishable_key), + storage_scheme: self.storage_scheme, + locker_id: self.locker_id, + metadata: self.metadata, + routing_algorithm: self.routing_algorithm, + primary_business_details: self.primary_business_details, + created_at: self.created_at, + modified_at: self.modified_at, + intent_fulfillment_time: self.intent_fulfillment_time, + frm_routing_algorithm: self.frm_routing_algorithm, + payout_routing_algorithm: self.payout_routing_algorithm, + organization_id: self.organization_id, + is_recon_enabled: self.is_recon_enabled, + default_profile: self.default_profile, + recon_status: self.recon_status, + payment_link_config: self.payment_link_config, + pm_collect_link_config: self.pm_collect_link_config, + }) + } + + async fn convert_back( + item: Self::DstType, + key: &Secret>, + ) -> CustomResult + where + Self: Sized, + { + let publishable_key = + item.publishable_key + .ok_or(ValidationError::MissingRequiredField { + field_name: "publishable_key".to_string(), + })?; + + async { + Ok::>(Self { + id: Some(item.id), + merchant_id: item.merchant_id, + return_url: item.return_url, + enable_payment_response_hash: item.enable_payment_response_hash, + payment_response_hash_key: item.payment_response_hash_key, + redirect_to_merchant_with_http_post: item.redirect_to_merchant_with_http_post, + merchant_name: item + .merchant_name + .async_lift(|inner| decrypt(inner, key.peek())) + .await?, + merchant_details: item + .merchant_details + .async_lift(|inner| decrypt(inner, key.peek())) + .await?, + webhook_details: item.webhook_details, + sub_merchants_enabled: item.sub_merchants_enabled, + parent_merchant_id: item.parent_merchant_id, + publishable_key, + storage_scheme: item.storage_scheme, + locker_id: item.locker_id, + metadata: item.metadata, + routing_algorithm: item.routing_algorithm, + frm_routing_algorithm: item.frm_routing_algorithm, + primary_business_details: item.primary_business_details, + created_at: item.created_at, + modified_at: item.modified_at, + intent_fulfillment_time: item.intent_fulfillment_time, + payout_routing_algorithm: item.payout_routing_algorithm, + organization_id: item.organization_id, + is_recon_enabled: item.is_recon_enabled, + default_profile: item.default_profile, + recon_status: item.recon_status, + payment_link_config: item.payment_link_config, + pm_collect_link_config: item.pm_collect_link_config, + }) + } + .await + .change_context(ValidationError::InvalidValue { + message: "Failed while decrypting merchant data".to_string(), + }) + } + + async fn construct_new(self) -> CustomResult { + let now = date_time::now(); + Ok(diesel_models::merchant_account::MerchantAccountNew { + merchant_id: self.merchant_id, + merchant_name: self.merchant_name.map(Encryption::from), + merchant_details: self.merchant_details.map(Encryption::from), + return_url: self.return_url, + webhook_details: self.webhook_details, + sub_merchants_enabled: self.sub_merchants_enabled, + parent_merchant_id: self.parent_merchant_id, + enable_payment_response_hash: Some(self.enable_payment_response_hash), + payment_response_hash_key: self.payment_response_hash_key, + redirect_to_merchant_with_http_post: Some(self.redirect_to_merchant_with_http_post), + publishable_key: Some(self.publishable_key), + locker_id: self.locker_id, + metadata: self.metadata, + routing_algorithm: self.routing_algorithm, + primary_business_details: self.primary_business_details, + created_at: now, + modified_at: now, + intent_fulfillment_time: self.intent_fulfillment_time, + frm_routing_algorithm: self.frm_routing_algorithm, + payout_routing_algorithm: self.payout_routing_algorithm, + organization_id: self.organization_id, + is_recon_enabled: self.is_recon_enabled, + default_profile: self.default_profile, + recon_status: self.recon_status, + payment_link_config: self.payment_link_config, + pm_collect_link_config: self.pm_collect_link_config, + }) + } +} + +impl MerchantAccount { + pub fn get_compatible_connector(&self) -> Option { + let metadata: Option = + self.metadata.as_ref().and_then(|meta| { + meta.clone() + .parse_value("MerchantAccountMetadata") + .map_err(|err| logger::error!("Failed to deserialize {:?}", err)) + .ok() + }); + metadata.and_then(|a| a.compatible_connector) + } +} diff --git a/crates/openapi/Cargo.toml b/crates/openapi/Cargo.toml index b8c55084a5..9b81225bcb 100644 --- a/crates/openapi/Cargo.toml +++ b/crates/openapi/Cargo.toml @@ -12,4 +12,9 @@ serde_json = "1.0.115" utoipa = { version = "4.2.0", features = ["preserve_order", "preserve_path_order", "time"] } api_models = { version = "0.1.0", path = "../api_models", features = ["frm", "payouts", "openapi"] } -common_utils = {version = "0.1.0", path = "../common_utils"} +common_utils = { version = "0.1.0", path = "../common_utils" } +router_env = { version = "0.1.0", path = "../router_env" } + +[features] +v2 = ["api_models/v2"] +default = [] diff --git a/crates/openapi/src/main.rs b/crates/openapi/src/main.rs index e626b4fad9..f957e0cdd9 100644 --- a/crates/openapi/src/main.rs +++ b/crates/openapi/src/main.rs @@ -2,14 +2,24 @@ mod openapi; mod routes; fn main() { - let file_path = "api-reference/openapi_spec.json"; + #[cfg(not(feature = "v2"))] + let relative_file_path = "api-reference/openapi_spec.json"; + + #[cfg(feature = "v2")] + let relative_file_path = "api-reference/v2/openapi_spec.json"; + + let mut file_path = router_env::workspace_path(); + file_path.push(relative_file_path); + + let openapi = ::openapi(); + #[allow(clippy::expect_used)] std::fs::write( file_path, - ::openapi() + openapi .to_pretty_json() .expect("Failed to serialize OpenAPI specification as JSON"), ) .expect("Failed to write OpenAPI specification to file"); - println!("Successfully saved OpenAPI specification file at '{file_path}'"); + println!("Successfully saved OpenAPI specification file at '{relative_file_path}'"); } diff --git a/crates/openapi/src/openapi.rs b/crates/openapi/src/openapi.rs index 3b37824be3..82bde980a1 100644 --- a/crates/openapi/src/openapi.rs +++ b/crates/openapi/src/openapi.rs @@ -559,7 +559,7 @@ Never share your secret api keys. Keep them guarded and secure. )] // Bypass clippy lint for not being constructed #[allow(dead_code)] -pub struct ApiDoc; +pub(crate) struct ApiDoc; struct SecurityAddon; diff --git a/crates/router/Cargo.toml b/crates/router/Cargo.toml index e4bfb4a454..f23029e36d 100644 --- a/crates/router/Cargo.toml +++ b/crates/router/Cargo.toml @@ -31,6 +31,7 @@ payouts = ["api_models/payouts", "common_enums/payouts", "hyperswitch_domain_mod payout_retry = ["payouts"] recon = ["email", "api_models/recon"] retry = [] +v2 = ["api_models/v2"] [dependencies] actix-cors = "0.6.5" @@ -67,7 +68,7 @@ mime = "0.3.17" nanoid = "0.4.0" num_cpus = "1.16.0" once_cell = "1.19.0" -openidconnect = "3.5.0" # TODO: remove reqwest +openidconnect = "3.5.0" # TODO: remove reqwest openssl = "0.10.64" qrcode = "0.14.0" rand = "0.8.5" @@ -122,7 +123,6 @@ router_derive = { version = "0.1.0", path = "../router_derive" } router_env = { version = "0.1.0", path = "../router_env", features = ["log_extra_implicit_fields", "log_custom_entries_to_extra"] } scheduler = { version = "0.1.0", path = "../scheduler", default-features = false } storage_impl = { version = "0.1.0", path = "../storage_impl", default-features = false } -openapi = { version = "0.1.0", path = "../openapi", optional = true } erased-serde = "0.4.4" quick-xml = { version = "0.31.0", features = ["serialize"] } rdkafka = "0.36.2" diff --git a/crates/router/src/analytics.rs b/crates/router/src/analytics.rs index cf98748048..cb34b8f6e9 100644 --- a/crates/router/src/analytics.rs +++ b/crates/router/src/analytics.rs @@ -333,7 +333,7 @@ pub mod routes { |state, auth: AuthenticationData, req, _| async move { analytics::sdk_events::get_metrics( &state.pool, - auth.merchant_account.publishable_key.as_ref(), + &auth.merchant_account.publishable_key, req, ) .await @@ -369,8 +369,8 @@ pub mod routes { |state, auth: AuthenticationData, req, _| async move { analytics::active_payments::get_metrics( &state.pool, - auth.merchant_account.publishable_key.as_ref(), - Some(&auth.merchant_account.merchant_id), + &auth.merchant_account.publishable_key, + &auth.merchant_account.merchant_id, req, ) .await @@ -407,7 +407,7 @@ pub mod routes { analytics::auth_events::get_metrics( &state.pool, &auth.merchant_account.merchant_id, - auth.merchant_account.publishable_key.as_ref(), + &auth.merchant_account.publishable_key, req, ) .await @@ -534,7 +534,7 @@ pub mod routes { analytics::sdk_events::get_filters( &state.pool, req, - auth.merchant_account.publishable_key.as_ref(), + &auth.merchant_account.publishable_key, ) .await .map(ApplicationResponse::Json) @@ -603,13 +603,9 @@ pub mod routes { &req, json_payload.into_inner(), |state, auth: AuthenticationData, req, _| async move { - sdk_events_core( - &state.pool, - req, - auth.merchant_account.publishable_key.unwrap_or_default(), - ) - .await - .map(ApplicationResponse::Json) + sdk_events_core(&state.pool, req, &auth.merchant_account.publishable_key) + .await + .map(ApplicationResponse::Json) }, &auth::JWTAuth(Permission::Analytics), api_locking::LockAction::NotApplicable, diff --git a/crates/router/src/consts/user.rs b/crates/router/src/consts/user.rs index 261f8a1efc..18fdf0dcbe 100644 --- a/crates/router/src/consts/user.rs +++ b/crates/router/src/consts/user.rs @@ -1,5 +1,10 @@ +use common_utils::consts::MAX_ALLOWED_MERCHANT_NAME_LENGTH; + pub const MAX_NAME_LENGTH: usize = 70; -pub const MAX_COMPANY_NAME_LENGTH: usize = 70; + +/// The max length of company name and merchant should be same +/// because we are deriving the merchant name from company name +pub const MAX_COMPANY_NAME_LENGTH: usize = MAX_ALLOWED_MERCHANT_NAME_LENGTH; pub const BUSINESS_EMAIL: &str = "biz@hyperswitch.io"; pub const RECOVERY_CODES_COUNT: usize = 8; diff --git a/crates/router/src/core/admin.rs b/crates/router/src/core/admin.rs index d51f463f23..dcc649279a 100644 --- a/crates/router/src/core/admin.rs +++ b/crates/router/src/core/admin.rs @@ -5,7 +5,6 @@ use api_models::{ enums as api_enums, routing as routing_types, }; use common_utils::{ - crypto::{generate_cryptographically_secure_random_string, OptionalSecretValue}, date_time, ext_traits::{AsyncExt, ConfigExt, Encode, ValueExt}, pii, @@ -18,6 +17,8 @@ use pm_auth::connector::plaid::transformers::PlaidAuthType; use router_env::metrics::add_attributes; use uuid::Uuid; +#[cfg(all(not(feature = "v2"), feature = "olap"))] +use crate::types::transformers::ForeignFrom; use crate::{ consts, core::{ @@ -36,7 +37,7 @@ use crate::{ types::{self as domain_types, AsyncLift}, }, storage::{self, enums::MerchantStorageScheme}, - transformers::{ForeignFrom, ForeignTryFrom}, + transformers::ForeignTryFrom, }, utils::{self, OptionExt}, }; @@ -50,76 +51,69 @@ pub fn create_merchant_publishable_key() -> String { ) } +pub async fn insert_merchant_configs( + db: &dyn StorageInterface, + merchant_id: &String, +) -> RouterResult<()> { + db.insert_config(configs::ConfigNew { + key: format!("{}_requires_cvv", merchant_id), + config: "true".to_string(), + }) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Error while setting requires_cvv config")?; + + db.insert_config(configs::ConfigNew { + key: utils::get_merchant_fingerprint_secret_key(merchant_id), + config: utils::generate_id(consts::FINGERPRINT_SECRET_LENGTH, "fs"), + }) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Error while inserting merchant fingerprint secret")?; + + Ok(()) +} + +#[cfg(feature = "olap")] +fn add_publishable_key_to_decision_service( + state: &SessionState, + merchant_account: &domain::MerchantAccount, +) { + let state = state.clone(); + let publishable_key = merchant_account.publishable_key.clone(); + let merchant_id = merchant_account.merchant_id.clone(); + + authentication::decision::spawn_tracked_job( + async move { + authentication::decision::add_publishable_key( + &state, + publishable_key.into(), + merchant_id, + None, + ) + .await + }, + authentication::decision::ADD, + ); +} + +#[cfg(feature = "olap")] pub async fn create_merchant_account( state: SessionState, req: api::MerchantAccountCreate, ) -> RouterResponse { let db = state.store.as_ref(); - let master_key = db.get_master_key(); let key = services::generate_aes256_key() .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Unable to generate aes 256 key")?; - let publishable_key = Some(create_merchant_publishable_key()); + let master_key = db.get_master_key(); - let primary_business_details = req - .primary_business_details - .clone() - .unwrap_or_default() - .encode_to_value() - .change_context(errors::ApiErrorResponse::InvalidDataValue { - field_name: "primary_business_details", - })?; - - let merchant_details: OptionalSecretValue = req - .merchant_details - .as_ref() - .map(|merchant_details| { - merchant_details.encode_to_value().change_context( - errors::ApiErrorResponse::InvalidDataValue { - field_name: "merchant_details", - }, - ) - }) - .transpose()? - .map(Into::into); - - let webhook_details = req - .webhook_details - .as_ref() - .map(|webhook_details| { - webhook_details.encode_to_value().change_context( - errors::ApiErrorResponse::InvalidDataValue { - field_name: "webhook details", - }, - ) - }) - .transpose()?; - - if let Some(ref routing_algorithm) = req.routing_algorithm { - let _: api_models::routing::RoutingAlgorithm = routing_algorithm - .clone() - .parse_value("RoutingAlgorithm") - .change_context(errors::ApiErrorResponse::InvalidDataValue { - field_name: "routing_algorithm", - }) - .attach_printable("Invalid routing algorithm given")?; - } - - let pm_collect_link_config = req - .pm_collect_link_config - .as_ref() - .map(|c| { - c.encode_to_value() - .change_context(errors::ApiErrorResponse::InvalidDataValue { - field_name: "pm_collect_link_config", - }) - }) - .transpose()?; + let merchant_id = req.get_merchant_reference_id().get_string_repr().to_owned(); let key_store = domain::MerchantKeyStore { - merchant_id: req.merchant_id.clone(), + merchant_id: merchant_id.clone(), key: domain_types::encrypt(key.to_vec().into(), master_key) .await .change_context(errors::ApiErrorResponse::InternalServerError) @@ -127,118 +121,311 @@ pub async fn create_merchant_account( created_at: date_time::now(), }; - let enable_payment_response_hash = req.enable_payment_response_hash.unwrap_or(true); - - let payment_response_hash_key = req - .payment_response_hash_key - .or(Some(generate_cryptographically_secure_random_string(64))); + let domain_merchant_account = req + .create_domain_model_from_request(db, key_store.clone()) + .await?; db.insert_merchant_key_store(key_store.clone(), &master_key.to_vec().into()) .await .to_duplicate_response(errors::ApiErrorResponse::DuplicateMerchantAccount)?; - let parent_merchant_id = get_parent_merchant( - db, - req.sub_merchants_enabled, - req.parent_merchant_id, - &key_store, - ) - .await?; - - let metadata = req - .metadata - .as_ref() - .map(|meta| { - meta.encode_to_value() - .change_context(errors::ApiErrorResponse::InvalidDataValue { - field_name: "metadata", - }) - }) - .transpose()? - .map(Secret::new); - - let fingerprint = Some(utils::generate_id(consts::FINGERPRINT_SECRET_LENGTH, "fs")); - if let Some(fingerprint) = fingerprint { - db.insert_config(configs::ConfigNew { - key: format!("fingerprint_secret_{}", req.merchant_id), - config: fingerprint, - }) + let merchant_account = db + .insert_merchant(domain_merchant_account, &key_store) .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Mot able to generate Merchant fingerprint")?; - }; + .to_duplicate_response(errors::ApiErrorResponse::DuplicateMerchantAccount)?; - let organization_id = if let Some(organization_id) = req.organization_id.as_ref() { - db.find_organization_by_org_id(organization_id) - .await - .to_not_found_response(errors::ApiErrorResponse::GenericNotFoundError { - message: "organization with the given id does not exist".to_string(), - })?; - organization_id.to_string() - } else { - let new_organization = api_models::organization::OrganizationNew::new(None); - let db_organization = ForeignFrom::foreign_from(new_organization); - let organization = db - .insert_organization(db_organization) - .await - .to_duplicate_response(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Error when creating organization")?; - organization.org_id - }; + add_publishable_key_to_decision_service(&state, &merchant_account); - let mut merchant_account = async { - Ok::<_, error_stack::Report>(domain::MerchantAccount { - merchant_id: req.merchant_id, - merchant_name: req - .merchant_name - .async_lift(|inner| domain_types::encrypt_optional(inner, &key)) - .await?, - merchant_details: merchant_details - .async_lift(|inner| domain_types::encrypt_optional(inner, &key)) - .await?, - return_url: req.return_url.map(|a| a.to_string()), - webhook_details, - routing_algorithm: Some(serde_json::json!({ - "algorithm_id": null, - "timestamp": 0 - })), - sub_merchants_enabled: req.sub_merchants_enabled, - parent_merchant_id, - enable_payment_response_hash, - payment_response_hash_key, - redirect_to_merchant_with_http_post: req - .redirect_to_merchant_with_http_post - .unwrap_or_default(), - publishable_key, - locker_id: req.locker_id, - metadata, - storage_scheme: MerchantStorageScheme::PostgresOnly, - primary_business_details, - created_at: date_time::now(), - modified_at: date_time::now(), - intent_fulfillment_time: None, - frm_routing_algorithm: req.frm_routing_algorithm, - #[cfg(feature = "payouts")] - payout_routing_algorithm: req.payout_routing_algorithm, - #[cfg(not(feature = "payouts"))] - payout_routing_algorithm: None, - id: None, - organization_id, - is_recon_enabled: false, - default_profile: None, - recon_status: diesel_models::enums::ReconStatus::NotRequested, - payment_link_config: None, - pm_collect_link_config, + insert_merchant_configs(db, &merchant_id).await?; + + Ok(service_api::ApplicationResponse::Json( + api::MerchantAccountResponse::foreign_try_from(merchant_account) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed while generating response")?, + )) +} + +#[cfg(feature = "olap")] +#[async_trait::async_trait] +trait MerchantAccountCreateBridge { + async fn create_domain_model_from_request( + self, + db: &dyn StorageInterface, + key: domain::MerchantKeyStore, + ) -> RouterResult; +} + +#[cfg(all(not(feature = "v2"), feature = "olap"))] +#[async_trait::async_trait] +impl MerchantAccountCreateBridge for api::MerchantAccountCreate { + async fn create_domain_model_from_request( + self, + db: &dyn StorageInterface, + key_store: domain::MerchantKeyStore, + ) -> RouterResult { + let publishable_key = create_merchant_publishable_key(); + + let primary_business_details = self.get_primary_details_as_value().change_context( + errors::ApiErrorResponse::InvalidDataValue { + field_name: "primary_business_details", + }, + )?; + + let webhook_details = self.get_webhook_details_as_value().change_context( + errors::ApiErrorResponse::InvalidDataValue { + field_name: "webhook details", + }, + )?; + + let pm_collect_link_config = self.get_pm_link_config_as_value().change_context( + errors::ApiErrorResponse::InvalidDataValue { + field_name: "pm_collect_link_config", + }, + )?; + + let merchant_details = self.get_merchant_details_as_secret().change_context( + errors::ApiErrorResponse::InvalidDataValue { + field_name: "merchant_details", + }, + )?; + + self.parse_routing_algorithm() + .change_context(errors::ApiErrorResponse::InvalidDataValue { + field_name: "routing_algorithm", + }) + .attach_printable("Invalid routing algorithm given")?; + + let metadata = self.get_metadata_as_secret().change_context( + errors::ApiErrorResponse::InvalidDataValue { + field_name: "metadata", + }, + )?; + + // Get the enable payment response hash as a boolean, where the default value is true + let enable_payment_response_hash = self.get_enable_payment_response_hash(); + + let payment_response_hash_key = self.get_payment_response_hash_key(); + + let parent_merchant_id = get_parent_merchant( + db, + self.sub_merchants_enabled, + self.parent_merchant_id, + &key_store, + ) + .await?; + + let organization_id = CreateOrValidateOrganization::new(self.organization_id) + .create_or_validate(db) + .await?; + + let key = key_store.key.into_inner(); + + let mut merchant_account = async { + Ok::<_, error_stack::Report>( + domain::MerchantAccount { + merchant_id: self.merchant_id.get_string_repr().to_owned(), + merchant_name: self + .merchant_name + .async_lift(|inner| domain_types::encrypt_optional(inner, key.peek())) + .await?, + merchant_details: merchant_details + .async_lift(|inner| domain_types::encrypt_optional(inner, key.peek())) + .await?, + return_url: self.return_url.map(|a| a.to_string()), + webhook_details, + routing_algorithm: Some(serde_json::json!({ + "algorithm_id": null, + "timestamp": 0 + })), + sub_merchants_enabled: self.sub_merchants_enabled, + parent_merchant_id, + enable_payment_response_hash, + payment_response_hash_key, + redirect_to_merchant_with_http_post: self + .redirect_to_merchant_with_http_post + .unwrap_or_default(), + publishable_key, + locker_id: self.locker_id, + metadata, + storage_scheme: MerchantStorageScheme::PostgresOnly, + primary_business_details, + created_at: date_time::now(), + modified_at: date_time::now(), + intent_fulfillment_time: None, + frm_routing_algorithm: self.frm_routing_algorithm, + #[cfg(feature = "payouts")] + payout_routing_algorithm: self.payout_routing_algorithm, + #[cfg(not(feature = "payouts"))] + payout_routing_algorithm: None, + id: None, + organization_id, + is_recon_enabled: false, + default_profile: None, + recon_status: diesel_models::enums::ReconStatus::NotRequested, + payment_link_config: None, + pm_collect_link_config, + }, + ) + } + .await + .change_context(errors::ApiErrorResponse::InternalServerError)?; + + CreateBusinessProfile::new(self.primary_business_details.clone()) + .create_business_profiles(db, &mut merchant_account) + .await?; + + Ok(merchant_account) + } +} + +#[cfg(feature = "olap")] +enum CreateOrValidateOrganization { + /// Creates a new organization + #[cfg(not(feature = "v2"))] + Create, + /// Validates if this organization exists in the records + Validate { organization_id: String }, +} + +#[cfg(feature = "olap")] +impl CreateOrValidateOrganization { + #[cfg(all(not(feature = "v2"), feature = "olap"))] + /// Create an action to either create or validate the given organization_id + /// If organization_id is passed, then validate if this organization exists + /// If not passed, create a new organization + fn new(organization_id: Option) -> Self { + if let Some(organization_id) = organization_id { + Self::Validate { organization_id } + } else { + Self::Create + } + } + + #[cfg(all(feature = "v2", feature = "olap"))] + /// Create an action to validate the provided organization_id + fn new(organization_id: String) -> Self { + Self::Validate { organization_id } + } + + #[cfg(feature = "olap")] + /// Apply the action, whether to create the organization or validate the given organization_id + async fn create_or_validate(&self, db: &dyn StorageInterface) -> RouterResult { + Ok(match self { + #[cfg(not(feature = "v2"))] + Self::Create => { + let new_organization = api_models::organization::OrganizationNew::new(None); + let db_organization = ForeignFrom::foreign_from(new_organization); + let organization = db + .insert_organization(db_organization) + .await + .to_duplicate_response(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Error when creating organization")?; + organization.org_id + } + Self::Validate { organization_id } => { + db.find_organization_by_org_id(organization_id) + .await + .to_not_found_response(errors::ApiErrorResponse::GenericNotFoundError { + message: "organization with the given id does not exist".to_string(), + })?; + organization_id.to_string() + } }) } - .await - .change_context(errors::ApiErrorResponse::InternalServerError)?; +} - // Create a default business profile - // If business_labels are passed, then use it as the profile_name - // else use `default` as the profile_name - if let Some(business_details) = req.primary_business_details.as_ref() { - for business_profile in business_details { +#[cfg(all(not(feature = "v2"), feature = "olap"))] +enum CreateBusinessProfile { + /// Create business profiles from primary business details + /// If there is only one business profile created, then set this profile as default + CreateFromPrimaryBusinessDetails { + primary_business_details: Vec, + }, + /// Create a default business profile, set this as default profile + CreateDefaultBusinessProfile, +} + +#[cfg(all(not(feature = "v2"), feature = "olap"))] +impl CreateBusinessProfile { + /// Create a new business profile action from the given information + /// If primary business details exist, then create business profiles from them + /// If primary business details are empty, then create default business profile + fn new(primary_business_details: Option>) -> Self { + match primary_business_details { + Some(primary_business_details) if !primary_business_details.is_empty() => { + Self::CreateFromPrimaryBusinessDetails { + primary_business_details, + } + } + _ => Self::CreateDefaultBusinessProfile, + } + } + + async fn create_business_profiles( + &self, + db: &dyn StorageInterface, + merchant_account: &mut domain::MerchantAccount, + ) -> RouterResult<()> { + match self { + Self::CreateFromPrimaryBusinessDetails { + primary_business_details, + } => { + let business_profiles = Self::create_business_profiles_for_each_business_details( + db, + merchant_account.clone(), + primary_business_details, + ) + .await?; + + // Update the default business profile in merchant account + if business_profiles.len() == 1 { + merchant_account.default_profile = business_profiles + .first() + .map(|business_profile| business_profile.profile_id.clone()) + } + } + Self::CreateDefaultBusinessProfile => { + let business_profile = self + .create_default_business_profile(db, merchant_account.clone()) + .await?; + + merchant_account.default_profile = Some(business_profile.profile_id); + } + } + + Ok(()) + } + + /// Create default business profile + async fn create_default_business_profile( + &self, + db: &dyn StorageInterface, + merchant_account: domain::MerchantAccount, + ) -> RouterResult { + let business_profile = create_and_insert_business_profile( + db, + api_models::admin::BusinessProfileCreate::default(), + merchant_account.clone(), + ) + .await?; + + Ok(business_profile) + } + + /// Create business profile for each primary_business_details, + /// If there is no default profile in merchant account and only one primary_business_detail + /// is available, then create a default business profile. + async fn create_business_profiles_for_each_business_details( + db: &dyn StorageInterface, + merchant_account: domain::MerchantAccount, + primary_business_details: &Vec, + ) -> RouterResult> { + let mut business_profiles_vector = Vec::with_capacity(primary_business_details.len()); + + // This must ideally be run in a transaction, + // if there is an error in inserting some business profile, because of unique constraints + // the whole query must be rolled back + for business_profile in primary_business_details { let profile_name = format!("{}_{}", business_profile.country, business_profile.business); @@ -247,7 +434,7 @@ pub async fn create_merchant_account( ..Default::default() }; - let _ = create_and_insert_business_profile( + create_and_insert_business_profile( db, business_profile_create_request, merchant_account.clone(), @@ -258,63 +445,101 @@ pub async fn create_merchant_account( "Business profile already exists {business_profile_insert_error:?}" ); }) - .map(|business_profile| { - if business_details.len() == 1 && merchant_account.default_profile.is_none() { - merchant_account.default_profile = Some(business_profile.profile_id); - } - }); + .map(|business_profile| business_profiles_vector.push(business_profile)) + .ok(); } - } else { - let business_profile = create_and_insert_business_profile( - db, - api_models::admin::BusinessProfileCreate::default(), - merchant_account.clone(), - ) - .await?; - // Update merchant account with the business profile id - merchant_account.default_profile = Some(business_profile.profile_id); - }; - - let merchant_account = db - .insert_merchant(merchant_account, &key_store) - .await - .to_duplicate_response(errors::ApiErrorResponse::DuplicateMerchantAccount)?; - - if let Some(api_key) = merchant_account.publishable_key.as_ref() { - let state = state.clone(); - let api_key = api_key.clone(); - let merchant_id = merchant_account.merchant_id.clone(); - - authentication::decision::spawn_tracked_job( - async move { - authentication::decision::add_publishable_key( - &state, - api_key.into(), - merchant_id, - None, - ) - .await - }, - authentication::decision::ADD, - ); + Ok(business_profiles_vector) } +} - db.insert_config(configs::ConfigNew { - key: format!("{}_requires_cvv", merchant_account.merchant_id), - config: "true".to_string(), - }) - .await - .map_err(|err| { - crate::logger::error!("Error while setting requires_cvv config: {err:?}"); - }) - .ok(); +#[cfg(all(feature = "v2", feature = "olap"))] +#[async_trait::async_trait] +impl MerchantAccountCreateBridge for api::MerchantAccountCreate { + async fn create_domain_model_from_request( + self, + db: &dyn StorageInterface, + key_store: domain::MerchantKeyStore, + ) -> RouterResult { + let publishable_key = create_merchant_publishable_key(); - Ok(service_api::ApplicationResponse::Json( - api::MerchantAccountResponse::try_from(merchant_account) - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed while generating response")?, - )) + let metadata = self.get_metadata_as_secret().change_context( + errors::ApiErrorResponse::InvalidDataValue { + field_name: "metadata", + }, + )?; + + let merchant_details = self.get_merchant_details_as_secret().change_context( + errors::ApiErrorResponse::InvalidDataValue { + field_name: "merchant_details", + }, + )?; + + let primary_business_details = self.get_primary_details_as_value().change_context( + errors::ApiErrorResponse::InvalidDataValue { + field_name: "primary_business_details", + }, + )?; + + CreateOrValidateOrganization::new(self.organization_id.clone()) + .create_or_validate(db) + .await?; + + let key = key_store.key.into_inner(); + + async { + Ok::<_, error_stack::Report>( + domain::MerchantAccount { + merchant_id: self + .get_merchant_reference_id() + .get_string_repr() + .to_owned(), + merchant_name: Some( + domain_types::encrypt( + self.merchant_name + .map(|merchant_name| merchant_name.into_inner()), + key.peek(), + ) + .await?, + ), + merchant_details: merchant_details + .async_lift(|inner| domain_types::encrypt_optional(inner, key.peek())) + .await?, + return_url: None, + webhook_details: None, + routing_algorithm: Some(serde_json::json!({ + "algorithm_id": null, + "timestamp": 0 + })), + sub_merchants_enabled: None, + parent_merchant_id: None, + enable_payment_response_hash: true, + payment_response_hash_key: None, + redirect_to_merchant_with_http_post: true, + publishable_key, + locker_id: None, + metadata, + storage_scheme: MerchantStorageScheme::PostgresOnly, + primary_business_details, + created_at: date_time::now(), + modified_at: date_time::now(), + intent_fulfillment_time: None, + frm_routing_algorithm: None, + payout_routing_algorithm: None, + id: None, + organization_id: self.organization_id, + is_recon_enabled: false, + default_profile: None, + recon_status: diesel_models::enums::ReconStatus::NotRequested, + payment_link_config: None, + pm_collect_link_config: None, + }, + ) + } + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("failed to encrypt merchant details") + } } #[cfg(feature = "olap")] @@ -331,7 +556,7 @@ pub async fn list_merchant_account( let merchant_accounts = merchant_accounts .into_iter() .map(|merchant_account| { - api::MerchantAccountResponse::try_from(merchant_account).change_context( + api::MerchantAccountResponse::foreign_try_from(merchant_account).change_context( errors::ApiErrorResponse::InvalidDataValue { field_name: "merchant_account", }, @@ -361,7 +586,7 @@ pub async fn get_merchant_account( .to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)?; Ok(service_api::ApplicationResponse::Json( - api::MerchantAccountResponse::try_from(merchant_account) + api::MerchantAccountResponse::foreign_try_from(merchant_account) .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to construct response")?, )) @@ -657,7 +882,7 @@ pub async fn merchant_account_update( // If there are any new business labels generated, create business profile Ok(service_api::ApplicationResponse::Json( - api::MerchantAccountResponse::try_from(response) + api::MerchantAccountResponse::foreign_try_from(response) .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed while generating response")?, )) @@ -695,13 +920,17 @@ pub async fn merchant_account_delete( is_deleted = is_merchant_account_deleted && is_merchant_key_store_deleted; } - if let Some(api_key) = merchant_account.publishable_key { - let state = state.clone(); - authentication::decision::spawn_tracked_job( - async move { authentication::decision::revoke_api_key(&state, api_key.into()).await }, - authentication::decision::REVOKE, - ) - } + let state = state.clone(); + authentication::decision::spawn_tracked_job( + async move { + authentication::decision::revoke_api_key( + &state, + merchant_account.publishable_key.into(), + ) + .await + }, + authentication::decision::REVOKE, + ); match db .delete_config_by_key(format!("{}_requires_cvv", merchant_id).as_str()) diff --git a/crates/router/src/core/blocklist/utils.rs b/crates/router/src/core/blocklist/utils.rs index 118dbb3f66..baa2e51404 100644 --- a/crates/router/src/core/blocklist/utils.rs +++ b/crates/router/src/core/blocklist/utils.rs @@ -210,7 +210,7 @@ pub async fn get_merchant_fingerprint_secret( state: &SessionState, merchant_id: &str, ) -> RouterResult { - let key = get_merchant_fingerprint_secret_key(merchant_id); + let key = utils::get_merchant_fingerprint_secret_key(merchant_id); let config_fetch_result = state.store.find_config_by_key(&key).await; match config_fetch_result { @@ -240,10 +240,6 @@ pub async fn get_merchant_fingerprint_secret( } } -fn get_merchant_fingerprint_secret_key(merchant_id: &str) -> String { - format!("fingerprint_secret_{merchant_id}") -} - async fn duplicate_check_insert_bin( bin: &str, state: &SessionState, diff --git a/crates/router/src/core/payment_link.rs b/crates/router/src/core/payment_link.rs index e9f9a6b401..49e4885dc9 100644 --- a/crates/router/src/core/payment_link.rs +++ b/crates/router/src/core/payment_link.rs @@ -119,8 +119,7 @@ pub async fn initiate_payment_link_flow( })? }; - let (pub_key, currency, client_secret) = validate_sdk_requirements( - merchant_account.publishable_key, + let (currency, client_secret) = validate_sdk_requirements( payment_intent.currency, payment_intent.client_secret.clone(), )?; @@ -229,7 +228,7 @@ pub async fn initiate_payment_link_flow( order_details, return_url, session_expiry, - pub_key, + pub_key: merchant_account.publishable_key, client_secret, merchant_logo: payment_link_config.logo.clone(), max_items_visible_after_collapse: 3, @@ -294,14 +293,9 @@ fn get_meta_tags_html(payment_details: api_models::payments::PaymentLinkDetails) } fn validate_sdk_requirements( - pub_key: Option, currency: Option, client_secret: Option, -) -> Result<(String, api_models::enums::Currency, String), errors::ApiErrorResponse> { - let pub_key = pub_key.ok_or(errors::ApiErrorResponse::MissingRequiredField { - field_name: "pub_key", - })?; - +) -> Result<(api_models::enums::Currency, String), errors::ApiErrorResponse> { let currency = currency.ok_or(errors::ApiErrorResponse::MissingRequiredField { field_name: "currency", })?; @@ -309,7 +303,7 @@ fn validate_sdk_requirements( let client_secret = client_secret.ok_or(errors::ApiErrorResponse::MissingRequiredField { field_name: "client_secret", })?; - Ok((pub_key, currency, client_secret)) + Ok((currency, client_secret)) } pub async fn list_payment_link( diff --git a/crates/router/src/core/payment_methods.rs b/crates/router/src/core/payment_methods.rs index e31a97b363..6fcd27fefd 100644 --- a/crates/router/src/core/payment_methods.rs +++ b/crates/router/src/core/payment_methods.rs @@ -275,12 +275,7 @@ pub async fn render_pm_collect_link( ))?; let js_data = payment_methods::PaymentMethodCollectLinkDetails { - publishable_key: merchant_account - .publishable_key - .ok_or(errors::ApiErrorResponse::MissingRequiredField { - field_name: "publishable_key", - })? - .into(), + 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.customer_id, diff --git a/crates/router/src/core/payout_link.rs b/crates/router/src/core/payout_link.rs index 0039b01986..d500667877 100644 --- a/crates/router/src/core/payout_link.rs +++ b/crates/router/src/core/payout_link.rs @@ -150,12 +150,7 @@ pub async fn initiate_payout_link( .unwrap_or(fallback_enabled_payout_methods.to_vec()); let js_data = payouts::PayoutLinkDetails { - publishable_key: merchant_account - .publishable_key - .ok_or(errors::ApiErrorResponse::MissingRequiredField { - field_name: "publishable_key", - })? - .into(), + publishable_key: masking::Secret::new(merchant_account.publishable_key), client_secret: link_data.client_secret.clone(), payout_link_id: payout_link.link_id, payout_id: payout_link.primary_reference, diff --git a/crates/router/src/lib.rs b/crates/router/src/lib.rs index 10563cc575..e07434f9bb 100644 --- a/crates/router/src/lib.rs +++ b/crates/router/src/lib.rs @@ -174,7 +174,7 @@ pub fn mk_app( server_app = server_app.service(routes::Cards::server(state.clone())); server_app = server_app.service(routes::Cache::server(state.clone())); - server_app = server_app.service(routes::Health::server(state)); + server_app = server_app.service(routes::Health::server(state.clone())); server_app } diff --git a/crates/router/src/routes/admin.rs b/crates/router/src/routes/admin.rs index cd39275650..3f08834e44 100644 --- a/crates/router/src/routes/admin.rs +++ b/crates/router/src/routes/admin.rs @@ -8,21 +8,7 @@ use crate::{ types::api::admin, }; -/// Merchant Account - Create -/// -/// Create a new account for a merchant and the merchant could be a seller or retailer or client who likes to receive and send payments. -#[utoipa::path( - post, - path = "/accounts", - request_body= MerchantAccountCreate, - responses( - (status = 200, description = "Merchant Account Created", body = MerchantAccountResponse), - (status = 400, description = "Invalid data") - ), - tag = "Merchant Account", - operation_id = "Create a Merchant Account", - security(("admin_api_key" = [])) -)] +#[cfg(feature = "olap")] #[instrument(skip_all, fields(flow = ?Flow::MerchantsAccountCreate))] pub async fn merchant_account_create( state: web::Data, @@ -41,6 +27,7 @@ pub async fn merchant_account_create( )) .await } + /// Merchant Account - Retrieve /// /// Retrieve a merchant account details. diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 4fad6f863e..5a1a924a4e 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -1004,7 +1004,16 @@ impl Blocklist { pub struct MerchantAccount; -#[cfg(feature = "olap")] +#[cfg(all(feature = "v2", feature = "olap"))] +impl MerchantAccount { + pub fn server(state: AppState) -> Scope { + web::scope("/v2/accounts") + .app_data(web::Data::new(state)) + .service(web::resource("").route(web::post().to(merchant_account_create))) + } +} + +#[cfg(all(feature = "olap", not(feature = "v2")))] impl MerchantAccount { pub fn server(state: AppState) -> Scope { web::scope("/accounts") diff --git a/crates/router/src/routes/recon.rs b/crates/router/src/routes/recon.rs index 5aac419a22..aabbd637ae 100644 --- a/crates/router/src/routes/recon.rs +++ b/crates/router/src/routes/recon.rs @@ -20,6 +20,7 @@ use crate::{ api::{self as api_types, enums}, domain::{UserEmail, UserFromStorage, UserName}, storage, + transformers::ForeignTryFrom, }, }; @@ -209,7 +210,7 @@ pub async fn recon_merchant_account_update( } Ok(service_api::ApplicationResponse::Json( - api_types::MerchantAccountResponse::try_from(response).change_context( + api_types::MerchantAccountResponse::foreign_try_from(response).change_context( errors::ApiErrorResponse::InvalidDataValue { field_name: "merchant_account", }, diff --git a/crates/router/src/types/api/admin.rs b/crates/router/src/types/api/admin.rs index cb801b934e..9aa2c95f55 100644 --- a/crates/router/src/types/api/admin.rs +++ b/crates/router/src/types/api/admin.rs @@ -15,9 +15,10 @@ use crate::{ types::{domain, storage, transformers::ForeignTryFrom}, }; -impl TryFrom for MerchantAccountResponse { +#[cfg(not(feature = "v2"))] +impl ForeignTryFrom for MerchantAccountResponse { type Error = error_stack::Report; - fn try_from(item: domain::MerchantAccount) -> Result { + fn foreign_try_from(item: domain::MerchantAccount) -> Result { let primary_business_details: Vec = item .primary_business_details .parse_value("primary_business_details")?; @@ -39,7 +40,7 @@ impl TryFrom for MerchantAccountResponse { routing_algorithm: item.routing_algorithm, sub_merchants_enabled: item.sub_merchants_enabled, parent_merchant_id: item.parent_merchant_id, - publishable_key: item.publishable_key, + publishable_key: Some(item.publishable_key), metadata: item.metadata, locker_id: item.locker_id, primary_business_details, @@ -55,6 +56,30 @@ impl TryFrom for MerchantAccountResponse { } } +#[cfg(feature = "v2")] +impl ForeignTryFrom for MerchantAccountResponse { + type Error = error_stack::Report; + fn foreign_try_from(item: domain::MerchantAccount) -> Result { + use common_utils::ext_traits::OptionExt; + + let merchant_name = item + .merchant_name + .get_required_value("merchant_name")? + .into_inner(); + + Ok(Self { + id: item.merchant_id, + merchant_name, + merchant_details: item.merchant_details, + publishable_key: item.publishable_key, + metadata: item.metadata, + organization_id: item.organization_id, + is_recon_enabled: item.is_recon_enabled, + recon_status: item.recon_status, + }) + } +} + impl ForeignTryFrom for BusinessProfileResponse { type Error = error_stack::Report; diff --git a/crates/router/src/types/domain.rs b/crates/router/src/types/domain.rs index be93f30bfb..041f7517f0 100644 --- a/crates/router/src/types/domain.rs +++ b/crates/router/src/types/domain.rs @@ -3,9 +3,14 @@ pub mod behaviour { pub use hyperswitch_domain_models::behaviour::{Conversion, ReverseConversion}; } +pub mod merchant_account { + pub use hyperswitch_domain_models::merchant_account::*; +} + +pub use merchant_account::*; + mod customer; mod event; -mod merchant_account; mod merchant_connector_account; mod merchant_key_store { pub use hyperswitch_domain_models::merchant_key_store::MerchantKeyStore; @@ -19,7 +24,6 @@ pub mod user_key_store; pub use address::*; pub use customer::*; pub use event::*; -pub use merchant_account::*; pub use merchant_connector_account::*; pub use merchant_key_store::*; pub use payments::*; diff --git a/crates/router/src/types/domain/merchant_account.rs b/crates/router/src/types/domain/merchant_account.rs index aa1faeab67..e69de29bb2 100644 --- a/crates/router/src/types/domain/merchant_account.rs +++ b/crates/router/src/types/domain/merchant_account.rs @@ -1,290 +0,0 @@ -use common_utils::{ - crypto::{OptionalEncryptableName, OptionalEncryptableValue}, - date_time, - ext_traits::ValueExt, - pii, -}; -use diesel_models::{ - encryption::Encryption, enums::MerchantStorageScheme, - merchant_account::MerchantAccountUpdateInternal, -}; -use error_stack::ResultExt; -use masking::{PeekInterface, Secret}; -use router_env::logger; - -use crate::{ - errors::{CustomResult, ValidationError}, - types::domain::types::{self, AsyncLift}, -}; - -#[derive(Clone, Debug, serde::Serialize)] -pub struct MerchantAccount { - pub id: Option, - pub merchant_id: String, - pub return_url: Option, - pub enable_payment_response_hash: bool, - pub payment_response_hash_key: Option, - pub redirect_to_merchant_with_http_post: bool, - pub merchant_name: OptionalEncryptableName, - pub merchant_details: OptionalEncryptableValue, - pub webhook_details: Option, - pub sub_merchants_enabled: Option, - pub parent_merchant_id: Option, - pub publishable_key: Option, - pub storage_scheme: MerchantStorageScheme, - pub locker_id: Option, - pub metadata: Option, - pub routing_algorithm: Option, - pub primary_business_details: serde_json::Value, - pub frm_routing_algorithm: Option, - pub created_at: time::PrimitiveDateTime, - pub modified_at: time::PrimitiveDateTime, - pub intent_fulfillment_time: Option, - pub payout_routing_algorithm: Option, - pub organization_id: String, - pub is_recon_enabled: bool, - pub default_profile: Option, - pub recon_status: diesel_models::enums::ReconStatus, - pub payment_link_config: Option, - pub pm_collect_link_config: Option, -} - -#[allow(clippy::large_enum_variant)] -#[derive(Debug)] -pub enum MerchantAccountUpdate { - Update { - merchant_name: OptionalEncryptableName, - merchant_details: OptionalEncryptableValue, - return_url: Option, - webhook_details: Option, - sub_merchants_enabled: Option, - parent_merchant_id: Option, - enable_payment_response_hash: Option, - payment_response_hash_key: Option, - redirect_to_merchant_with_http_post: Option, - publishable_key: Option, - locker_id: Option, - metadata: Option, - routing_algorithm: Option, - primary_business_details: Option, - intent_fulfillment_time: Option, - frm_routing_algorithm: Option, - payout_routing_algorithm: Option, - default_profile: Option>, - payment_link_config: Option, - pm_collect_link_config: Option, - }, - StorageSchemeUpdate { - storage_scheme: MerchantStorageScheme, - }, - ReconUpdate { - recon_status: diesel_models::enums::ReconStatus, - }, - UnsetDefaultProfile, - ModifiedAtUpdate, -} - -impl From for MerchantAccountUpdateInternal { - fn from(merchant_account_update: MerchantAccountUpdate) -> Self { - match merchant_account_update { - MerchantAccountUpdate::Update { - merchant_name, - merchant_details, - return_url, - webhook_details, - routing_algorithm, - sub_merchants_enabled, - parent_merchant_id, - enable_payment_response_hash, - payment_response_hash_key, - redirect_to_merchant_with_http_post, - publishable_key, - locker_id, - metadata, - primary_business_details, - intent_fulfillment_time, - frm_routing_algorithm, - payout_routing_algorithm, - default_profile, - payment_link_config, - pm_collect_link_config, - } => Self { - merchant_name: merchant_name.map(Encryption::from), - merchant_details: merchant_details.map(Encryption::from), - frm_routing_algorithm, - return_url, - webhook_details, - routing_algorithm, - sub_merchants_enabled, - parent_merchant_id, - enable_payment_response_hash, - payment_response_hash_key, - redirect_to_merchant_with_http_post, - publishable_key, - locker_id, - metadata, - primary_business_details, - modified_at: Some(date_time::now()), - intent_fulfillment_time, - payout_routing_algorithm, - default_profile, - payment_link_config, - pm_collect_link_config, - ..Default::default() - }, - MerchantAccountUpdate::StorageSchemeUpdate { storage_scheme } => Self { - storage_scheme: Some(storage_scheme), - modified_at: Some(date_time::now()), - ..Default::default() - }, - MerchantAccountUpdate::ReconUpdate { recon_status } => Self { - recon_status: Some(recon_status), - ..Default::default() - }, - MerchantAccountUpdate::UnsetDefaultProfile => Self { - default_profile: Some(None), - ..Default::default() - }, - MerchantAccountUpdate::ModifiedAtUpdate => Self { - modified_at: Some(date_time::now()), - ..Default::default() - }, - } - } -} - -#[async_trait::async_trait] -impl super::behaviour::Conversion for MerchantAccount { - type DstType = diesel_models::merchant_account::MerchantAccount; - type NewDstType = diesel_models::merchant_account::MerchantAccountNew; - async fn convert(self) -> CustomResult { - Ok(diesel_models::merchant_account::MerchantAccount { - id: self.id.ok_or(ValidationError::MissingRequiredField { - field_name: "id".to_string(), - })?, - merchant_id: self.merchant_id, - return_url: self.return_url, - enable_payment_response_hash: self.enable_payment_response_hash, - payment_response_hash_key: self.payment_response_hash_key, - redirect_to_merchant_with_http_post: self.redirect_to_merchant_with_http_post, - merchant_name: self.merchant_name.map(|name| name.into()), - merchant_details: self.merchant_details.map(|details| details.into()), - webhook_details: self.webhook_details, - sub_merchants_enabled: self.sub_merchants_enabled, - parent_merchant_id: self.parent_merchant_id, - publishable_key: self.publishable_key, - storage_scheme: self.storage_scheme, - locker_id: self.locker_id, - metadata: self.metadata, - routing_algorithm: self.routing_algorithm, - primary_business_details: self.primary_business_details, - created_at: self.created_at, - modified_at: self.modified_at, - intent_fulfillment_time: self.intent_fulfillment_time, - frm_routing_algorithm: self.frm_routing_algorithm, - payout_routing_algorithm: self.payout_routing_algorithm, - organization_id: self.organization_id, - is_recon_enabled: self.is_recon_enabled, - default_profile: self.default_profile, - recon_status: self.recon_status, - payment_link_config: self.payment_link_config, - pm_collect_link_config: self.pm_collect_link_config, - }) - } - - async fn convert_back( - item: Self::DstType, - key: &Secret>, - ) -> CustomResult - where - Self: Sized, - { - async { - Ok::>(Self { - id: Some(item.id), - merchant_id: item.merchant_id, - return_url: item.return_url, - enable_payment_response_hash: item.enable_payment_response_hash, - payment_response_hash_key: item.payment_response_hash_key, - redirect_to_merchant_with_http_post: item.redirect_to_merchant_with_http_post, - merchant_name: item - .merchant_name - .async_lift(|inner| types::decrypt(inner, key.peek())) - .await?, - merchant_details: item - .merchant_details - .async_lift(|inner| types::decrypt(inner, key.peek())) - .await?, - webhook_details: item.webhook_details, - sub_merchants_enabled: item.sub_merchants_enabled, - parent_merchant_id: item.parent_merchant_id, - publishable_key: item.publishable_key, - storage_scheme: item.storage_scheme, - locker_id: item.locker_id, - metadata: item.metadata, - routing_algorithm: item.routing_algorithm, - frm_routing_algorithm: item.frm_routing_algorithm, - primary_business_details: item.primary_business_details, - created_at: item.created_at, - modified_at: item.modified_at, - intent_fulfillment_time: item.intent_fulfillment_time, - payout_routing_algorithm: item.payout_routing_algorithm, - organization_id: item.organization_id, - is_recon_enabled: item.is_recon_enabled, - default_profile: item.default_profile, - recon_status: item.recon_status, - payment_link_config: item.payment_link_config, - pm_collect_link_config: item.pm_collect_link_config, - }) - } - .await - .change_context(ValidationError::InvalidValue { - message: "Failed while decrypting merchant data".to_string(), - }) - } - - async fn construct_new(self) -> CustomResult { - let now = date_time::now(); - Ok(diesel_models::merchant_account::MerchantAccountNew { - merchant_id: self.merchant_id, - merchant_name: self.merchant_name.map(Encryption::from), - merchant_details: self.merchant_details.map(Encryption::from), - return_url: self.return_url, - webhook_details: self.webhook_details, - sub_merchants_enabled: self.sub_merchants_enabled, - parent_merchant_id: self.parent_merchant_id, - enable_payment_response_hash: Some(self.enable_payment_response_hash), - payment_response_hash_key: self.payment_response_hash_key, - redirect_to_merchant_with_http_post: Some(self.redirect_to_merchant_with_http_post), - publishable_key: self.publishable_key, - locker_id: self.locker_id, - metadata: self.metadata, - routing_algorithm: self.routing_algorithm, - primary_business_details: self.primary_business_details, - created_at: now, - modified_at: now, - intent_fulfillment_time: self.intent_fulfillment_time, - frm_routing_algorithm: self.frm_routing_algorithm, - payout_routing_algorithm: self.payout_routing_algorithm, - organization_id: self.organization_id, - is_recon_enabled: self.is_recon_enabled, - default_profile: self.default_profile, - recon_status: self.recon_status, - payment_link_config: self.payment_link_config, - pm_collect_link_config: self.pm_collect_link_config, - }) - } -} - -impl MerchantAccount { - pub fn get_compatible_connector(&self) -> Option { - let metadata: Option = - self.metadata.as_ref().and_then(|meta| { - meta.clone() - .parse_value("MerchantAccountMetadata") - .map_err(|err| logger::error!("Failed to deserialize {:?}", err)) - .ok() - }); - metadata.and_then(|a| a.compatible_connector) - } -} diff --git a/crates/router/src/types/domain/user.rs b/crates/router/src/types/domain/user.rs index 9894beb54c..4c48c21204 100644 --- a/crates/router/src/types/domain/user.rs +++ b/crates/router/src/types/domain/user.rs @@ -4,7 +4,9 @@ use api_models::{ admin as admin_api, organization as api_org, user as user_api, user_role as user_role_api, }; use common_enums::TokenPurpose; -use common_utils::{crypto::Encryptable, errors::CustomResult, pii}; +#[cfg(not(feature = "v2"))] +use common_utils::id_type; +use common_utils::{crypto::Encryptable, errors::CustomResult, new_type::MerchantName, pii}; use diesel_models::{ enums::{TotpStatus, UserStatus}, organization as diesel_org, @@ -337,6 +339,15 @@ pub struct NewUserMerchant { new_organization: NewUserOrganization, } +impl TryFrom for MerchantName { + // We should ideally not get this error because all the validations are done for company name + type Error = error_stack::Report; + + fn try_from(company_name: UserCompanyName) -> Result { + Self::new(company_name.get_secret()).change_context(UserErrors::CompanyNameParsingError) + } +} + impl NewUserMerchant { pub fn get_company_name(&self) -> Option { self.company_name.clone().map(UserCompanyName::get_secret) @@ -369,35 +380,68 @@ impl NewUserMerchant { Ok(()) } + #[cfg(feature = "v2")] + fn create_merchant_account_request(&self) -> UserResult { + let merchant_name = if let Some(company_name) = self.company_name.clone() { + MerchantName::try_from(company_name) + } else { + MerchantName::new("merchant".to_string()) + .change_context(UserErrors::InternalServerError) + .attach_printable("merchant name validation failed") + } + .map(Secret::new)?; + + Ok(admin_api::MerchantAccountCreate { + merchant_name, + organization_id: self.new_organization.get_organization_id(), + metadata: None, + merchant_details: None, + }) + } + + #[cfg(not(feature = "v2"))] + fn create_merchant_account_request(&self) -> UserResult { + Ok(admin_api::MerchantAccountCreate { + merchant_id: id_type::MerchantId::from(self.get_merchant_id().into()) + .change_context(UserErrors::MerchantIdParsingError) + .attach_printable( + "Unable to convert to MerchantId type because of constraint violations", + )?, + metadata: None, + locker_id: None, + return_url: None, + merchant_name: self.get_company_name().map(Secret::new), + webhook_details: None, + publishable_key: None, + organization_id: Some(self.new_organization.get_organization_id()), + merchant_details: None, + routing_algorithm: None, + parent_merchant_id: None, + sub_merchants_enabled: None, + frm_routing_algorithm: None, + #[cfg(feature = "payouts")] + payout_routing_algorithm: None, + primary_business_details: None, + payment_response_hash_key: None, + enable_payment_response_hash: None, + redirect_to_merchant_with_http_post: None, + pm_collect_link_config: None, + }) + } + pub async fn create_new_merchant_and_insert_in_db( &self, state: SessionState, ) -> UserResult<()> { self.check_if_already_exists_in_db(state.clone()).await?; + + let merchant_account_create_request = self + .create_merchant_account_request() + .attach_printable("unable to construct merchant account create request")?; + Box::pin(admin::create_merchant_account( state.clone(), - admin_api::MerchantAccountCreate { - merchant_id: self.get_merchant_id(), - metadata: None, - locker_id: None, - return_url: None, - merchant_name: self.get_company_name().map(Secret::new), - webhook_details: None, - publishable_key: None, - organization_id: Some(self.new_organization.get_organization_id()), - merchant_details: None, - routing_algorithm: None, - parent_merchant_id: None, - sub_merchants_enabled: None, - frm_routing_algorithm: None, - #[cfg(feature = "payouts")] - payout_routing_algorithm: None, - primary_business_details: None, - payment_response_hash_key: None, - enable_payment_response_hash: None, - redirect_to_merchant_with_http_post: None, - pm_collect_link_config: None, - }, + merchant_account_create_request, )) .await .change_context(UserErrors::InternalServerError) diff --git a/crates/router/src/utils.rs b/crates/router/src/utils.rs index 9662220d43..64e2f534b7 100644 --- a/crates/router/src/utils.rs +++ b/crates/router/src/utils.rs @@ -1007,6 +1007,10 @@ pub async fn flatten_join_error(handle: Handle) -> RouterResult { } } +pub(crate) fn get_merchant_fingerprint_secret_key(merchant_id: impl AsRef) -> String { + format!("fingerprint_secret_{}", merchant_id.as_ref()) +} + #[cfg(test)] mod tests { use crate::utils;