feat(merchant_account): add merchant account create v2 route (#5061)

Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
Co-authored-by: Arun Raj M <jarnura47@gmail.com>
Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Sanchith Hegde <22217505+SanchithHegde@users.noreply.github.com>
Co-authored-by: Chethan Rao <70657455+Chethan-rao@users.noreply.github.com>
Co-authored-by: Sarthak Soni <76486416+Sarthak1799@users.noreply.github.com>
Co-authored-by: Sandeep Kumar <83278309+tsdk02@users.noreply.github.com>
Co-authored-by: Abhitator216 <abhishek.kanojia@juspay.in>
Co-authored-by: Abhishek Kanojia <89402434+Abhitator216@users.noreply.github.com>
Co-authored-by: ivor-juspay <138492857+ivor-juspay@users.noreply.github.com>
Co-authored-by: Sampras Lopes <sampras.lopes@juspay.in>
Co-authored-by: Pa1NarK <69745008+pixincreate@users.noreply.github.com>
Co-authored-by: likhinbopanna <131246334+likhinbopanna@users.noreply.github.com>
Co-authored-by: Sahkal Poddar <sahkalplanet@gmail.com>
Co-authored-by: Hrithikesh <61539176+hrithikesh026@users.noreply.github.com>
Co-authored-by: Shankar Singh C <83439957+ShankarSinghC@users.noreply.github.com>
Co-authored-by: SamraatBansal <55536657+SamraatBansal@users.noreply.github.com>
Co-authored-by: GORAKHNATH YADAV <gorakhcodes@gmail.com>
Co-authored-by: Apoorv Dixit <64925866+apoorvdixit88@users.noreply.github.com>
Co-authored-by: ShivanshMathurJuspay <104988143+ShivanshMathurJuspay@users.noreply.github.com>
Co-authored-by: awasthi21 <107559116+awasthi21@users.noreply.github.com>
Co-authored-by: Prajjwal Kumar <prajjwal.kumar@juspay.in>
This commit is contained in:
Narayan Bhat
2024-07-10 13:15:36 +05:30
committed by GitHub
parent fa7add19ee
commit d6b9151e9e
36 changed files with 1568 additions and 972 deletions

47
Cargo.lock generated
View File

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

View File

@ -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": {

View File

@ -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<MetricsResponse<MetricsBucketResponse>> {
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<MetricsBucketResponse> = 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<MetricsBucketResponse> = 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,
}],
})
}

View File

@ -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<MetricsResponse<MetricsBucketResponse>> {
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<MetricsBucketResponse> = 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<MetricsBucketResponse> = 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,
}],
})
}

View File

@ -26,17 +26,17 @@ use crate::{
pub async fn sdk_events_core(
pool: &AnalyticsProvider,
req: SdkEventsRequest,
publishable_key: String,
publishable_key: &str,
) -> AnalyticsResult<Vec<SdkEventsResult>> {
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<MetricsResponse<MetricsBucketResponse>> {
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<MetricsBucketResponse> = 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<MetricsBucketResponse> = 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<SdkEventFiltersResponse> {
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::<Vec<String>>();
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::<Vec<String>>();
res.query_data.push(SdkEventFilterValue {
dimension: dim,
values,
})
}
Ok(res)

View File

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

View File

@ -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<String>,example = "NewAge Retailer")]
pub merchant_name: Option<Secret<String>>,
/// Details about the merchant
/// Details about the merchant, can contain phone and emails of primary and secondary contact person
pub merchant_details: Option<MerchantDetails>,
/// 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<bool>,
/// 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<Object>, example = r#"{ "city": "NY", "unit": "245" }"#)]
pub metadata: Option<MerchantAccountMetadata>,
@ -93,7 +100,7 @@ pub struct MerchantAccountCreate {
#[schema(value_type = Option<Object>,example = json!({"type": "single", "data": "signifyd"}))]
pub frm_routing_algorithm: Option<serde_json::Value>,
/// 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<String>,
/// Default payment method collect link config
@ -101,11 +108,135 @@ pub struct MerchantAccountCreate {
pub pm_collect_link_config: Option<BusinessCollectLinkConfig>,
}
#[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<String> {
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<serde_json::Value, errors::ParsingError> {
self.primary_business_details
.clone()
.unwrap_or_default()
.encode_to_value()
}
pub fn get_pm_link_config_as_value(
&self,
) -> CustomResult<Option<serde_json::Value>, 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<Option<pii::SecretSerdeValue>, 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<Option<pii::SecretSerdeValue>, 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<Option<serde_json::Value>, 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<new_type::MerchantName>,
/// Details about the merchant, contains phone and emails of primary and secondary contact person.
pub merchant_details: Option<MerchantDetails>,
/// Metadata is useful for storing additional, unstructured information about the merchant account.
#[schema(value_type = Option<Object>, example = r#"{ "city": "NY", "unit": "245" }"#)]
pub metadata: Option<MerchantAccountMetadata>,
/// 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<Option<pii::SecretSerdeValue>, 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<Option<pii::SecretSerdeValue>, 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<serde_json::Value, errors::ParsingError> {
Vec::<PrimaryBusinessDetails>::new().encode_to_value()
}
}
#[derive(Clone, Debug, Deserialize, Serialize, ToSchema)]
pub struct AuthenticationConnectorDetails {
/// List of authentication connectors
#[schema(value_type = Vec<AuthenticationConnectors>)]
pub authentication_connectors: Vec<enums::AuthenticationConnectors>,
pub authentication_connectors: Vec<api_enums::AuthenticationConnectors>,
/// 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<BusinessCollectLinkConfig>,
}
#[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<String>,
/// 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<Object>, example = r#"{ "city": "NY", "unit": "245" }"#)]
pub metadata: Option<pii::SecretSerdeValue>,
@ -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<BusinessCollectLinkConfig>)]
pub pm_collect_link_config: Option<BusinessCollectLinkConfig>,
}
#[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<String>,
/// Details about the merchant
#[schema(value_type = Option<MerchantDetails>)]
pub merchant_details: Option<Encryptable<pii::SecretSerdeValue>>,
/// 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<Object>, example = r#"{ "city": "NY", "unit": "245" }"#)]
pub metadata: Option<pii::SecretSerdeValue>,
/// 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 {

View File

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

View File

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

View File

@ -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<char> {
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<const MAX_LENGTH: u8, const MIN_LENGTH: u8>(AlphaNumericId);
pub(crate) struct LengthId<const MAX_LENGTH: u8, const MIN_LENGTH: u8>(AlphaNumericId);
/// Error generated from violation of constraints for MerchantReferenceId
#[derive(Debug, Deserialize, Serialize, Error, PartialEq, Eq)]
pub(crate) enum MerchantReferenceIdError<const MAX_LENGTH: u8, const MIN_LENGTH: u8> {
pub(crate) enum LengthIdError<const MAX_LENGTH: u8, const MIN_LENGTH: u8> {
#[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<const MAX_LENGTH: u8, const MIN_LENGTH:
AlphanumericIdError(AlphaNumericIdError),
}
impl From<AlphaNumericIdError> for MerchantReferenceIdError<0, 0> {
impl From<AlphaNumericIdError> for LengthIdError<0, 0> {
fn from(alphanumeric_id_error: AlphaNumericIdError) -> Self {
Self::AlphanumericIdError(alphanumeric_id_error)
}
}
impl<const MAX_LENGTH: u8, const MIN_LENGTH: u8> MerchantReferenceId<MAX_LENGTH, MIN_LENGTH> {
impl<const MAX_LENGTH: u8, const MIN_LENGTH: u8> LengthId<MAX_LENGTH, MIN_LENGTH> {
/// Generates new [MerchantReferenceId] from the given input string
pub fn from(
input_string: Cow<'static, str>,
) -> Result<Self, MerchantReferenceIdError<MAX_LENGTH, MIN_LENGTH>> {
) -> Result<Self, LengthIdError<MAX_LENGTH, MIN_LENGTH>> {
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<const MAX_LENGTH: u8, const MIN_LENGTH: u8> MerchantReferenceId<MAX_LENGTH,
pub fn new(prefix: &str) -> 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<MAX_LENGTH, MIN_LENGTH>
for LengthId<MAX_LENGTH, MIN_LENGTH>
{
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
@ -145,7 +158,7 @@ impl<'de, const MAX_LENGTH: u8, const MIN_LENGTH: u8> Deserialize<'de>
}
impl<DB, const MAX_LENGTH: u8, const MIN_LENGTH: u8> ToSql<sql_types::Text, DB>
for MerchantReferenceId<MAX_LENGTH, MIN_LENGTH>
for LengthId<MAX_LENGTH, MIN_LENGTH>
where
DB: Backend,
String: ToSql<sql_types::Text, DB>,
@ -155,16 +168,14 @@ where
}
}
impl<const MAX_LENGTH: u8, const MIN_LENGTH: u8> Display
for MerchantReferenceId<MAX_LENGTH, MIN_LENGTH>
{
impl<const MAX_LENGTH: u8, const MIN_LENGTH: u8> Display for LengthId<MAX_LENGTH, MIN_LENGTH> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0 .0)
}
}
impl<DB, const MAX_LENGTH: u8, const MIN_LENGTH: u8> FromSql<sql_types::Text, DB>
for MerchantReferenceId<MAX_LENGTH, MIN_LENGTH>
for LengthId<MAX_LENGTH, MIN_LENGTH>
where
DB: Backend,
String: FromSql<sql_types::Text, DB>,
@ -235,7 +246,7 @@ mod merchant_reference_id_tests {
#[test]
fn test_valid_reference_id() {
let parsed_merchant_reference_id =
serde_json::from_str::<MerchantReferenceId<MAX_LENGTH, MIN_LENGTH>>(VALID_REF_ID_JSON);
serde_json::from_str::<LengthId<MAX_LENGTH, MIN_LENGTH>>(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<MAX_LENGTH, MIN_LENGTH>,
>(INVALID_REF_ID_JSON);
let parsed_merchant_reference_id =
serde_json::from_str::<LengthId<MAX_LENGTH, MIN_LENGTH>>(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<MAX_LENGTH, MIN_LENGTH>,
>(INVALID_REF_ID_JSON);
let parsed_merchant_reference_id =
serde_json::from_str::<LengthId<MAX_LENGTH, MIN_LENGTH>>(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<MAX_LENGTH, MIN_LENGTH>,
>(INVALID_REF_ID_LENGTH);
let parsed_merchant_reference_id =
serde_json::from_str::<LengthId<MAX_LENGTH, MIN_LENGTH>>(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::<MAX_LENGTH, MIN_LENGTH>::from(INVALID_REF_ID_LENGTH.into());
LengthId::<MAX_LENGTH, MIN_LENGTH>::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)));
}
}

View File

@ -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<MAX_ALLOWED_MERCHANT_REFERENCE_ID_LENGTH, MIN_REQUIRED_MERCHANT_REFERENCE_ID_LENGTH>,
);
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<Self, errors::ValidationError> {
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<DB> ToSql<sql_types::Text, DB> for CustomerId
where
DB: Backend,
MerchantReferenceId<
MAX_ALLOWED_MERCHANT_REFERENCE_ID_LENGTH,
MIN_REQUIRED_MERCHANT_REFERENCE_ID_LENGTH,
>: ToSql<sql_types::Text, DB>,
LengthId<MAX_ALLOWED_MERCHANT_REFERENCE_ID_LENGTH, MIN_REQUIRED_MERCHANT_REFERENCE_ID_LENGTH>:
ToSql<sql_types::Text, DB>,
{
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<DB> FromSql<sql_types::Text, DB> for CustomerId
where
DB: Backend,
MerchantReferenceId<
MAX_ALLOWED_MERCHANT_REFERENCE_ID_LENGTH,
MIN_REQUIRED_MERCHANT_REFERENCE_ID_LENGTH,
>: FromSql<sql_types::Text, DB>,
LengthId<MAX_ALLOWED_MERCHANT_REFERENCE_ID_LENGTH, MIN_REQUIRED_MERCHANT_REFERENCE_ID_LENGTH>:
FromSql<sql_types::Text, DB>,
{
fn from_sql(value: DB::RawValue<'_>) -> diesel::deserialize::Result<Self> {
MerchantReferenceId::<
LengthId::<
MAX_ALLOWED_MERCHANT_REFERENCE_ID_LENGTH,
MIN_REQUIRED_MERCHANT_REFERENCE_ID_LENGTH,
>::from_sql(value)

View File

@ -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<MAX_ALLOWED_MERCHANT_REFERENCE_ID_LENGTH, MIN_REQUIRED_MERCHANT_REFERENCE_ID_LENGTH>,
);
/// 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<DB> Queryable<sql_types::Text, DB> for MerchantId
where
DB: Backend,
Self: FromSql<sql_types::Text, DB>,
{
type Row = Self;
fn build(row: Self::Row) -> diesel::deserialize::Result<Self> {
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<Self, errors::ValidationError> {
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<DB> ToSql<sql_types::Text, DB> for MerchantId
where
DB: Backend,
LengthId<MAX_ALLOWED_MERCHANT_REFERENCE_ID_LENGTH, MIN_REQUIRED_MERCHANT_REFERENCE_ID_LENGTH>:
ToSql<sql_types::Text, DB>,
{
fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, DB>) -> diesel::serialize::Result {
self.0.to_sql(out)
}
}
impl<DB> FromSql<sql_types::Text, DB> for MerchantId
where
DB: Backend,
LengthId<MAX_ALLOWED_MERCHANT_REFERENCE_ID_LENGTH, MIN_REQUIRED_MERCHANT_REFERENCE_ID_LENGTH>:
FromSql<sql_types::Text, DB>,
{
fn from_sql(value: DB::RawValue<'_>) -> diesel::deserialize::Result<Self> {
LengthId::<
MAX_ALLOWED_MERCHANT_REFERENCE_ID_LENGTH,
MIN_REQUIRED_MERCHANT_REFERENCE_ID_LENGTH,
>::from_sql(value)
.map(Self)
}
}

View File

@ -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<const MAX_LENGTH: u8, const MIN_LENGTH: u8>(
/// Generate a ReferenceId with the default length with the given prefix
fn generate_ref_id_with_default_length<const MAX_LENGTH: u8, const MIN_LENGTH: u8>(
prefix: &str,
) -> MerchantReferenceId<MAX_LENGTH, MIN_LENGTH> {
MerchantReferenceId::<MAX_LENGTH, MIN_LENGTH>::new(prefix)
) -> LengthId<MAX_LENGTH, MIN_LENGTH> {
LengthId::<MAX_LENGTH, MIN_LENGTH>::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());

View File

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

View File

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

View File

@ -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<i32>,
pub merchant_id: String,
pub return_url: Option<String>,
pub enable_payment_response_hash: bool,
pub payment_response_hash_key: Option<String>,
pub redirect_to_merchant_with_http_post: bool,
pub merchant_name: OptionalEncryptableName,
pub merchant_details: OptionalEncryptableValue,
pub webhook_details: Option<serde_json::Value>,
pub sub_merchants_enabled: Option<bool>,
pub parent_merchant_id: Option<String>,
pub publishable_key: String,
pub storage_scheme: MerchantStorageScheme,
pub locker_id: Option<String>,
pub metadata: Option<pii::SecretSerdeValue>,
pub routing_algorithm: Option<serde_json::Value>,
pub primary_business_details: serde_json::Value,
pub frm_routing_algorithm: Option<serde_json::Value>,
pub created_at: time::PrimitiveDateTime,
pub modified_at: time::PrimitiveDateTime,
pub intent_fulfillment_time: Option<i64>,
pub payout_routing_algorithm: Option<serde_json::Value>,
pub organization_id: String,
pub is_recon_enabled: bool,
pub default_profile: Option<String>,
pub recon_status: diesel_models::enums::ReconStatus,
pub payment_link_config: Option<serde_json::Value>,
pub pm_collect_link_config: Option<serde_json::Value>,
}
#[allow(clippy::large_enum_variant)]
#[derive(Debug)]
pub enum MerchantAccountUpdate {
Update {
merchant_name: OptionalEncryptableName,
merchant_details: OptionalEncryptableValue,
return_url: Option<String>,
webhook_details: Option<serde_json::Value>,
sub_merchants_enabled: Option<bool>,
parent_merchant_id: Option<String>,
enable_payment_response_hash: Option<bool>,
payment_response_hash_key: Option<String>,
redirect_to_merchant_with_http_post: Option<bool>,
publishable_key: Option<String>,
locker_id: Option<String>,
metadata: Option<pii::SecretSerdeValue>,
routing_algorithm: Option<serde_json::Value>,
primary_business_details: Option<serde_json::Value>,
intent_fulfillment_time: Option<i64>,
frm_routing_algorithm: Option<serde_json::Value>,
payout_routing_algorithm: Option<serde_json::Value>,
default_profile: Option<Option<String>>,
payment_link_config: Option<serde_json::Value>,
pm_collect_link_config: Option<serde_json::Value>,
},
StorageSchemeUpdate {
storage_scheme: MerchantStorageScheme,
},
ReconUpdate {
recon_status: diesel_models::enums::ReconStatus,
},
UnsetDefaultProfile,
ModifiedAtUpdate,
}
impl From<MerchantAccountUpdate> 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<Self::DstType, ValidationError> {
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<Vec<u8>>,
) -> CustomResult<Self, ValidationError>
where
Self: Sized,
{
let publishable_key =
item.publishable_key
.ok_or(ValidationError::MissingRequiredField {
field_name: "publishable_key".to_string(),
})?;
async {
Ok::<Self, error_stack::Report<common_utils::errors::CryptoError>>(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<Self::NewDstType, ValidationError> {
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<api_models::enums::Connector> {
let metadata: Option<api_models::admin::MerchantAccountMetadata> =
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)
}
}

View File

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

View File

@ -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::ApiDoc as utoipa::OpenApi>::openapi();
#[allow(clippy::expect_used)]
std::fs::write(
file_path,
<openapi::ApiDoc as utoipa::OpenApi>::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}'");
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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<api::MerchantAccountResponse> {
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<common_utils::errors::CryptoError>>(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<domain::MerchantAccount>;
}
#[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<domain::MerchantAccount> {
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<common_utils::errors::CryptoError>>(
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<String>) -> 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<String> {
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<admin_types::PrimaryBusinessDetails>,
},
/// 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<Vec<admin_types::PrimaryBusinessDetails>>) -> 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<diesel_models::business_profile::BusinessProfile> {
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<admin_types::PrimaryBusinessDetails>,
) -> RouterResult<Vec<diesel_models::business_profile::BusinessProfile>> {
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<domain::MerchantAccount> {
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<common_utils::errors::CryptoError>>(
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())

View File

@ -210,7 +210,7 @@ pub async fn get_merchant_fingerprint_secret(
state: &SessionState,
merchant_id: &str,
) -> RouterResult<String> {
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,

View File

@ -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<String>,
currency: Option<api_models::enums::Currency>,
client_secret: Option<String>,
) -> 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(

View File

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

View File

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

View File

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

View File

@ -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<AppState>,
@ -41,6 +27,7 @@ pub async fn merchant_account_create(
))
.await
}
/// Merchant Account - Retrieve
///
/// Retrieve a merchant account details.

View File

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

View File

@ -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",
},

View File

@ -15,9 +15,10 @@ use crate::{
types::{domain, storage, transformers::ForeignTryFrom},
};
impl TryFrom<domain::MerchantAccount> for MerchantAccountResponse {
#[cfg(not(feature = "v2"))]
impl ForeignTryFrom<domain::MerchantAccount> for MerchantAccountResponse {
type Error = error_stack::Report<errors::ParsingError>;
fn try_from(item: domain::MerchantAccount) -> Result<Self, Self::Error> {
fn foreign_try_from(item: domain::MerchantAccount) -> Result<Self, Self::Error> {
let primary_business_details: Vec<api_models::admin::PrimaryBusinessDetails> = item
.primary_business_details
.parse_value("primary_business_details")?;
@ -39,7 +40,7 @@ impl TryFrom<domain::MerchantAccount> 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<domain::MerchantAccount> for MerchantAccountResponse {
}
}
#[cfg(feature = "v2")]
impl ForeignTryFrom<domain::MerchantAccount> for MerchantAccountResponse {
type Error = error_stack::Report<errors::ValidationError>;
fn foreign_try_from(item: domain::MerchantAccount) -> Result<Self, Self::Error> {
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<storage::business_profile::BusinessProfile> for BusinessProfileResponse {
type Error = error_stack::Report<errors::ParsingError>;

View File

@ -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::*;

View File

@ -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<i32>,
pub merchant_id: String,
pub return_url: Option<String>,
pub enable_payment_response_hash: bool,
pub payment_response_hash_key: Option<String>,
pub redirect_to_merchant_with_http_post: bool,
pub merchant_name: OptionalEncryptableName,
pub merchant_details: OptionalEncryptableValue,
pub webhook_details: Option<serde_json::Value>,
pub sub_merchants_enabled: Option<bool>,
pub parent_merchant_id: Option<String>,
pub publishable_key: Option<String>,
pub storage_scheme: MerchantStorageScheme,
pub locker_id: Option<String>,
pub metadata: Option<pii::SecretSerdeValue>,
pub routing_algorithm: Option<serde_json::Value>,
pub primary_business_details: serde_json::Value,
pub frm_routing_algorithm: Option<serde_json::Value>,
pub created_at: time::PrimitiveDateTime,
pub modified_at: time::PrimitiveDateTime,
pub intent_fulfillment_time: Option<i64>,
pub payout_routing_algorithm: Option<serde_json::Value>,
pub organization_id: String,
pub is_recon_enabled: bool,
pub default_profile: Option<String>,
pub recon_status: diesel_models::enums::ReconStatus,
pub payment_link_config: Option<serde_json::Value>,
pub pm_collect_link_config: Option<serde_json::Value>,
}
#[allow(clippy::large_enum_variant)]
#[derive(Debug)]
pub enum MerchantAccountUpdate {
Update {
merchant_name: OptionalEncryptableName,
merchant_details: OptionalEncryptableValue,
return_url: Option<String>,
webhook_details: Option<serde_json::Value>,
sub_merchants_enabled: Option<bool>,
parent_merchant_id: Option<String>,
enable_payment_response_hash: Option<bool>,
payment_response_hash_key: Option<String>,
redirect_to_merchant_with_http_post: Option<bool>,
publishable_key: Option<String>,
locker_id: Option<String>,
metadata: Option<pii::SecretSerdeValue>,
routing_algorithm: Option<serde_json::Value>,
primary_business_details: Option<serde_json::Value>,
intent_fulfillment_time: Option<i64>,
frm_routing_algorithm: Option<serde_json::Value>,
payout_routing_algorithm: Option<serde_json::Value>,
default_profile: Option<Option<String>>,
payment_link_config: Option<serde_json::Value>,
pm_collect_link_config: Option<serde_json::Value>,
},
StorageSchemeUpdate {
storage_scheme: MerchantStorageScheme,
},
ReconUpdate {
recon_status: diesel_models::enums::ReconStatus,
},
UnsetDefaultProfile,
ModifiedAtUpdate,
}
impl From<MerchantAccountUpdate> 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<Self::DstType, ValidationError> {
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<Vec<u8>>,
) -> CustomResult<Self, ValidationError>
where
Self: Sized,
{
async {
Ok::<Self, error_stack::Report<common_utils::errors::CryptoError>>(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<Self::NewDstType, ValidationError> {
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<api_models::enums::Connector> {
let metadata: Option<api_models::admin::MerchantAccountMetadata> =
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)
}
}

View File

@ -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<UserCompanyName> for MerchantName {
// We should ideally not get this error because all the validations are done for company name
type Error = error_stack::Report<UserErrors>;
fn try_from(company_name: UserCompanyName) -> Result<Self, Self::Error> {
Self::new(company_name.get_secret()).change_context(UserErrors::CompanyNameParsingError)
}
}
impl NewUserMerchant {
pub fn get_company_name(&self) -> Option<String> {
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<admin_api::MerchantAccountCreate> {
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<admin_api::MerchantAccountCreate> {
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)

View File

@ -1007,6 +1007,10 @@ pub async fn flatten_join_error<T>(handle: Handle<T>) -> RouterResult<T> {
}
}
pub(crate) fn get_merchant_fingerprint_secret_key(merchant_id: impl AsRef<str>) -> String {
format!("fingerprint_secret_{}", merchant_id.as_ref())
}
#[cfg(test)]
mod tests {
use crate::utils;